C 语言字符串操作

#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <ctype.h>

char *strupr_t(char *str)
{
	char *orign=str;
	for (; *str!='\0'; str++)
		*str = toupper(*str);
	return orign;
}

char *strlowr_t(char *str)
{
	char *orign=str;
	for (; *str!='\0'; str++)
		*str = tolower(*str);
	return orign;
}

//字符串拷贝(strcpy, strncpy)
void testStrCpy(){
	char dest[1024] = {0};
	char *str = "abcde";
	//strcpy()函数会将源字符串中的结束符('\0')也拷贝到目的字符串中。
    //注意,strcpy()可能会导致溢出。
	strcpy(dest,str);

	printf("%s\n", dest);


	char dest2[7] = {0};
	char *src2 = "abcde";
	dest2[5] = 'A';
	//该函数从源字符串中拷贝n个字符到目的字符串;如果源字符串长度不足,则用 NULL 填充,以保证将n个字符写入目的字符串中;如果源字符串中前n个字符不包含字符串结束符,函数不会为目的字符串添加上结束符。
	//所以如果有需要,应该在拷贝后自己在目的字符串尾部添加结束符。
	strncpy(dest2, src2, 5);
	printf("%s\n", dest2);
}

void testStrCmp()
{
	char *s11 = "abcde";
	char *s12 = "abcef";
	char *s13 = "ad";
	//从这个结果可以发现,strcmp()是根据字典序来对字符串进行比较的。进一步的,还可以发现strcmp()的返回值是比较过程中最后一次比较时两个字符的值的差
	printf("compare(%s, %s) -> %d\n", s11, s12, strcmp(s11, s12));
	printf("compare(%s, %s) -> %d\n", s12, s13, strcmp(s12, s13));
	printf("compare(%s, %s) -> %d\n", s11, s13, strcmp(s11, s13));

	char *s21 = "abcde";
	char *s22 = "abcfg";
	char *s23 = "ad";
	//和strcmp()的区别是,strncmp()只对s1和s2的前n个字节进行比较。
	printf("compare(%s, %s, 4) -> %d\n", s21, s22, strncmp(s21, s22, 4));
	printf("compare(%s, %s, 2) -> %d\n", s21, s23, strncmp(s21, s23, 2));
	printf("compare(%s, %s, 3) -> %d\n", s22, s23, strncmp(s22, s23, 3));

	char *s31 = "AbcdE";
	char *s32 = "abcdE";
    //使用strcasecmp()应该包含 strings.h 而不是 string.h
    //strcasecmp()在比较时不区分大小写
    //strncasecmp()之于strcasecmp()就如strncmp()之于strcmp()
	printf("compare(%s, %s) with case -> %d\n", s31, s32, strcmp(s31, s32));
	printf("compare(%s, %s) ignore case -> %d\n", s31, s32, strcasecmp(s31, s32));
}

void testStrCat()
{
	char dest[1024] = "hello ";
	char *src = "world!";
	//strcat()首先会覆盖掉目的字符串的结束符,然后把源字符串的内容追加到后面,并在最后添加结束符。如果目的字符串缓冲区长度不够,将导致溢出。
    //strcat()在操作完成后,返回目的字符串的首地址,这样可以方便地进行链式操作。
	printf("%s\n", strcat(dest, src));

	char dest2[1024] = "hello ";
	char *src2 = "world!kkkkaaaa";
	//strncat()将最多n个字节的内容追加到目的字符串尾部,并且会在追加后添加终止符号。
    //同strcat()一样,它返回目的字符串的首地址。
	printf("%s\n", strncat(dest2, src2,6));
}

void testStrSearch()
{
	char *s1 = "hello world!";
	char c = 'l';
	//strchr()返回一个字符指针,指向指定字符在指定字符串中第一次出现的位置。如果在指定字符串中没有找到指定字符,则返回 NULL 。该函数的第二个参数按理来说应当是一个字符,不过标准库中确实是int类型。
	printf("%s\n", strchr(s1, c));
	//strrchr()和strchr()类似,但它返回的是指定字符在指定字符串中最后一次出现的位置。如果未找到,同样返回 NULL 。
	printf("%s\n", strrchr(s1, c));
	//strchrnul()的功能和strchr()只有细微的区别,那就是,当没有找到指定字符时,strchrnul()不返回 NULL ,而是返回字符串结束符的位置。
	//这里由于strchrnul()的特性,没办法通过打印字符串来了解strchrnul()的操作
	//'strchrnull' is invalid in C99
	//printf("%p, %p\n", s1, strchrnull(s1, 'f'));

	char *accept = "wo";
	//strpbrk()和strchr()的区别在于,strchr()是从字符串里搜索 一个字符 ,而strpbrk()则是在字符串里搜索 一个字符集中的字符 ,看第二个参数就明白了。strpbrk()遍历字符串,如果发现某个字符在指定的 字符集 中,则立即返回指向该字符的指针。如果最后没有找到任何在指定字符集中的字符,则返回 NULL 。
	printf("%s\n", strpbrk(s1, accept));
}

void testStrSplit()
{
	char s[1024] = "abc;lsdk:lskdj,;slsj";
	char *delm = ";:,";
	char *result = NULL;
	int len = strlen(s);
	int i = 0;
    //strtok()根据第二个参数指定的分隔符(可能存在多个不同的分隔符)将指定字符串分割成多个子串。通过多次调用strtok(),可以依次获得字符串的多个子串的首地址。要注意的是,除了第一次调用时将待分割字符串作为第一个参数,后续的调用要将第一个参数置为 NULL 。当字符串已经无法再分割时,strtok()返回 NULL 。
    //除了上面说过的strtok()的用法外,还要注意的是,作为待分割的字符串,它必须是 可更改的 。否则虽然可以通过编译,但运行会出错。要理解这个现象,首先要了解strtok()的内部机制。
	result = strtok(s, delm);
	while (result != NULL) {
		printf("Source:%s, Sub:%s\n", s, result);
		result = strtok(NULL, delm);
	}
}

/*
T	o	 	b	e	 	o	r	 	n	o	t	 	t	o	 	b	e
84	111	32	98	101	32	111	114	32	110	111	116	32	116	111	32	98	101
84	111	0	98	101	32	111	114	32	110	111	116	32	116	111	32	98	101
84	111	0	98	101	0	111	114	32	110	111	116	32	116	111	32	98	101
84	111	0	98	101	0	111	114	0	110	111	116	32	116	111	32	98	101
84	111	0	98	101	0	111	114	0	110	111	116	0	116	111	32	98	101
84	111	0	98	101	0	111	114	0	110	111	116	0	116	111	0	98	101
84	111	0	98	101	0	111	114	0	110	111	116	0	116	111	0	98	101

可以看到,s中的分隔符,逐次地被置为'\0'即字符串结束符。这就是strtok()分割字符串的内部原理了。而strtok()返回的指针,其实就是s中各个子串的起始位置了。如果s指向的内容是无法被修改的,那么strtok()自然也就无法将原先的分隔符置为字符结束符了。

当然了,由于源字符串会被修改,在实际中,如果需要,可以用strdup()来建立一个源字符串的副本。
*/
void testStrSplit2()
{
	char s[64] = "To be or not to be";
	char *delm = " ";
	char *result = NULL;
	int i =0,len = strlen(s);
	for (i =0;i<len;i++){
		printf("%c ", s[i]);
	}
	printf("\n");
	for (i=0;i<len;i++){
		printf("%d ",(int)s[i]);
	}
	printf("\n");

	result = strtok(s, delm);

	while (result != NULL){
		for (i=0;i<len;i++){
			printf("%d ",(int)s[i]);
		}
		printf("\n");
		result = strtok(NULL, delm);
	}
}

