本文转载自互联网,侵删html
什么是并发java
在过去单CPU时代,单任务在一个时间点只能执行单一程序。以后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并非真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操做系统来完成多任务间对CPU的运行切换,以使得每一个任务都有机会得到必定的时间片运行。git
随着多任务对软件开发者带来的新挑战,程序不在能假设独占全部的CPU时间、全部的内存和其余计算机资源。一个好的程序榜样是在其再也不使用这些资源时对其进行释放,以使得其余程序能有机会使用这些资源。程序员
再后来发展到多线程技术,使得在一个程序内部能拥有多个线程并行执行。一个线程的执行能够被认为是一个CPU在执行该程序。当一个程序运行在多线程下,就好像有多个CPU在同时执行该程序。github
多线程比多任务更加有挑战。多线程是在同一个程序内部并行执行,所以会对相同的内存空间进行并发读写操做。这多是在单线程程序中历来不会遇到的问题。其中的一些错误也未必会在单CPU机器上出现,由于两个线程历来不会获得真正的并行执行。然而,更现代的计算机伴随着多核CPU的出现,也就意味着不一样的线程能被不一样的CPU核获得真正意义的并行执行。web
若是一个线程在读一个内存时,另外一个线程正向该内存进行写操做,那进行读操做的那个线程将得到什么结果呢?是写操做以前旧的值?仍是写操做成功以后的新值?或是一半新一半旧的值?或者,若是是两个线程同时写同一个内存,在操做完成后将会是什么结果呢?是第一个线程写入的值?仍是第二个线程写入的值?仍是两个线程写入的一个混合值?所以如没有合适的预防措施,任何结果都是可能的。并且这种行为的发生甚至不能预测,因此结果也是不肯定性的。面试
Java是最早支持多线程的开发的语言之一,Java从一开始就支持了多线程能力,所以Java开发者能常遇到上面描述的问题场景。这也是我想为Java并发技术而写这篇系列的缘由。做为对本身的笔记,和对其余Java开发的追随者均可获益的。算法
该系列主要关注Java多线程,但有些在多线程中出现的问题会和多任务以及分布式系统中出现的存在相似,所以该系列会将多任务和分布式系统方面做为参考,因此叫法上称为“并发性”,而不是“多线程”。数据库
尽管面临不少挑战,多线程有一些优势使得它一直被使用。这些优势是:编程
想象一下,一个应用程序须要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件须要5秒,处理一个文件须要2秒。处理两个文件则须要:
1 |
5 秒读取文件A |
2 |
2 秒处理文件A |
3 |
5 秒读取文件B |
4 |
2 秒处理文件B |
5 |
--------------------- |
6 |
总共须要 14 秒 |
从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据。在这段时间里,CPU很是的空闲。它能够作一些别的事情。经过改变操做的顺序,就可以更好的使用CPU资源。看下面的顺序:
1 |
5 秒读取文件A |
2 |
5 秒读取文件B + 2 秒处理文件A |
3 |
2 秒处理文件B |
4 |
--------------------- |
5 |
总共须要 12 秒 |
CPU等待第一个文件被读取完。而后开始读取第二个文件。当第二文件在被读取的时候,CPU会去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU大部分时间是空闲的。
总的说来,CPU可以在等待IO的时候作一些其余的事情。这个不必定就是磁盘IO。它也能够是网络的IO,或者用户输入。一般状况下,网络和磁盘的IO比CPU和内存的IO慢的多。
在单线程应用程序中,若是你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每一个文件读取和处理的状态。相反,你能够启动两个线程,每一个线程处理一个文件的读取和操做。线程会在等待磁盘读取文件的过程当中被阻塞。在等待的时候,其余的线程可以使用CPU去处理已经读取完的文件。其结果就是,磁盘老是在繁忙地读取不一样的文件到内存中。这会带来磁盘和CPU利用率的提高。并且每一个线程只须要记录一个文件,所以这种方式也很容易编程实现。
服务器的流程以下所述:
1 |
while (server is active){ |
2 |
listen for request |
3 |
process request |
4 |
} |
若是一个请求须要占用大量的时间来处理,在这段时间内新的客户端就没法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另外一种设计是,监听线程把请求传递给工做者线程(worker thread),而后马上返回去监听。而工做者线程则可以处理这个请求并发送一个回复给客户端。这种设计以下所述:
1 |
while (server is active){ |
2 |
listen for request |
3 |
hand request to worker thread |
4 |
} |
这种方式,服务端线程迅速地返回去监听。所以,更多的客户端可以发送请求给服务端。这个服务也变得响应更快。
桌面应用也是一样如此。若是你点击一个按钮开始运行一个耗时的任务,这个线程既要执行任务又要更新窗口和按钮,那么在任务执行的过程当中,这个应用程序看起来好像没有反应同样。相反,任务能够传递给工做者线程(word thread)。当工做者线程在繁忙地处理任务的时候,窗口线程能够自由地响应其余用户的请求。当工做者线程完成任务的时候,它发送信号给窗口线程。窗口线程即可以更新应用程序窗口,并显示任务的结果。对用户而言,这种具备工做者线程设计的程序显得响应速度更快。
从一个单线程的应用到一个多线程的应用并不只仅带来好处,它也会有一些代价。不要仅仅为了使用多线程而使用多线程。而应该明确在使用多线程时能多来的好处比所付出的代价大的时候,才使用多线程。若是存在疑问,应该尝试测量一下应用程序的性能和响应能力,而不仅是猜想。
虽然有一些多线程应用程序比单线程的应用程序要简单,但其余的通常都更复杂。在多线程访问共享数据的时候,这部分代码须要特别的注意。线程之间的交互每每很是复杂。不正确的线程同步产生的错误很是难以被发现,而且重现以修复。
当CPU从执行一个线程切换到执行另一个线程的时候,它须要先存储当前线程的本地的数据,程序指针等,而后载入另外一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,而后切换到另一个上下文中执行另一个线程。
上下文切换并不廉价。若是没有必要,应该减小上下文切换的发生。
你能够经过维基百科阅读更多的关于上下文切换相关的内容:
http://en.wikipedia.org/wiki/Context_switch
线程在运行的时候须要从计算机里面获得一些资源。除了CPU,线程还须要一些内存来维持它本地的堆栈。它也须要占用操做系统中一些资源来管理线程。咱们能够尝试编写一个程序,让它建立100个线程,这些线程什么事情都不作,只是在等待,而后看看这个程序在运行的时候占用了多少内存。
在同一程序中运行多个线程自己不会致使问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源作了写操做时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
多线程同时执行下面的代码可能会出错:
1 |
public class Counter { |
2 |
protected long count = 0 ; |
3 |
public void add( long value){ |
4 |
this .count = this .count + value; |
5 |
} |
6 |
} |
想象下线程A和B同时执行同一个Counter对象的add()方法,咱们没法知道操做系统什么时候会在两个线程之间切换。JVM并非将这段代码视为单条指令来执行的,而是按照下面的顺序:
观察线程A和B交错执行会发生什么:
两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而因为两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。而后各自加了2和3,并分别写回内存。最终的值并非指望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也多是线程B。若是没有采用合适的同步机制,线程间的交叉执行状况就没法预料。
竞态条件 & 临界区
当两个线程竞争同一资源时,若是对资源的访问顺序敏感,就称存在竞态条件。致使竞态条件发生的代码区称做临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就能够避免竞态条件。
容许被多个线程同时执行的代码称做线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引起竞态条件。所以,了解Java线程执行时共享了什么资源很重要。
局部变量
局部变量存储在线程本身的栈中。也就是说,局部变量永远也不会被多个线程共享。因此,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:
1 |
public void someMethod(){ |
2 |
|
3 |
long threadSafeInt = 0 ; |
4 |
5 |
threadSafeInt++; |
6 |
} |
局部的对象引用
对象的局部引用和基础类型的局部变量不太同样。尽管引用自己没有被共享,但引用所指的对象并无存储在线程的栈内。全部的对象都存在共享堆中。若是在某个方法中建立的对象不会逃逸出(译者注:即该对象不会被其它方法得到,也不会被非局部变量引用到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象做为参数传给其它方法,只要别的线程获取不到这个对象,那它还是线程安全的。下面是一个线程安全的局部引用样例:
01 |
public void someMethod(){ |
02 |
|
03 |
LocalObject localObject = new LocalObject(); |
04 |
05 |
localObject.callMethod(); |
06 |
method2(localObject); |
07 |
} |
08 |
09 |
public void method2(LocalObject localObject){ |
10 |
localObject.setValue( "value" ); |
11 |
} |
样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每一个执行someMethod()的线程都会建立本身的LocalObject对象,并赋值给localObject引用。所以,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即便将LocalObject做为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。固然,若是LocalObject经过某些方法被传给了别的线程,那它就再也不是线程安全的了。
对象成员
对象成员存储在堆上。若是两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:
1 |
public class NotThreadSafe{ |
2 |
StringBuilder builder = new StringBuilder(); |
3 |
|
4 |
public add(String text){ |
5 |
this .builder.append(text); |
6 |
} |
7 |
} |
若是两个线程同时调用同一个NotThreadSafe
实例上的add()方法,就会有竞态条件问题。例如:
01 |
NotThreadSafe sharedInstance = new NotThreadSafe(); |
02 |
03 |
new Thread( new MyRunnable(sharedInstance)).start(); |
04 |
new Thread( new MyRunnable(sharedInstance)).start(); |
05 |
06 |
public class MyRunnable implements Runnable{ |
07 |
NotThreadSafe instance = null ; |
08 |
|
09 |
public MyRunnable(NotThreadSafe instance){ |
10 |
this .instance = instance; |
11 |
} |
12 |
13 |
public void run(){ |
14 |
this .instance.add( "some text" ); |
15 |
} |
16 |
} |
注意两个MyRunnable共享了同一个NotThreadSafe对象。所以,当它们调用add()方法时会形成竞态条件。
固然,若是这两个线程在不一样的NotThreadSafe实例上调用call()方法,就不会致使竞态条件。下面是稍微修改后的例子:
1 |
new Thread( new MyRunnable( new NotThreadSafe())).start(); |
2 |
new Thread( new MyRunnable( new NotThreadSafe())).start(); |
如今两个线程都有本身单独的NotThreadSafe对象,调用add()方法时就会互不干扰,不再会有竞态条件问题了。因此非线程安全的对象仍能够经过某种方式来消除竞态条件。
线程控制逃逸规则
线程控制逃逸规则能够帮助你判断代码中对某些资源的访问是不是线程安全的。
资源能够是对象,数组,文件,数据库链接,套接字等等。Java中你无需主动销毁对象,因此“销毁”指再也不有引用指向对象。
即便对象自己线程安全,但若是该对象中包含其余资源(文件,数据库链接),整个应用也许就再也不是线程安全的了。好比2个线程都建立了各自的数据库链接,每一个链接自身是线程安全的,但它们所链接到的同一个数据库也许不是线程安全的。好比,2个线程执行以下代码:
若是两个线程同时执行,并且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:
一样的问题也会发生在文件或其余共享资源上。所以,区分某个线程控制的对象是资源自己,仍是仅仅到某个资源的引用很重要。
当多个线程同时访问同一个资源,而且其中的一个或者多个线程对这个资源进行了写操做,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。
咱们能够经过建立不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。以下示例:
01 |
public class ImmutableValue{ |
02 |
private int value = 0 ; |
03 |
04 |
public ImmutableValue( int value){ |
05 |
this .value = value; |
06 |
} |
07 |
08 |
public int getValue(){ |
09 |
return this .value; |
10 |
} |
11 |
} |
请注意ImmutableValue类的成员变量value
是经过构造函数赋值的,而且在类中没有set方法。这意味着一旦ImmutableValue实例被建立,value
变量就不能再被修改,这就是不可变性。但你能够经过getValue()方法读取这个变量的值。
(译者注:注意,“不变”(Immutable)和“只读”(Read Only)是不一样的。当一个变量是“只读”时,变量的值不能直接改变,可是能够在其它变量发生改变的时候发生改变。好比,一我的的出生年月日是“不变”属性,而一我的的年龄即是“只读”属性,可是不是“不变”属性。随着时间的变化,一我的的年龄会随之发生变化,而一我的的出生年月日则不会变化。这就是“不变”和“只读”的区别。(摘自《Java与模式》第34章))
若是你须要对ImmutableValue类的实例进行操做,能够经过获得value变量后建立一个新的实例来实现,下面是一个对value变量进行加法操做的示例:
01 |
public class ImmutableValue{ |
02 |
private int value = 0 ; |
03 |
04 |
public ImmutableValue( int value){ |
05 |
this .value = value; |
06 |
} |
07 |
08 |
public int getValue(){ |
09 |
return this .value; |
10 |
} |
11 |
12 |
public ImmutableValue add( int valueToAdd){ |
13 |
return new ImmutableValue( this .value + valueToAdd); |
14 |
} |
15 |
} |
请注意add()方法以加法操做的结果做为一个新的ImmutableValue类实例返回,而不是直接对它本身的value变量进行操做。
引用不是线程安全的!
重要的是要记住,即便一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:
01 |
public void Calculator{ |
02 |
private ImmutableValue currentValue = null ; |
03 |
04 |
public ImmutableValue getValue(){ |
05 |
return currentValue; |
06 |
} |
07 |
08 |
public void setValue(ImmutableValue newValue){ |
09 |
this .currentValue = newValue; |
10 |
} |
11 |
12 |
public void add( int newValue){ |
13 |
this .currentValue = this .currentValue.add(newValue); |
14 |
} |
15 |
} |
Calculator类持有一个指向ImmutableValue实例的引用。注意,经过setValue()方法和add()方法可能会改变这个引用。所以,即便Calculator类内部使用了一个不可变对象,但Calculator类自己仍是可变的,所以Calculator类不是线程安全的。换句话说:ImmutableValue类是线程安全的,但使用它的类不是。当尝试经过不可变性去得到线程安全时,这点是须要牢记的。
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法便可。
线程是什么?
线程(Thread)是一个对象(Object)。用来干什么?Java 线程(也称 JVM 线程)是 Java 进程内容许多个同时进行的任务。该进程内并发的任务成为线程(Thread),一个进程里至少一个线程。
Java 程序采用多线程方式来支持大量的并发请求处理,程序若是在多线程方式执行下,其复杂度远高于单线程串行执行。那么多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
为啥使用多线程?
聊到多线程,多半会聊并发与并行,咋理解并区分这两个的区别呢?
Java 建立线程对象有两种方法:
若是一个类继承Thread,则不适合资源共享。可是若是实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具备的优点:
1):适合多个相同的程序代码的线程去处理同一个资源
2):能够避免java中的单继承的限制
3):增长程序的健壮性,代码能够被多个线程共享,代码和数据独立
直接看代码:
一、继承Thread的demo
提醒一下你们:main方法其实也是一个线程。在java中全部的线程都是同时启动的,至于何时,哪一个先执行,彻底看谁先获得CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。由于每当使用java命令执行一个类的时候,实际上都会启动一个jvm,每个jvm实际上就是在操做系统中启动了一个进程。
新建 MyThread 对象,代码以下:
/** * 继承 Thread 类建立线程对象 * @author Jeff Lee @ bysocket.com * @since 2018年01月27日21:03:02 */ public class MyThread extends Thread { @Override // 能够省略 public void run() { System.out.println("MyThread 的线程对象正在执行任务"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { MyThread thread = new MyThread(); thread.start(); System.out.println("MyThread 的线程对象 " + thread.getId()); } } }
MyThread 类继承了 Thread 对象,并重写(Override)了 run 方法,实现线程里面的逻辑。main 函数是使用 for 语句,循环建立了 10 个线程,调用 start 方法启动线程,最后打印当前线程对象的 ID。
run 方法和 start 方法的区别是什么呢?
run 方法就是跑的意思,线程启动后,会调用 run 方法。
start 方法就是启动的意思,就是启动新线程实例。启动线程后,才会调线程的 run 方法。
执行 main 方法后,控制台打印以下:
可见,线程的 ID 是线程惟一标识符,每一个线程 ID 都是不同的。
start 方法和 run 方法的关系如图所示:转存失败从新上传取消
同理,实现 Runnable 接口类建立线程对象也很简单,只是不一样的形式。新建 MyThreadBrother 代码以下:
/** * 实现 Runnable 接口类建立线程对象 * @author Jeff Lee @ bysocket.com * @since 2018年01月27日21:22:57 */ public class MyThreadBrother implements Runnable { @Override // 能够省略 public void run() { System.out.println("MyThreadBrother 的线程对象正在执行任务"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new MyThreadBrother()); thread.start(); System.out.println("MyThreadBrother 的线程对象 " + thread.getId()); } } }
具体代码:「java-concurrency-core-learning」
https://github.com/JeffLi1993/java-concurrency-core-learning
在运行上面两个小 demo 后,JVM 执行了 main 函数线程,而后在主线程中执行建立了新的线程。正常状况下,全部线程执行到运行结束为止。除非某个线程中调用了 System.exit(1) 则被终止。
在实际开发中,一个请求到响应式是一个线程。但在这个线程中可使用线程池建立新的线程,去执行任务。
新建 MyThreadInfo 类,打印线程对象属性,代码以下:
/** * 线程实例对象的属性值 * @author Jeff Lee @ bysocket.com * @since 2018年01月27日21:24:40 */ public class MyThreadInfo extends Thread { @Override // 能够省略 public void run() { System.out.println("MyThreadInfo 的线程实例正在执行任务"); // System.exit(1); } public static void main(String[] args) { MyThreadInfo thread = new MyThreadInfo(); thread.start(); System.out.print("MyThreadInfo 的线程对象 \n" + "线程惟一标识符:" + thread.getId() + "\n" + "线程名称:" + thread.getName() + "\n" + "线程状态:" + thread.getState() + "\n" + "线程优先级:" + thread.getPriority()); } }
执行代码打印以下:
线程是一个对象,它有惟一标识符 ID、名称、状态、优先级等属性。线程只能修改其优先级和名称等属性 ,没法修改 ID 、状态。ID 是 JVM 分配的,名字默认也为 Thread-XX,XX是一组数字。线程初始状态为 NEW。
线程优先级的范围是 1 到 10 ,其中 1 是最低优先级,10 是最高优先级。不推荐改变线程的优先级,若是业务须要,天然能够修改线程优先级到最高,或者最低。
线程的状态实现经过 Thread.State 常量类实现,有 6 种线程状态:new(新建)、runnnable(可运行)、blocked(阻塞)、waiting(等待)、time waiting (定时等待)和 terminated(终止)。状态转换图以下:
线程状态流程大体以下:
本文介绍了线程与多线程的基础篇,包括了线程启动及线程状态等。下一篇咱们聊下线程的具体操做。包括中断、终止等
1.1 什么是线程中断?
线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,经过其余线程调用了该线程的 interrupt()
方法,使得该线程中断标志位属性改变。
深刻思考下,线程中断不是去中断了线程,偏偏是用来通知该线程应该被中断了。具体是一个标志位属性,到底该线程生命周期是去终止,仍是继续运行,由线程根据标志位属性自行处理。
1.2 线程中断操做
调用线程的 interrupt()
方法,根据线程不一样的状态会有不一样的结果。
下面新建 InterruptedThread 对象,代码以下:
/** * 一直运行的线程,中断状态为 true * * @author Jeff Lee @ bysocket.com * @since 2018年02月23日19:03:02 */ public class InterruptedThread implements Runnable { @Override // 能够省略 public void run() { // 一直 run while (true) { } } public static void main(String[] args) throws Exception { Thread interruptedThread = new Thread(new InterruptedThread(), "InterruptedThread"); interruptedThread.start(); TimeUnit.SECONDS.sleep(2); interruptedThread.interrupt(); System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted()); TimeUnit.SECONDS.sleep(2); } }
运行 main 函数,结果以下:
代码详解:
interrupt()
方法,中断状态置为 true,但不会影响线程的继续运行另外一种状况,新建 InterruptedException 对象,代码以下:
/** * 抛出 InterruptedException 的线程,中断状态被重置为默认状态 false * * @author Jeff Lee @ bysocket.com * @since 2018年02月23日19:03:02 */ public class InterruptedException implements Runnable { @Override // 能够省略 public void run() { // 一直 sleep try { TimeUnit.SECONDS.sleep(10); } catch (java.lang.InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { Thread interruptedThread = new Thread(new InterruptedException(), "InterruptedThread"); interruptedThread.start(); TimeUnit.SECONDS.sleep(2); // 中断被阻塞状态(sleep、wait、join 等状态)的线程,会抛出异常 InterruptedException // 在抛出异常 InterruptedException 前,JVM 会先将中断状态重置为默认状态 false interruptedThread.interrupt(); System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted()); TimeUnit.SECONDS.sleep(2); } }
运行 main 函数,结果以下:
代码详解:
小结下线程中断:
代码:https://github.com/JeffLi1993/java-concurrency-core-learning
好比在 IDEA 中强制关闭程序,当即中止程序,不给程序释放资源等操做,确定是不正确的。线程终止也存在相似的问题,因此须要考虑如何终止线程?
上面聊到了线程中断,能够利用线程中断标志位属性来安全终止线程。同理也可使用 boolean 变量来控制是否须要终止线程。
新建 ,代码以下:
/** * 安全终止线程 * * @author Jeff Lee @ bysocket.com * @since 2018年02月23日19:03:02 */ public class ThreadSafeStop { public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "CountThread"); countThread.start(); // 睡眠 1 秒,通知 CountThread 中断,并终止线程 TimeUnit.SECONDS.sleep(1); countThread.interrupt(); Runner two = new Runner(); countThread = new Thread(two,"CountThread"); countThread.start(); // 睡眠 1 秒,而后设置线程中止状态,并终止线程 TimeUnit.SECONDS.sleep(1); two.stopSafely(); } private static class Runner implements Runnable { private long i; // 终止状态 private volatile boolean on = true; @Override public void run() { while (on && !Thread.currentThread().isInterrupted()) { // 线程执行具体逻辑 i++; } System.out.println("Count i = " + i); } public void stopSafely() { on = false; } } }
从上面代码能够看出,经过 while (on && !Thread.currentThread().isInterrupted())
代码来实现线程是否跳出执行逻辑,并终止。可是疑问点就来了,为啥须要 on
和 isInterrupted()
两项一块儿呢?用其中一个方式不就好了吗?答案在下面
on
经过 volatile 关键字修饰,达到线程之间可见,从而实现线程的终止。但当线程状态为被阻塞状态(sleep、wait、join 等状态)时,对成员变量操做也阻塞,进而没法执行安全终止线程isInterrupted();
只去解决阻塞状态下的线程安全终止。不少好友介绍,若是用 Spring 栈开发到使用线程或者线程池,那么尽可能使用框架这块提供的线程操做及框架提供的终止等
原文出处http://cmsblogs.com/ 『chenssy』
ThreadLocal是啥?之前面试别人时就喜欢问这个,有些伙伴喜欢把它和线程同步机制混为一谈,事实上ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,可是它并非解决多线程共享变量的问题。那么ThreadLocal究竟是什么呢?
API是这样介绍它的:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
该类提供了线程局部 (thread-local) 变量。这些变量不一样于它们的普通对应物,由于访问某个变量(经过其
get
或set
方法)的每一个线程都有本身的局部变量,它独立于变量的初始化副本。ThreadLocal
实例一般是类中的 private static 字段,它们但愿将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
因此ThreadLocal与线程同步机制不一样,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每个线程建立一个单独的变量副本,故而每一个线程均可以独立地改变本身所拥有的变量副本,而不会影响其余线程所对应的副本。能够说ThreadLocal为多线程环境下变量问题提供了另一种解决思路。
ThreadLocal定义了四个方法:
除了这四个方法,ThreadLocal内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操做。ThreadLocalMap提供了一种用键值对方式存储每个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
对于ThreadLocal须要注意的有两点:
1. ThreadLocal实例自己是不存储值,它只是提供了一个在当前线程中找到副本值得key。
2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。
下图是Thread、ThreadLocal、ThreadLocalMap的关系(http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)
示例以下:
运行结果:
从运行结果能够看出,ThreadLocal确实是能够达到线程隔离机制,确保变量的安全性。这里咱们想一个问题,在上面的代码中ThreadLocal的initialValue()方法返回的是0,加入该方法返回得是一个对象呢,会产生什么后果呢?例如:
具体过程请参考:对ThreadLocal实现原理的一点思考
ThreadLocal虽然解决了这个多线程变量的复杂问题,可是它的源码实现倒是比较简单的。ThreadLocalMap是实现ThreadLocal的关键,咱们先从它入手。
ThreadLocalMap其内部利用Entry来实现key-value的存储,以下:
从上面代码中能够看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,因此说Entry所对应key(ThreadLocal实例)的引用为一个弱引用(关于弱引用这里就很少说了,感兴趣的能够关注这篇博客:Java 理论与实践: 用弱引用堵住内存泄漏)
ThreadLocalMap的源码稍微多了点,咱们就看两个最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。
**set(ThreadLocal> key, Object value)**
这个set()操做和咱们在集合了解的put()方式有点儿不同,虽然他们都是key-value结构,不一样在于他们解决散列冲突的方式不一样。集合Map的put()采用的是拉链法,而ThreadLocalMap的set()则是采用开放定址法(具体请参考散列冲突处理系列博客)。掌握了开放地址法该方法就一目了然了。
set()操做除了存储元素外,还有一个很重要的做用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法能够清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义以下:
从名字上面咱们能够看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦建立其散列值就已经肯定了,生成过程则是调用nextHashCode():
nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就能够看出他们的定义。
getEntry()
因为采用了开放定址法,因此当前key的散列值和元素在数组的索引并非彻底对应的,首先取一个探测数(key的散列值),若是所对应的key就是咱们所要找的元素,则返回,不然调用getEntryAfterMiss(),以下:
这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,可以有效地避免内存泄漏。
返回当前线程所对应的线程变量
首先经过当前线程获取所对应的成员变量ThreadLocalMap,而后经过ThreadLocalMap获取当前ThreadLocal的Entry,最后经过所获取的Entry获取目标值result。
getMap()方法能够获取当前线程所对应的ThreadLocalMap,以下:
设置当前线程的线程局部变量的值。
获取当前线程所对应的ThreadLocalMap,若是不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,若是不存在,则调用createMap()方法新建一个,以下:
返回该线程局部变量的初始值。
该方法定义为protected级别且返回为null,很明显是要子类实现它的,因此咱们在使用ThreadLocal的时候通常都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,而且仅执行1次。
将当前线程局部变量的值删除。
该方法的目的是减小内存的占用。固然,咱们不须要显示调用该方法,由于一个线程结束后,它所对应的局部变量就会被垃圾回收。
前面提到每一个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,咱们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,可是value却不必定可以被回收,由于他还与Current Thread存在一个强引用关系,以下(图片来自http://www.jianshu.com/p/ee8c9dccc953):
因为存在这个强引用关系,会致使value没法回收。若是这个线程对象不会销毁那么这个强引用关系则会一直存在,就会出现内存泄漏状况。因此说只要这个线程对象可以及时被GC回收,就不会出现内存泄漏。若是碰到线程池,那就更坑了。
那么要怎么避免这个问题呢?
在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),若是遇到key == null的状况,会对value设置为null。固然咱们也能够显示调用ThreadLocal的remove()方法进行处理。
下面再对ThreadLocal进行简单的总结:
- ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每一个线程处理本身的状态而引入的一个机制。这点相当重要。
- 每一个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
- ThreadLocal并非为线程保存对象的副本,它仅仅只起到一个索引的做用。它的主要木得视为每个线程隔离一个类的实例,这个实例的做用范围仅限于线程内部。
有关于JMM内存模型的详细介绍将在下一章讲述。
本文转载自并发编程网 – ifeve.com
更多内容请关注微信公众号【Java技术江湖】
一位阿里 Java 工程师的技术小站。做者黄小斜,专一 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!(关注公众号后回复”Java“便可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送做者原创的Java学习指南、Java程序员面试指南等干货资源)