一、什么是线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。—《引自维基百科》
二、多线程的优点
1.在多核或多CPU上使用多线程程序能提高程序的执行吞吐率。
2.在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责IO处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
3.无需跨进程边界;
4.程序逻辑和控制方式简单(同时读取多个文件);
5.所有线程可以直接共享内存和变量等;
三、多线程的缺点
1.每个线程与主程序共用地址空间,受限于2GB地址空间;
2.线程之间的同步和加锁控制比较麻烦;
3.到达一定的线程数程度后,即使再增加CPU也无法提高性能。
4.线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。【当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。】
四、多线程的挑战
多线程是在同一个程序内部并行执行,因此会对相同的内存空间进行并发读写操作。这可能是在单线程程序中从来不会遇到的问题。其中的一些错误也未必会在单CPU机器上出现,因为两个线程从来不会得到真正的并行执行。然而,更现代的计算机伴随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行。
如果一个线程在读一个内存时,另一个线程正向该内存进行写操作,那进行读操作的那个线程将获得什么结果呢?是写操作之前旧的值?还是写操作成功之后的新值?或是一半新一半旧的值?或者,如果是两个线程同时写同一个内存,在操作完成后将会是什么结果呢?是第一个线程写入的值?还是第二个线程写入的值?还是两个线程写入的一个混合值?因此如没有合适的预防措施,任何结果都是可能的。而且这种行为的发生甚至不能预测,所以结果也是不确定性的。
五、线程安全
允许被多个线程同时执行的代码称作线程安全的代码,线程安全的代码不包含竞态条件。
局部变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:
public void safe(){
int safeInt = 1;
safeInt ++;
System.out.println(safeInt);
}
局部的对象引用
对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(译者注:即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:
public void method(){
SafeObject safeObject = new SafeObject();
safeObject.callAnyOne();
method(safeObject);
}
private void method(SafeObject safeObject) {
safeObject.setCallMethod("hello world");
}
不可变对象
我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:
package demo.com.test.thread;
public class ImmutableValue {
private int value =0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue() {
return value;
}
}
请注意ImmutableValue类的成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。
【注:“不变”(Immutable)和“只读”(Read Only)是不同的。当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。比如,一个人的出生年月日是“不变”属性,而一个人的年龄便是“只读”属性,但是不是“不变”属性。随着时间的变化,一个人的年龄会随之发生变化,而一个人的出生年月日则不会变化。这就是“不变”和“只读”的区别。(摘自《Java与模式》第34章)】
六、逃逸分析(Escape Analysis)
上面说到逃逸那什么时逃逸呢:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。下面是逃逸代码:
public StringBuilder createString(String s1,String s2){
StringBuilder sb = new StringBuilder();
sb.append(s1).append(s2);
return sb;
}
StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
那我们改一下让他变成不逃逸的代码,参考如下:
public String createString2(String s1,String s2){
StringBuilder sb = new StringBuilder();
sb.append(s1).append(s2);
return sb.toString();
}
不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
如果能证明一个对象不会逃逸到方法或线程外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,也就是和局部变量一样,这样也是线程安全的。
七、线程控制逃逸规则
线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。
如果一个资源的创建,使用,销毁都在同一个线程内完成,
且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。
即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码:
检查记录X是否存在,如果不存在,插入X
如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:
线程1检查记录X是否存在。检查结果:不存在
线程2检查记录X是否存在。检查结果:不存在
线程1插入记录X
线程2插入记录X
同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。
八、引用
http://www.importnew.com/23150.html
http://rednaxelafx.iteye.com/blog/659108/
https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B