void testStrSplit3()
{
	char s[64] = "Hello World";
	char *delm = " ";
	char *result = NULL, *ptr = NULL;

	printf("Source:%p\n", s);
	//char *strtok_r(char *str, const char *delim, char **saveptr);
	//strtok_r()是Linux下的strtok()的可重入版本(线程安全版本),它比strtok()多了一个参数 saveptr ,这个参数用于在分割字符串时保存上下文。
	result = strtok_r(s, delm, &ptr);

	while (result != NULL){
		printf("Result:%p\t", result);
		printf("Saveptr:%p\t", ptr);
		printf("---%s\t", result);
		printf("---%s\n", ptr);
		//可以看到,saveptr这个指针在每次调用strtok_r()后就指向了未分割的部分的首地址。相对地,strtok()则是在内部有一个静态缓冲区,通过这个静态缓冲区来记录未处理的起始位置,所以strtok()不是线程安全的。
		result = strtok_r(NULL, delm, &ptr);
	}

}

//因为和strtok()的这个不同之处,strsep不需要区分第一次调用后后续的连续调用,可以用统一的操作来对字符串进行分割。
void testStrSplit4()
{
	char s[64] = "To be or not to be";
	char *source = s;
	char *delm = " ";
	char *result = NULL;

	while (source != NULL){
		printf("Source: %s | ", source);
		result = strsep(&source,delm);
		printf("result: %s | ", result);
	}
}

void testStrMatch()
{
	char *s = "To be or not to be.";
	char *p = "be";
	//strstr()返回字符串needle在字符串haystack中第一次出现的位置;如果没有匹配,则返回 NULL 。
	printf("%s\n", strstr(s, p));
}

void testStrDup()
{
	char *s = "aabbccddee";
	char *dup = strdup(s);
	//strdup()调用malloc()分配一块内存并将字符串s的内容拷贝进去,产生s的副本。要注意的是,在最后应该调用free()来释放副本。
	printf("%s\n",dup);
	free(dup);

	char *s1 = "poiuytrewq";
	//strndup()和strdup()类似,但最多只拷贝s的前n个字节。如果s的长度大于n,还会在副本后添加终止符。
	char *dup1 = strndup(s1, 4);
	printf("%s\n",dup1);
	free(dup1);

	//strdupa()和strdup()类似,但在分配内存时,它使用alloca()而不是malloc()。
	//strndupa()之于strdupa()就如strndup()之于strdup(),不再赘述。
}

/*
函数名: swab
功  能: 交换字节
用  法: void swab (char *from, char *to, int nbytes);
*/
void testStringSwab()
{
	char source[15] = "rFna koBlrna d";
	char target[15];

	swab(source,target,strlen(source));
	printf("%s\n",target);
}

/*
函数名: strupr
功  能: 将串中的小写字母转换为大写字母
用  法: char *strupr(char *str);
*/
void testStrUpper()
{
	char string[100] = "abcdefghijklmnopqrstuvwxyz";
	char *ptr;

   /* converts string to upper case characters */
	ptr = strupr_t(string);
	printf("%s\n", ptr);
}

/*
函数名: strtol
功  能: 将串转换为长整数
用  法: long strtol(char *str, char **endptr, int base);
*/
void testStrtol()
{
	char *string = "87654321", *endptr;
	long lnumber;

   /* strtol converts string to long integer  */
	lnumber = strtol(string, &endptr, 10);
	printf("string = %s  long = %ld\n", string, lnumber);
}

/*
函数名: strtod
功  能: 将字符串转换为double型值
用  法: double strtod(char *str, char **endptr);
*/
void testStrtod()
{
	char input[80], *endptr;
	double value;

	printf("Enter a floating point number:");
	gets(input);
	value = strtod(input, &endptr);
	printf("The string is %s the number is %lf\n", input, value);
}

/*
函数名: strspn
功  能: 在串中查找指定字符集的子集的第一次出现
用  法: int strspn(char *str1, char *str2);
*/
void testStrspan()
{
	char *string1 = "1234567890";
	char *string2 = "123DC8";
	int length;

	length = strspn(string1, string2);
	printf("Character where strings differ is at position %d\n", length);
}


/*
函数名: strset
功  能: 将一个串中的所有字符都设为指定字符
用  法: char *strset(char *str, char c);
*/
// void testStrset()
// {
// 	char string[10] = "123456789";
// 	char symbol = 'c';

// 	printf("Before strset(): %s\n", string);
// 	strset(string, symbol);
// 	printf("After strset():  %s\n", string);

// }

/*
函数名: strnset
功  能: 将一个串中的n个字符都设为指定字符
用  法: char *strnset(char *str, char ch, unsigned n);
*/
// void testStrnset()
// {
// 	char *string = "abcdefghijklmnopqrstuvwxyz";
// 	char letter = 'x';

// 	printf("string before strnset: %s\n", string);
// 	strnset(string, letter, 13);
// 	printf("string after  strnset: %s\n", string);
// }

void strrev(char *head)
{
  if (!head) return;
  char *tail = head;
  // find the 0 terminator, like head+strlen
  while(*tail) ++tail;
  // tail points to the last real char
  --tail;               
  // head still points to the first
  for( ; head < tail; ++head, --tail) {
      // walk pointers inwards until they meet or cross in the middle
      char h = *head, t = *tail;
      *head = t;           // swapping as we go
      *tail = h;
  }
}

void reverse(char s[])
{
    int length = strlen(s) ;
    int c, i, j;

    for (i = 0, j = length - 1; i < j; i++, j--)
    {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

/*
函数名: strrev
功  能: 串倒转
用  法: char *strrev(char *str);
*/
void testStrrev()
{
	char forward[15] = "string";

	printf("Before strrev(): %s\n", forward);
	strrev(forward);
	printf("After strrev():  %s\n", forward);
	reverse(forward);
	printf("After reverse():  %s\n", forward);
}


/*
函数名: strcspn
功  能: 在串中查找第一个给定字符集内容的段
用  法: int strcspn(char *str1, char *str2);
*/
void testStrcspn()
{
	char *string1 = "1234567890";
	char *string2 = "747DC8";
	int length;

	length = strcspn(string1, string2);
	printf("Character where strings intersect is at position %d\n", length);
}



/*
atof(将字符串转换成浮点型数)
atoi(将字符串转换成整型数)
atol(将字符串转换成长整型数)
strtod(将字符串转换成浮点数)
strtol(将字符串转换成长整型数)
strtoul(将字符串转换成无符号长整型数)
toascii(将整型数转换成合法的ASCII 码字符)
toupper(将小写字母转换成大写字母)
tolower(将大写字母转换成小写字母)
atof(将字符串转换成浮点型数)
*/

int main(int argc, char *argv[])
{
	testStrCpy();
	testStrCmp();
	testStrCat();
	testStrSearch();
	testStrSplit();
	testStrSplit2();
	testStrSplit3();
	testStrSplit4();
	testStrMatch();
	testStrDup();

	testStrspan();
	// testStrset();
	// testStrnset();
	testStrrev();
	testStrcspn();
	testStrtod();
	testStrtol();
	testStrUpper();
	return 0;
}

How do I install Java on Mac OSX allowing version switching?

原文链接

note: These solutions work for various versions of Java including Java 8 and the new Java 13, and for any other previous Java version covered by the listed version managers. This includes alternative JDK’s from OpenJDK, Oracle, IBM, Azul, Amazon Correto, Graal and more. Easily work with Java 7, Java 8, Java 9, Java 10, Java 11, Java 12, and Java 13!

You have a few options of how to do the installation as well as manage JDK switching. Installation can be done by Homebrew, SDKMANJabba, or a manual install. Switching can be done by JEnvSDKMANJabba, or manually by setting JAVA_HOME. All of these are described below.


Installation

First, install Java using whatever method you prefer including Homebrew, SDKMAN or a manual install of the tar.gz file. The advantages of a manual install is that the location of the JDK can be placed in a standardized location for Mac OSX.

Install with SDKMAN

This is a simple model in that it handles both installation and version switching, with a caveat that it installs the JDK into a non-standard directory.

<see below “Installing and Switching versions with SDKMAN”>

Install using Jabba

This is also a simple model in that both installation and version switching are handled by the same tool. The installations are made to a non-standard directory.

<see below “Installing and Switching versions with Jabba”>

Install manually from OpenJDK download page:

  1. Download OpenJDK for Mac OSX from http://jdk.java.net/ (for example Java 13)
  2. Unarchive the OpenJDK tar, and place the resulting folder (i.e. jdk-13.jdk) into your /Library/Java/JavaVirtualMachines/ folder since this is the standard and expected location of JDK installs. You can also install anywhere you want in reality.

Install with Homebrew

The version of Java available in Homebrew Cask previous to October 3, 2018 was indeed the Oracle JVM. Now however, it has now been updated to OpenJDK. Be sure to update Homebrew and then you will see the lastest version available for install.

  1. install Homebrew if you haven’t already. Make sure it is updated:
    brew update
  2. Add the casks tap, if you haven’t already (or you are not seeing older Java versions anymore with step #3):
    brew tap homebrew/cask-versions

    and for the AdoptOpenJDK versions, add that tap:

    brew tap adoptopenjdk/openjdk

    These casks change their Java versions often, and there might be other taps out there with additional Java versions.

  3. Look for installable versions:
    brew search java   

    or for AdoptOpenJDK versions:

    brew search jdk     
  4. Check the details on the version that will be installed:
    brew cask info java

    or for the AdoptOpenJDK version:

    brew cask info adoptopenjdk
  5. Install a specific version of the JDK such as java11adoptopenjdk8, or just java or adoptopenjdk for the current. For example:
    brew cask install java

    You can use the fully qualified path to older versions as well:

    brew cask install homebrew/cask-versions/java11

And these will be installed into /Library/Java/JavaVirtualMachines/ which is the traditional location expected on Mac OSX.

Other installation options:

Some other flavours of openJDK are:

Azul Systems Java Zulu certified builds of OpenJDK can be installed by following the instructions on their site.

Zulu® is a certified build of OpenJDK that is fully compliant with the Java SE standard. Zulu is 100% open source and freely downloadable. Now Java developers, system administrators, and end users can enjoy the full benefits of open source Java with deployment flexibility and control over upgrade timing.

Amazon Correto OpenJDK builds have an easy to use an installation package for version 8 or version 11 (other versions are coming), and installs to the standard /Library/Java/JavaVirtualMachines/ directory on Mac OSX.

Amazon Corretto is a no-cost, multiplatform, production-ready distribution of the Open Java Development Kit (OpenJDK). Corretto comes with long-term support that will include performance enhancements and security fixes. Amazon runs Corretto internally on thousands of production services and Corretto is certified as compatible with the Java SE standard. With Corretto, you can develop and run Java applications on popular operating systems, including Linux, Windows, and macOS.


Where is my JDK?!?!

To find locations of previously installed Java JDK’s installed at the default system locations, use:

/usr/libexec/java_home -V

Matching Java Virtual Machines (6):
13, x86_64: “OpenJDK 13” /Library/Java/JavaVirtualMachines/openjdk-13.jdk/Contents/Home 12, x86_64: “OpenJDK 12” /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
11, x86_64: “Java SE 11” /Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home
10.0.2, x86_64: “Java SE 10.0.2” /Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home
9, x86_64: “Java SE 9” /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home
1.8.0_144, x86_64: “Java SE 8” /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home

You can also report just the location of a specific Java version using -v. For example for Java 13:

/usr/libexec/java_home -v 13

/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

Knowing the location of the installed JDK’s is also useful when using tools like JEnv, or adding a local install to SDKMAN, or linking a system JDK in Jabba — and you need to know where to find them.

If you need to find JDK’s installed by other tools, check these locations:

  • SDKMAN installs to ~/.sdkman/candidates/java/
  • Jabba installs to ~/.jabba/jdk

Switching versions manually

The Java executable is a wrapper that will use whatever JDK is configured in JAVA_HOME, so you can change that to also change which JDK is in use.

For example, if you installed or untar’d JDK 13 to /Library/Java/JavaVirtualMachines/jdk-13.jdk if it is the highest version number it should already be the default, if not you could simply set:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

And now whatever Java executable is in the path will see this and use the correct JDK.

Using the /usr/libexec/java_home utility as previously described helps you to create aliases or to run commands to change Java versions by identifying the locations of different JDK installations. For example, creating shell aliases in your .profile or .bash_profile to change JAVA_HOME for you:

export JAVA_8_HOME=$(/usr/libexec/java_home -v1.8)
export JAVA_9_HOME=$(/usr/libexec/java_home -v9)
export JAVA_10_HOME=$(/usr/libexec/java_home -v10)
export JAVA_11_HOME=$(/usr/libexec/java_home -v11)
export JAVA_12_HOME=$(/usr/libexec/java_home -v12)
export JAVA_13_HOME=$(/usr/libexec/java_home -v13)

alias java8='export JAVA_HOME=$JAVA_8_HOME'
alias java9='export JAVA_HOME=$JAVA_9_HOME'
alias java10='export JAVA_HOME=$JAVA_10_HOME'
alias java11='export JAVA_HOME=$JAVA_11_HOME'
alias java12='export JAVA_HOME=$JAVA_12_HOME'
alias java13='export JAVA_HOME=$JAVA_13_HOME'

# default to Java 13
java13

Then to change versions, just use the alias.

java8
java -version

java version “1.8.0_144”

Of course, setting JAVA_HOME manually works too!


Switching versions with JEnv

JEnv expects the Java JDK’s to already exist on the machine and can be in any location. Typically you will find installed Java JDK’s in /Library/Java/JavaVirtualMachines/. JEnv allows setting the global version of Java, one for the current shell, and a per-directory local version which is handy when some projects require different versions than others.

  1. Install JEnv if you haven’t already, instructions on the site http://www.jenv.be/ for manual install or using Homebrew.
  2. Add any Java version to JEnv (adjust the directory if you placed this elsewhere):
    jenv add /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home
  3. Set your global version using this command:
    jenv global 13

You can also add other existing versions using jenv add in a similar manner, and list those that are available. For example Java 8:

jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home 
jenv versions

See the JEnv docs for more commands. You may now switch between any Java versions (Oracle, OpenJDK, other) at any time either for the whole system, for shells, or per local directory.

To help manage JAVA_HOME while using JEnv you can add the export plugin to do this for you.

$ jenv enable-plugin export
  You may restart your session to activate jenv export plugin echo export plugin activated

The export plugin may not adjust JAVA_HOME if it is already set, so you may need to clear this variable in your profile so that it can be managed by JEnv.

You can also use jenv exec <command> <parms...> to run single commands with JAVA_HOME and PATH set correctly for that one command, which could include opening another shell.


Installing and Switching versions with SDKMAN

SDKMAN is a bit different and handles both the install and the switching. SDKMAN also places the installed JDK’s into its own directory tree, which is typically ~/.sdkman/candidates/java. SDKMAN allows setting a global default version, and a version specific to the current shell.

  1. Install SDKMAN from https://sdkman.io/install
  2. List the Java versions available to make sure you know the version ID
    sdk list java
  3. Install one of those versions, for example, Java 13:
    sdk install java 13.0.0-open 
  4. Make 13 the default version:
    sdk default java 13.0.0-open

    Or switch to 13 for the session:

    sdk use java 13.0.0-open

When you list available versions for installation using the list command, you will see a wide variety of distributions of Java:

sdk list java

And install additional versions, such as JDK 8:

sdk install java 8.0.181-oracle

SDKMAN can work with previously installed existing versions. Just do a local install giving your own version label and the location of the JDK:

sdk install java my-local-13 /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

And use it freely:

sdk use java my-local-13

More information is available in the SDKMAN Usage Guide along with other SDK’s it can install and manage.

SDKMAN will automatically manage your PATH and JAVA_HOME for you as you change versions.


Installing and Switching versions with Jabba

Jabba also handles both the install and the switching. Jabba also places the installed JDK’s into its own directory tree, which is typically ~/.jabba/jdk.

  1. Install Jabba by following the instructions on the home page.
  2. List available JDK’s
    jabba ls-remote
  3. Install Java JDK 12
    jabba install openjdk@1.12.0
  4. Use it:
    jabba use openjdk@1.12.0

You can also alias version names, link to existing JDK’s already installed, and find a mix of interesting JDK’s such as GraalVM, Adopt JDK, IBM JDK, and more. The complete usage guide is available on the home page as well.

Jabba will automatically manage your PATH and JAVA_HOME for you as you change versions.

shell后台并发执行实践

shell如何在后台执行

1.nohup命令
通常我们都是远程登录linux终端,而当我们退出终端时在之前终端运行的程序都会终止,有时候先想要退出终端也要程序继续执行这时nohup就登场了。nohup命令可以将程序以忽略挂起信号的方式运行起来,被运行的程序的输出信息将不会显示到终端。
nohup command > myout.file 2>&1 &

2.&后台执行
在命令后面加 & 可以让程序在后台执行
command &

3.Ctrl + z
当一个程序正在执行并且占用当前终端时我们同时按下 Ctrl + z ,这样就会把正在执行的前台程序放到后台挂起。

并发执行

1.正常执行

#!/bin/bash
Njob=10    #任务总数
for ((i=0; i<$Njob; i++)); do
{
	echo  "progress $i is sleeping for 1 seconds zzz…"
	sleep  1
}
done
echo -e "time-consuming: $SECONDS seconds"    #显示脚本执行耗时

执行结果

progress 0 is sleeping for 1 seconds zzz…
progress 1 is sleeping for 1 seconds zzz…
progress 2 is sleeping for 1 seconds zzz…
progress 3 is sleeping for 1 seconds zzz…
progress 4 is sleeping for 1 seconds zzz…
progress 5 is sleeping for 1 seconds zzz…
progress 6 is sleeping for 1 seconds zzz…
progress 7 is sleeping for 1 seconds zzz…
progress 8 is sleeping for 1 seconds zzz…
progress 9 is sleeping for 1 seconds zzz…
-e time-consuming: 10 seconds

2.并发后台执行

#!/bin/bash
Njob=10
for ((i=0; i<$Njob; i++)); do
    echo  "progress $i is sleeping for 3 seconds zzz…"
    sleep  3 &       #循环内容放到后台执行
done
wait      #等待循环结束再执行wait后面的内容
echo -e "time-consuming: $SECONDS seconds"    #显示脚本执行耗时

执行结果

progress 0 is sleeping for 3 seconds zzz…
progress 1 is sleeping for 3 seconds zzz…
progress 2 is sleeping for 3 seconds zzz…
progress 3 is sleeping for 3 seconds zzz…
progress 4 is sleeping for 3 seconds zzz…
progress 5 is sleeping for 3 seconds zzz…
progress 6 is sleeping for 3 seconds zzz…
progress 7 is sleeping for 3 seconds zzz…
progress 8 is sleeping for 3 seconds zzz…
progress 9 is sleeping for 3 seconds zzz…
-e time-consuming: 3 seconds

这种方式从功能上实现了使用shell脚本并行执行多个循环进程,但是它缺乏控制机制。

for设置了Njob次循环,同一时间Linux就触发Njob个进程一起执行。假设for里面执行的是scp,在没有pam_limits和cgroup限制的情况下,很有可能同一时刻过多的scp任务会耗尽系统的磁盘IO、连接数、带宽等资源,导致正常的业务受到影响。

一个应对办法是在for循环里面再嵌套一层循环,这样同一时间,系统最多只会执行内嵌循环限制值的个数的进程。不过还有一个问题,for后面的wait命令以循环中最慢的进程结束为结束(水桶效应)。如果嵌套循环中有某一个进程执行过程较慢,那么整体这一轮内嵌循环的执行时间就等于这个“慢”进程的执行时间,整体下来脚本的执行效率还是受到影响的。

分批并行的方式并发执行

#!/bin/bash
NQ=3
num=5
for ((i=0; i<$NQ; i++)); do
     for ((j=0; j<$num; j++)); do
         echo  "progress $i is sleeping for 3 seconds zzz…"
        sleep 3 &
     done
     wait
 done
#等待循环结束再执行wait后面的内容
echo -e "time-consuming: $SECONDS    seconds"    #显示脚本执行耗时

执行结果

 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 0 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 1 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
 progress 2 is sleeping for 3 seconds zzz…
-e time-consuming: 9    seconds

3.使用模拟队列来控制进程数量

要控制后台同一时刻的进程数量,需要在原有循环的基础上增加管理机制。

一个方法是以for循环的子进程PID做为队列元素,模拟一个限定最大进程数的队列(只是一个长度固定的数组,并不是真实的队列)。队列的初始长度为0,循环每创建一个进程,就让队列长度+1。当队列长度到达设置的并发进程限制数之后,每隔一段时间检查队列,如果队列长度还是等于限制值,那么不做操作,继续轮询;如果检测到有并发进程执行结束了,那么队列长度-1,轮询检测到队列长度小于限制值后,会启动下一个待执行的进程,直至所有等待执行的并发进程全部执行完。

#!/bin/bash
Njob=15 #任务总数
Nproc=5 #最大并发进程数

function PushQue {      #将PID值追加到队列中
           Que="$Que $1"
           Nrun=$(($Nrun+1))
}

function GenQue {       #更新队列信息,先清空队列信息,然后检索生成新的队列信息
           OldQue=$Que
           Que=""; Nrun=0
           for PID in $OldQue; do
                 if [[ -d /proc/$PID ]]; then
                        PushQue $PID
                 fi
           done
}

function ChkQue {       #检查队列信息,如果有已经结束了的进程的PID,那么更新队列信息
           OldQue=$Que
           for PID in $OldQue; do
                 if [[ ! -d /proc/$PID ]];   then
                 GenQue; break
                 fi
           done
}

for ((i=1; i<=$Njob; i++)); do
           echo "progress $i is sleeping for 3 seconds zzz…"
           sleep 3 &
           PID=$!
           PushQue $PID
           while [[ $Nrun -ge $Nproc ]]; do          # 如果Nrun大于Nproc,就一直ChkQue
                 ChkQue
                 sleep 0.1
           done
done
wait
echo -e "time-consuming: $SECONDS   seconds"    #显示脚本执行耗时

执行结果

progress 1 is sleeping for 3 seconds zzz…
progress 2 is sleeping for 3 seconds zzz…
progress 3 is sleeping for 3 seconds zzz…
progress 4 is sleeping for 3 seconds zzz…
progress 5 is sleeping for 3 seconds zzz…
progress 6 is sleeping for 3 seconds zzz…
progress 7 is sleeping for 3 seconds zzz…
progress 8 is sleeping for 3 seconds zzz…
progress 9 is sleeping for 3 seconds zzz…
progress 10 is sleeping for 3 seconds zzz…
progress 11 is sleeping for 3 seconds zzz…
progress 12 is sleeping for 3 seconds zzz…
progress 13 is sleeping for 3 seconds zzz…
progress 14 is sleeping for 3 seconds zzz…
progress 15 is sleeping for 3 seconds zzz…
-e time-consuming: 3   seconds

这种使用队列模型管理进程的方式在控制了后台进程数量的情况下,还能避免个别“慢”进程影响整体耗时的问题:

4.使用fifo管道特性来控制进程数量

管道是内核中的一个单向的数据通道,同时也是一个数据队列。具有一个读取端与一个写入端,每一端对应着一个文件描述符。
命名管道即FIFO文件,通过命名管道可以在不相关的进程之间交换数据。FIFO有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中。

FIFO有两种用途:

• FIFO由shell使用以便数据从一条管道线传输到另一条,为此无需创建临时文件,常见的操作cat file|grep keyword就是这种使用方式;
• FIFO用于客户进程-服务器进程程序中,已在客户进程与服务器进程之间传送数据,下面的例子将使用这种方式。

根据FIFO文件的读规则(参考http://www.cnblogs.com/yxmx/articles/1599187.html),如果有进程写打开FIFO,且当前FIFO内没有数据,对于设置了阻塞标志的读操作来说,将一直阻塞状态。

利用这一特性可以实现一个令牌机制。设置一个行数等于限定最大进程数Nproc的fifo文件,在for循环中设置创建一个进程时先read一次fifo文件,进程结束时再write一次fifo文件。如果当前子进程数达到限定最大进程数Nproc,则fifo文件为空,后续执行的并发进程被读fifo命令阻塞,循环内容被没有触发,直至有某一个并发进程执行结果并做写操作(相当于将令牌还给池子)。

需要注意的是,当并发数较大时,多个并发进程即使在使用sleep相同秒数模拟时,也会存在进程调度的顺序问题,因而并不是按启动顺序结束的,可能会后启动的进程先结束。

#!/bin/bash

Njob=15 #任务总数

Nproc=5 #最大并发进程数

mkfifo ./fifo.$$ && exec   9<>  ./fifo.$$     #通过文件描述符777访问fifo文件

for ((i=0; i<$Nproc; i++)); do  #向fifo文件先填充等于Nproc值的行数
  echo  "init time add $i" >&9
done
for ((i=0; i<$Njob; i++)); do
{
  read  -u  9             #从fifo文件读一行
  echo  "progress $i is sleeping for 3 seconds zzz…"
  sleep  3
  echo  "real time add $(($i+$Nproc))"  1>&9 #sleep完成后,向fifo文件重新写入一行
} &
done
wait
echo -e "time-consuming: $SECONDS seconds"
rm -f ./fifo.$$

执行结果

progress 0 is sleeping for 3 seconds zzz…
progress 1 is sleeping for 3 seconds zzz…
progress 2 is sleeping for 3 seconds zzz…
progress 3 is sleeping for 3 seconds zzz…
progress 4 is sleeping for 3 seconds zzz…
progress 5 is sleeping for 3 seconds zzz…
progress 6 is sleeping for 3 seconds zzz…
progress 8 is sleeping for 3 seconds zzz…
progress 12 is sleeping for 3 seconds zzz…
progress 13 is sleeping for 3 seconds zzz…
progress 9 is sleeping for 3 seconds zzz…
progress 11 is sleeping for 3 seconds zzz…
progress 14 is sleeping for 3 seconds zzz…
progress 10 is sleeping for 3 seconds zzz…
progress 7 is sleeping for 3 seconds zzz…
-e time-consuming: 10 seconds

原文地址:
Shell脚本实现并发多进程
Shell脚本并发执行

OKR 填写指南

一、OKR概述
OKR是一个目标管理工具。其中O指Objective,是团队或个人的工作目标;KR指Key Result,是一系列可以衡量的关键结果,用来判断Objective是否达成。在作业帮,OKR的制定和共享,是公司、团队和个人制定任务,对齐目标,协调和集中精力的重要手段。

二、OKR执行要点
工作目标设置应该激进,要使自己和团队感受到压力。
关键结果要容易打分衡量,不要模棱两可。
除保密事项外,OKR尽可能的公开,方便互相了解在忙些什么工作。
写好OKR,要求严格的聚焦,只写最重要的目标,不罗列堆砌。
OKR并不是绩效考核工具,但应该是自我检测工作成果的重要工具。
OKR不是一个共享的工作清单或者to-do list,而是一个管理精力,自我规划的重要工具。
没有经过沟通和对齐的OKR,相当于没有写,OKR的对齐方包括你的上级、同事和你承接需求(提出需求)的协同方。

三、设定激进、聚焦的目标

在每个OKR周期中,公司从CEO到全体员工都会制定自己的OKR,通常是三四个Objectives,每个Objective有3个左右的Key Results。这些OKR既有从上到下目标分解而来的,也有从基层收集的各种意见演化而来,所以OKR的制定是一个循环迭代和讨论的过程。

我们希望每一个人都制定激进的目标,这些目标看上去刚好处于 “这个周期完不成” 的边缘。事实证明,制定高目标,有助于我们取得优于普通水准的成果。 每个高目标都需要全情投入。所以OKR制定的另一方面,是严格地聚焦到重点方向,不贪多。但在选定的重点方向上,力求保证目标达成。

一些设定Objective的窍门:

选三五个目标即可,宁缺勿滥。太多目标,工作容易失去焦点,团队也会疲于应付。
描述最终状态,比如“上线xx功能”,“获得10%的市场份额”。
常规动作不要写进OKR,例如“继续推进”、“保持行业地位”。如果确是长期重要工作,要思考如何拆解成合理的周期OKR,而不是不经思考地写“继续做”。
团队leader是团队的大脑,更要注意排优先级和授权。

四、可衡量的Key Result

关键结果(Key Result,或简称KR)是用来评价目标是否达成的。考虑到这个功能,对关键结果最大的要求就是容易衡量,且直接支持目标的达成。

一些设定Key Result的窍门:

每个目标定3个左右KR即可,不多刻意多写。
注意,KR是用来衡量Objective的达成程度的,问一下自己“这个KR和相应的Objective有直接的支撑关系吗”。
要描述产出,而不是动作。产出是指类似于“发表一篇论文”这样的表述,而动作是指“进行研究并撰写论文”这样的表述。所以当一个KR中出现 “参与”、“分析”、“辅助”这样的词汇,或者描述过程的动词特别多时,可能就有问题了。
用客观、外部可观测和不模糊的表述。最好是你的同事也能够准确地对你的KR进行打分,这说明KR足够清晰。
五、常见错误
执行不好的OKR,不但对公司、团队和个人没有帮助,反而可能造成团队安于现状,甚至方向混淆、内耗。所以我们要避免常见的OKR执行错误,比如下面这些:

堆砌不重要的目标
OKR常常被写得很长、很多,很难用两三句阐述其中的重点。往往这样的OKR包含大量的常规工作。常规工作不是不可以写,而是要判断,这项工作需要我和团队付出额外的努力才能达成吗。因为OKR要求激进,“担心有可能完不成”,相当多的常规目标是达不到这个要求的。这里有两点需要澄清:一、OKR不是工作量的衡量工具,并且我们应当主要关注OKR对业务带来的实际效果,所以不必在OKR中堆砌工作量;二、避免堆砌,要求每个人有判断力,自觉抛弃低优先级的目标和低价值的目标,而将精力和资源放到高优先级目标上。可以问自己几个问题,“这件事不做有什么影响吗”,“这件事做了有实际的业务收益吗”。

只写一个单词
写OKR时容易偷懒,只写一个单词,比如“收入” “DAU” “xx项目”。这样的写法,既没有说明最终要达到的状态,也很难客观打分。同时在沟通和对齐的时候,其他同事也很难看懂。类似的,“其他项目”、“重点项目”也是不可取的Objective,因为项目真的重要话,应该明确说出来要达到什么目标;而归为“其他”的目标,通常可能并不重要。

给自己留余量
如果历史上所有的OKR都轻松达成,可能定目标的时候就不够有雄心。

六、沟通、对齐和进度更新
沟通和对齐有几个重要的作用。首先是在重要的方向上,配合团队和上下级形成合力,而不要往不同方向使力。其次,OKR的制定是一个迭代过程,从草稿到定稿,需要方方面面的意见输入。再次,要了解配合团队定OKR时的激进程度,判断如果配合团队的OKR只能完成0.5分,会不会影响自己的工作。另一方面,应该鼓励经常更新OKR进度,既是对自己的提醒鞭策,又是向同事同步信息的好方式。

查理·芒格:如何过上痛苦的生活?

在大学毕业典礼上,诞生过很多著名的演讲,如斯蒂夫·乔布斯 2005 年在斯坦福毕业典礼上的演讲,如 JK·罗琳 2008 年在哈佛大学毕业典礼上的演讲。大多数毕业演讲者会选择描述如何获得幸福的生活,而今天的作者查理·芒格(Charlie Thomas Munger)使用 逆向思维的原则,令人信服地从反面阐述了一名毕业生 如何才能过上痛苦的生活。至于那些宁愿继续保持无知和郁闷的读者,建议你们千万别阅读这篇讲稿。

痛苦人生的药方

在我听过的 20 次哈佛学校的毕业演讲中,哪次曾让我希望它再长些呢?这样的演讲只有约翰尼·卡森的那一次,他详述了保证痛苦人生的卡森药方。所以呢,我决定重复卡森的演讲,但以更大的规模,并加上我自己的药方。毕竟,我比卡森演讲时岁数更大,同一个年轻的有魅力的幽默家相比,我失败的次数更多,痛苦更多,痛苦的方式也更多。我显然很有资格进一步发挥卡森的主题。

那时卡森说他无法告诉毕业的同学如何才能得到幸福,但能够根据个人经验,告诉他们如何保证自己过上痛苦的生活。卡森给的确保痛苦生活的处方包括:

  1. 为了改变心情或者感觉而使用化学物质;

  2. 妒忌,以及

  3. 怨恨。

我现在还能想起来当时卡森用言之凿凿的口气说,他一次又一次地尝试了这些东西,结果每次都变得很痛苦。

要理解卡森为痛苦生活所开处方的第一味药物(使用化学物质)比较容易。我想补充几句。我年轻时最好的朋友有四个,他们非常聪明、正直和幽默,自身条件和家庭背景都很出色。其中两个早已去世,酒精是让他们早逝的一个因素;第三个人现在还醉生梦死地活着——假如那也算活着的话。

虽然易感性因人而异,我们任何人都有可能通过一个开始时难以察觉直到堕落之力强大到无法冲破的细微过程而染上恶瘾。不过呢,我活了 60 年,倒是没有见过有谁的生活因为害怕和避开这条诱惑性的毁灭之路而变得更加糟糕。

妒忌,和令人上瘾的化学物质一样,自然也能获得导致痛苦生活的大奖。早在遭到摩西戒律的谴责之前,它就已造成了许多大灾难。如果你们希望保持妒忌对痛苦生活的影响,我建议你们千万别去阅读塞缪尔·约翰逊(编者注:Samuel Johnson,1709——1784,英国作家,文学研究者和批评家)的任何传记,因为这位虔诚基督徒的生活以令人向往的方式展示了超越妒忌的可能性和好处。

就像卡森感受到的那样,怨恨对我来说也很灵验。如果你们渴望过上痛苦的生活,我找不到比它更灵的药方可以推荐给你们了。约翰逊说得好,他说生活本已艰辛得难以下咽,何必再将它塞进怨恨的苦涩果皮里呢。

对于你们之中那些想得到痛苦生活的人,我还要建议你们别去实践狄斯雷利的权宜之计,它是专为那些无法彻底戒掉怨恨老习惯的人所设计的。在成为伟大的英国首相的过程中,迪斯雷利学会了不让复仇成为行动的动机,但他也保留了某种发泄怨恨的办法,就是将那些敌人的名字写下来,放到抽屉里。然后时不时会翻看这些名字,自得其乐地记录下世界是怎样无须他插手就使他的敌人垮掉的。

本杰明·迪斯雷利(Benjamin Disraeli,1804 – 1881,英国保守党领袖,两度出任英国首相)

查理·芒格的四味药

好啦,卡森开的处方就说到这里。接下来是芒格另开的四味药。

第一,要反复无常,不要虔诚地做你正在做的事。只要养成这个习惯,你们就能够绰绰有余地抵消你们所有优点共同产生的效应,不管那种效应有多么巨大。如果你们喜欢不受信任并被排除在对人类贡献最杰出的人群之外,那么这味药物最适合你们。养成这个习惯,你们将会永远扮演寓言里那只兔子的角色,只不过跑得比你们快的不再只是一只优秀的乌龟,而是一群又一群平庸的乌龟,甚至还有些拄拐杖的平庸乌龟。

我必须警告你们,如果不服用我开出的第一味药,即使你们最初的条件并不好,你们也可能会难以过上痛苦的日子。我有个大学的室友,他以前患有严重的阅读障碍症,现在也是。但他算得上我认识的人中最可靠的。他的生活到目前为止很美满,拥有出色的太太和子女,掌管着某个数十亿美元的企业。如果你们想要避免这种传统的、主流文化的、富有成就的生活,却又坚持不懈地做到为人可靠,那么就算有其他再多的缺点,你们这个愿望恐怕也会落空。

说到「到目前为止很美满」这样一种生活,我忍不住想在这里引用克洛伊斯的话来再次强调人类生存状况那种「到目前为止」的那一面。克洛伊斯曾经是世界上最富裕的国王,后来沦为敌人的阶下囚,就在被活活烧死之前,他说:「哎呀,我现在才想起历史学家梭伦说过的那句话,『在生命没有结束之前,没有人的一生能够被称为是幸福的。』」

我为痛苦生活开出的第二味药是,尽可能从你们自身的经验获得知识,尽量别从其他人成功或失败的经验中广泛地吸取教训,不管他们是古人还是今人。这味药肯定能保证你们过上痛苦的生活,取得二流的成就。

只要看看身边发生的事情,你们就能明白拒不借鉴别人的教训所造成的后果。人类常见的灾难全都毫无创意——酒后驾车导致的身亡,鲁莽驾驶引起的残疾,无药可治的性病,加入毁形灭性的邪教的那些聪明的大学生被洗脑后变成的行尸走肉,由于重蹈前人显而易见的覆辙而导致的生意失败,还有各种形式的集体疯狂等等。你们若要寻找那条通往因为不小心、没有创意的错误而引起真正的人生麻烦的道路,我建议你们牢牢记住这句现代谚语:「人生就像悬挂式滑翔,起步没有成功就完蛋啦。」

避免广泛吸取知识的另一种做法是,别去钻研那些前辈的最好成果。这味药的功效在于让你们得到尽可能少的教育。

如果我再讲一个简短的历史故事,或许你们可以看得更清楚,从而更有效地过上与幸福无缘的生活。从前有个人,他勤奋地掌握了前人最优秀的成果,尽管开始研究分析几何的时候他的基础并不好,学得非常吃力。最终,他本人取得的成就引起了众人的瞩目,他是这样评价他自己的成果的:

「如果说我比其他人看得更远,那是因为我站在巨人的肩膀上。」

这人的骨灰如今埋在威斯敏斯特大教堂里,他的墓碑上有句异乎寻常的墓志铭:

「这里安葬着永垂不朽的艾萨克·牛顿爵士。」

我为你们的痛苦生活开出的第三味药是,当你们在人生的战场上遭遇第一、第二或者第三次严重的失败时,就请意志消沉,从此一蹶不振吧。因为即使是最幸运、最聪明的人,也会遇到许许多多的失败,这味药必定能保证你们永远地陷身在痛苦的泥沼里。请你们千万要忽略爱比克泰德(编者注:爱比克泰德出生在希拉波利斯城的奴隶家庭,而且患有终身残疾,他认为所有人都应该完全自由地掌握自己的生活,也应该与自然和谐相处。)亲自撰写的、恰如其分的墓志铭中蕴含的教训:「此处埋着爱比克泰德,一个奴隶,身体残疾,极其穷困,蒙受诸神的恩宠。」

为了让你们过上头脑混乱、痛苦不堪的日子,我所开的最后一味药是,请忽略小时候人们告诉我的那个乡下人故事。曾经有个乡下人说:「要是知道我会死在哪里就好啦,那我将永远不去那个地方。」大多数人和你们一样,嘲笑这个乡下人的无知,忽略他那朴素的智慧。如果我的经验有什么借鉴意义的话,那些热爱痛苦生活的人应该不惜任何代价避免应用这个乡下人的方法。若想获得失败,你们应该将这种乡下人的方法,也就是卡森在演讲中所用的方法,贬低得愚蠢之极、毫无用处。

「以避免失败为目标而成长」

卡森采用的研究方法是把问题反过来想。就是说要解出 X,得先研究如何才能得到非 X。伟大的代数学家雅各比用的也是卡森这种办法,众所周知,他经常重复一句话:「反过来想,总是反过来想。」雅各比知道事物的本质是这样的,许多难题只有在逆向思考的时候才能得到最好的解决。例如,当年几乎所有人都在试图修正麦克斯韦的电磁定律,以便它能够符合牛顿的三大运动定律,然而爱因斯坦却转了个 180 度大弯,修正了牛顿的定律,让其符合麦克斯韦的定律,结果他发现了相对论。

作为一个公认的传记爱好者,我认为假如查尔斯·罗伯特·达尔文是哈佛学校 1986 届毕业班的学生,他的成绩大概只能排到中等。然而现在他是科学史上的大名人。如果你们希望将来碌碌无为,那么千万不能以达尔文为榜样。

达尔文能够取得这样的成就,主要是因为他的工作方式。这种方式有悖于所有我列出的痛苦法则,而且还特别强调逆向思考:他总是致力于寻求证据来否定他已有的理论,无论他对这种理论有多么珍惜,无论这种理论是多么得之不易。与之相反,大多数人早年取得成就,然后就越来越拒绝新的、证伪性的信息,目的是让他们最初的结论能够保持完整。他们变成了菲利普·威利所评论的那类人:「他们固步自封,满足于已有的知识,永远不会去了解新的事物。」

达尔文的生平展示了乌龟如何可以在极端客观态度的帮助下跑到兔子前面去。这种态度能够帮助客观的人最后变成「蒙眼拼驴尾」游戏中惟一那个没有被遮住眼睛的玩家。

如果你们认为客观态度无足轻重,那么你们不但忽略了来自达尔文的训诲,也忽略了来自爱因斯坦的教导。爱因斯坦说他那些成功的理论来自「好奇、专注、毅力和自省」。他所说的自省,就是不停地试验与推翻他自己深爱的想法。

最后,尽可能地减少客观性,这样会帮助你减少获得世俗好处所需作出的让步以及所要承受的负担,因为客观态度并不只对伟大的物理学家和生物学家有效。它也能够帮助伯米吉地区的管道维修工更好地工作。因此,如果你们认为忠实于自己就是永远不改变你们年轻时的所有观念,那么你们不仅将会稳步地踏上通往极端无知的道路,而且还将走向事业中不愉快的经历给你带来的所有痛苦。

这次类似于说反话的演讲应该以类似于说反话的祝福来结束。这句祝语的灵感来自伊莱休·鲁特引用过的那首讲小狗去多佛的儿歌:「一步又一步,才能到多佛。」我祝福 1986 届毕业班的同学:

在座各位,愿你们在漫长的人生中日日以避免失败为目标而成长。■

节选自《穷查理宝典》(中信出版社)第四章,第一讲

JAVA 锁优化 笔记

高效并发是JDK一个非常重要的改进,HotSpot虚拟机开发团队花费大量的精力去实现各种锁的优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,这些技术都是为了线程间更高效地共享数据,以及解决竞争问题,提高程序执行效率。

自旋锁和自适应自旋
共享数据的锁定状态只会持续很短的时间,为了这一段时间挂起和恢复线程并不值得。如果机器有多个处理器,可以让多个线程同时并行执行,我们可以让后面请求锁的那个线程“稍等一段时间”,但是不放弃CPU执行时间,看看持有锁的线程是否很快就会释放掉锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这个技术就是所谓的自旋锁。

自旋锁在jdk1.4.2中已经引入,只不过默认是关闭状态,可以使用-XX:+UseSpinning参数开启,在jdk1.6中已经默认开启了。自旋等待不能代替阻塞,自旋要求有多处理器,自旋虽然避免了线程切换的开销,但是需要占用处理器时间,所以会白白消耗CPU资源,如果自旋时间非常长,就会带来资源的浪费。所以,自旋等待的时间必须有一定的限度,如果自旋次数超过了限定次数还没有成功获取锁,就应当使用传统的方式去挂起线程。自旋次数默认为10次,用户可以使用参数-XX:PreBlockSpin 配置。

jdk1.6引入自适应自旋锁,自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间和锁拥有者的状态来决定的。如果同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机会认为这次自旋也很有可能再次成功,进而允许自旋等待相对更长的时间。反之,如果对于某个锁,自旋很少获取到过,那么在以后获取这个锁的时候就会省略自旋的过程。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况会预测的越来越准。

锁消除
锁消除的主要判定依据来源于逃逸分析的数据支持,判断堆上的数据都不会逃逸出去从而被其他线程访问,那就可以吧他们当做栈上数据对待,认为数据是线程私有的,加锁操作就可以忽略。具体可以看StringBuffer 在一个方法内部 定义,作用域只在方法内,锁会安全的消除掉,编译后所有的同步操作就会直接执行。

锁粗化
如果一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体内,就算没有线程竞争,频繁的互斥同步操作也会导致性能损耗。如果虚拟机探测到有这种操作,会把加锁同步的范围扩展到整个操作的序列外,只进行一次加锁和解锁。

轻量级锁
轻量级锁并不是用来替代重量级锁的,本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能损耗。主要依赖虚拟机对象头数据,进行CAS操作来实现加锁和解锁操作。[备注:后续文章会详细介绍JAVA 对象的内存布局]。对象头[Mark Word]中有2bit存储锁标志位,1bit固定为0,其他状态(轻量级锁定,重量级锁定,GC标记,可偏向)
01 ——> 未锁定状态 –> 存储 对象哈希码 对象分代年龄
00 –> 轻量级锁定 –> 指向锁记录指针
10 –> 重量级锁定 –> 指向重量级锁的指针
11 –> GC标记 –> 不记录任何消息
01 –> 可偏向 –> 偏向线程ID 偏向时间戳 对象分代年龄

代码进入同步快,如果同步对象没有锁定(01 状态),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced 前缀,即 Displaced Mark Word),然后虚拟机使用CAS操作尝试把对象的Mark Word 更新指向 Lock Record的指针。如果操作成功线程就拥有了该对象的锁,并把对象Mark Word 的标志位修改为”00″,表示对象处于轻量级锁定状态,如果更新失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有这个对象的锁,那就可以直接进入同步块进行执行。否则,这个锁对象已经被其他线程抢占了。如果有2个以上线程竞争,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志位变为”10″,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待的线程也会进入阻塞状态。

解锁过程也是通过CAS操作的,如果对象的Mark Word 仍然指向着线程的锁记录,那就用CAS 操作把对象当前的Mark Word 和 线程中复制的Displaced Mark Word 替换回来。如果替换成功整个同步过程就成功了,如果替换失败,说明其他线程正在尝试获取该锁,那就再释放锁的同时唤醒其他被挂起的线程。

轻量级锁提升性能主要依据:“对于绝大部分的锁,整个同步周期内都是不存在竞争的”,这只是一个经验数据。如果存在锁的竞争,出了额外的CAS操作,还有互斥量的开销,轻量级锁会比传统的锁性能差。

偏向锁

轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不做了。

偏向锁的“偏”,是偏向的意思,锁会偏向于第一个获取到它的线程,如果下面的操作,没有其他线程去获取该锁,则持有偏向锁的线程将永远不会同步。

启用偏向锁:-XX:+UseBiasedLocking jdk1.6默认开启。
第一次获取锁需要把状态标志设置 01 (偏向模式) ,同时使用CAS操作把当前线程ID记录在Mark Word中,CAS操作成功,持有偏向锁的线程每次进入这个锁的同步块时,虚拟机没有任何同步操作(Locking,UnLocking,Update Mark Word)
当其他线程去获取这个锁的时候,偏向模式结束,根据锁对象是否处于锁定状态,撤销偏向后恢复到未锁定(01)或者轻量级锁(00),后续和轻量级锁执行流程一样。

偏向锁可以提高带有同步但是无竞争的程序性能。它同样是带有权衡收益(Trade Off)的优化,也就是说,并不一定对程序有利,如果程序锁竞争比较激烈,那偏向就是多余的,具体问题具体分析,有时候关闭偏向锁性能反而提升了。

LockSupport

concurrent包是基于AQS (AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:

  • Unsafe(提供CAS操作)
  • LockSupport(提供park/unpark操作)

因此,LockSupport非常重要。
两个重点
(1)操作对象
归根结底,LockSupport.park()和LockSupport.unpark(Thread thread)调用的是Unsafe中的native代码:

//LockSupport中
public static void park() {
     UNSAFE.park(false, 0L);
}
//LockSupport中
public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

Unsafe类中的对应方法:

    //park
    public native void park(boolean isAbsolute, long time);
    
    //unpack
    public native void unpark(Object var1);

park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。

与Object类的wait/notify机制相比,park/unpark有两个优点:
以thread为操作对象更符合阻塞线程的直观定义
操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

(2)关于“许可”

在上面的文字中,我使用了阻塞和唤醒,是为了和wait/notify做对比。

其实park/unpark的设计原理核心是“许可”:park是等待一个许可,unpark是为某线程提供一个许可。
如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。

有一点比较难理解的,是unpark操作可以再park操作之前。
也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。这其实是必须的。考虑最简单的生产者(Producer)消费者(Consumer)模型:Consumer需要消费一个资源,于是调用park操作等待;Producer则生产资源,然后调用unpark给予Consumer使用的许可。非常有可能的一种情况是,Producer先生产,这时候Consumer可能还没有构造好(比如线程还没启动,或者还没切换到该线程)。那么等Consumer准备好要消费时,显然这时候资源已经生产好了,可以直接用,那么park操作当然可以直接运行下去。如果没有这个语义,那将非常难以操作。

但是这个“许可”是不能叠加的,“许可”是一次性的。
比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。
继续阅读“LockSupport”

Java ReadWriteLock读写锁的使用

本文将提供Java中的ReadWriteLock和ReentrantReadWriteLock的示例。JDK 1.5中已经引入了ReadWriteLock和ReentrantReadWriteLock。ReentrantReadWriteLock是ReadWriteLock接口的实现,而ReadWriteLock扩展了Lock接口。ReentrantReadWriteLock是具有可重入性的ReadWriteLock的实现。ReentrantReadWriteLock具有关联的读写锁,可以重新获取这些锁。在本文,我们将通过完整的示例讨论ReadWriteLock和ReentrantReadWriteLock。

Lock
JDK 1.5中引入了java.util.concurrent.locks.Lock接口。Lock可以代替使用同步方法,并将有助于更有效的锁定系统。Lock在多线程环境中用于共享资源。Lock作用的方式是,任何线程必须必须首先获得锁才能访问受锁保护的共享资源。一次只有一个线程可以获取锁,一旦其工作完成,它将为队列中其他线程解锁资源。ReadWriteLock是扩展的接口Lock。

ReadWriteLock
JDK 1.5中引入了java.util.concurrent.locks.ReadWriteLock接口。ReadWriteLock是用于读取和写入操作的一对锁。如果没有写锁定请求,则多个线程可以同时获取读锁定请求。如果线程获得了对资源的写锁,则任何线程都无法获得对该资源的其他读或写锁。ReadWriteLock在读操作比写操作更频繁的情况下,效率更高,因为可以由多个线程同时为共享资源获取读锁定。

ReentrantReadWriteLock
JDK 1.5中引入了java.util.concurrent.locks.ReentrantReadWriteLock类。ReentrantReadWriteLock是的实现ReadWriteLock。我们将讨论ReentrantReadWriteLock的一些主要属性。

Acquisition order [获取顺序]
ReentrantReadWriteLock可以使用公平和非公平模式。默认是不公平的。当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

Reentrancy [重入]

什么是可重入锁,不可重入锁呢?”重入”字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的。

Lock downgrading [锁降级]
ReentrantReadWriteLock可以从写锁降级为读锁。这意味着,如果线程获得了写锁,则可以将其锁从写锁降级为读锁。顺序为:首先获取写锁,执行写操作,然后获取读锁,然后解锁写锁,然后在读操作之后最终解锁读锁。从读锁升级到写锁是不行的。
继续阅读“Java ReadWriteLock读写锁的使用”

CyclicBarrier 使用不当导致死锁问题

先上代码:


import java.util.concurrent.*;

public class App {

	public static ExecutorService pool = Executors.newFixedThreadPool(5);
	public static ExecutorService pool2 = Executors.newCachedThreadPool();

	public static void main(String[] args) {
		// 使用 CyclicBarrier 出现死锁问题
		// new GenTaskUseBarrier(pool, 10).start();
		// 使用 CachedThreadPool 解决 CyclicBarrier 死锁问题
		// new GenTaskUseBarrier(pool2, 10).start();
		// 使用 CountDownLatch 解决 CyclicBarrier 死锁问题
		// new GenTaskUseCountDown(pool, 10).start();

		// 模拟5个并发共享一个线程池
		// new MockConcurrence(pool, 5).start();
	}
}

class MockConcurrence extends Thread {
	ExecutorService pool;
	int count;

	public MockConcurrence(ExecutorService pool, int count) {
		super();
		this.pool = pool;
		this.count = count;
	}

	@Override
	public void run() {
		for (int i = 0; i < count; i++) {
			new GenTaskUseBarrier(pool, 5, false).start();
		}
	}
}

class GenTaskUseCountDown extends Thread {

	ExecutorService pool;

	int taskSize;

	public GenTaskUseCountDown(ExecutorService pool, int taskSize) {
		this.pool = pool;
		this.taskSize = taskSize;
	}

	@Override
	public void run() {
		CountDownLatch latch = new CountDownLatch(taskSize);
		for (int i = 0; i < taskSize; i++) {
			pool.submit(new MyTask(latch));
		}
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		pool.shutdown();
		System.out.println("All task done!");
	}
}

class GenTaskUseBarrier extends Thread {

	ExecutorService pool;

	int taskSize;

	boolean autoClose = true;

	public GenTaskUseBarrier(ExecutorService pool, int taskSize) {
		this.pool = pool;
		this.taskSize = taskSize;
	}

	public GenTaskUseBarrier(ExecutorService pool, int taskSize, boolean autoClose) {
		this(pool, taskSize);
		this.autoClose = autoClose;
	}

	@Override
	public void run() {
		CyclicBarrier barrier = new CyclicBarrier(taskSize, new Runnable() {
			@Override
			public void run() {
				if (autoClose) {
					pool.shutdown();
				}
				System.out.println("All task done!");
			}
		});
		for (int i = 0; i < taskSize; i++)
			pool.submit(new MyTask(barrier));
	}
}

class MyTask extends Thread {

	CyclicBarrier barrier;

	CountDownLatch latch;

	public MyTask(CyclicBarrier barrier) {
		this.barrier = barrier;
	}

	public MyTask(CountDownLatch latch) {
		this.latch = latch;
	}

	@Override
	public void run() {
		try {
			System.out.println("线程" + Thread.currentThread().getName() + "正在执行同一个任务");
			// 以睡眠来模拟几个线程执行一个任务的时间
			Thread.sleep(1000);
			System.out.println("线程" + Thread.currentThread().getName() + "执行任务完成,等待其他线程执行完毕");
			// 用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
			if (barrier != null) {
				barrier.await();
			}
			if (latch != null) {
				latch.countDown();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
		System.out.println("所有线程写入完毕");

	}

}

输出结果:

线程pool-1-thread-3正在执行同一个任务
线程pool-1-thread-4正在执行同一个任务
线程pool-1-thread-2正在执行同一个任务
线程pool-1-thread-1正在执行同一个任务
线程pool-1-thread-5正在执行同一个任务
线程pool-1-thread-2执行任务完成,等待其他线程执行完毕
线程pool-1-thread-3执行任务完成,等待其他线程执行完毕
线程pool-1-thread-4执行任务完成,等待其他线程执行完毕
线程pool-1-thread-1执行任务完成,等待其他线程执行完毕
线程pool-1-thread-5执行任务完成,等待其他线程执行完毕

//卡死在这个地方

原因分析:
1.提交了10个任务,但是只有5个调度线程、每次只能执行一个解析任务;
2.前面5个任务执行后由于调用了 barrier.await 被阻塞了,需要等待其他两个任务都达到栅栏状态;
3.前面5个任务的线程被阻塞了,导致没有空闲的调度线程去执行另外两个任务;
4.前面5个任务等待其他两个任务的栅栏唤醒,而其他5个任务则等待第一个任务的线程资源,从而进入死锁状态。

解决方案 [修改Main方法注释 可以逐个测试]:
1、调整线程池调度线程个数,提交多少个任务开多少个资源,如果并发调用的时候共享同一个线程池调度非常容易出现问题,一定要小心
2、使用 CachedThreadPool,或者自定义线程池
3、更换协作工具类为 CountDownLatch,将主线程阻塞直到所有的解析任务都被执行完成。