转载: https://www.cnblogs.com/qingquanzi/p/8228422.html
0、前提
看此文章的前提是必须已经学习了 LockSupport 的基本语法和了解 LockSupport 的 API的基本使用。
一、简单
在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和 notify/notifyAll 方法实现。
写一段例子代码,线程A执行一段业务逻辑后调用wait阻塞住自己。主线程调用notify方法唤醒线程A,线程A然后打印自己执行的结果。
TestObjWait
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
obj.wait();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" sum = "+sum);
}
},"ThreadA");
threadA.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" sleep over ");
obj.notify();
System.out.println(Thread.currentThread().getName()+" over ");
}
}
执行这段代码,不难发现这个错误:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
原因很简单,wait和notify/notifyAll 方法只能在 同步代码块 里用(这个有的面试官也会考察)。所以将代码修改为如下就可正常运行了:
TestObjWait1
package com.aop8.locksupport;
public class TestObjWait1 {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" sum = "+sum);
}
},"ThreadA");
threadA.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" sleep over ");
synchronized (obj){
obj.notify();
}
System.out.println(Thread.currentThread().getName()+" over ");
}
}
运行结果:
main sleep over
main over
ThreadA sum = 45
如果 换成LockSupport呢?简单得很,看代码:
TestObjWait2
package com.aop8.locksupport;
import java.util.concurrent.locks.LockSupport;
public class TestObjWait2 {
public static void main(String[] args)throws Exception {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(Thread.currentThread().getName()+ " sum = "+sum);
}
},"ThreadA");
threadA.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+ " sleep over ");
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName()+ " over ");
}
}
运行结果与上面一致。
二、灵活
如果只是LockSupport在使用起来比Object的wait/notify简单,那还真没必要专门讲解下LockSupport。最主要的是灵活性。
上边的例子代码中,主线程调用了Thread.sleep(1000)方法来等待线程A计算完成进入wait状态。如果去掉Thread.sleep()调用,代码如下:
package com.aop8.locksupport;
public class TestObjWait1 {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" sum = "+sum);
}
},"ThreadA");
threadA.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
// Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" sleep over ");
synchronized (obj){
obj.notify();
}
System.out.println(Thread.currentThread().getName()+" over ");
}
}
多运行几次上边的代码,有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。
原因就在于:主线程调用完notify后,线程A才进入wait方法,导致线程A一直阻塞住。由于线程A不是后台线程,所以整个程序无法退出。
那如果换做LockSupport呢?LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞吗?是的,没错。代码如下:
package com.aop8.locksupport;
import java.util.concurrent.locks.LockSupport;
public class TestObjWait2 {
public static void main(String[] args)throws Exception {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(Thread.currentThread().getName()+ " sum = "+sum);
}
},"ThreadA");
threadA.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
//Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+ " sleep over ");
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName()+ " over ");
}
}
不管你执行多少次,这段代码都能正常打印结果并退出。这就是LockSupport最大的灵活所在。
分析:
unpark函数可以先于park调用。比如 Main调用unpark函数,给 线程ThreadA 发了一个“许可”,那么当 线程ThreadA 调用park时,它发现已经有“许可”了,那么它会马上再继续运行。
LockSupport的 unpark() 可以先于park() 调用,这个正是它们的灵活之处。
一个线程它有可能在别的线程unPark之前,或者之后,或者同时调用了park,那么因为park的特性,它可以不用担心自己的park的时序问题。
总结一下,LockSupport比Object的wait/notify有两大优势:
①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
三、应用广泛
LockSupport在Java的工具类用应用很广泛,咱们这里找几个例子感受感受。以Java里最常用的类ThreadPoolExecutor为例。先看如下代码:
public class TestObjWait {
public static void main(String[] args)throws Exception {
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(1000);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1000, TimeUnit.SECONDS,queue);
Future<String> future = poolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return "hello";
}
});
String result = future.get();
System.out.println(result);
}
}
代码中我们向线程池中扔了一个任务,然后调用Future的get方法,同步阻塞等待线程池的执行结果。
这里就要问了:get方法是如何组塞住当前线程?线程池执行完任务后又是如何唤醒线程的呢?
咱们跟着源码一步步分析,先看线程池的submit方法的实现:
在submit方法里,线程池将我们提交的基于 Callable 实现的任务,封装为基于 RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。
进入newTaskFor方法,就一句话:return new FutureTask<T>(callable);
所以,咱们主线程调用 future 的 get 方法就是 FutureTask
的get方法,线程池执行的任务对象也是 ~的实例。
接下来看看 FutureTask
的get方法的实现:
比较简单,就是判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。
awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程。
上边已经说完了阻塞等待任务结果的逻辑,接下来再看看线程池执行完任务,唤醒等待线程的逻辑实现。
前边说了,咱们提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:
c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,想必唤醒线程的工作就在这里边了,看看代码实现吧:
没错就在这里边,先是通过 CAS 操作将所有等待的线程拿出来,然后便使用LockSupport的unpark唤醒每个线程。
在使用线程池的过程中,不知道你有没有这么一个疑问:线程池里没有任务时,线程池里的线程在干嘛呢?
看过我的这篇文章《线程池的工作原理与源码解读》的读者一定知道,线程会调用队列的take方法阻塞等待新任务。那队列的take方法是不是也跟Future的get方法实现一样呢?咱们来看看源码实现。
以ArrayBlockingQueue为例,take方法实现如下:
与想象的有点出入,他是使用了Lock的Condition的await方法实现线程阻塞。但当我们继续追下去进入await方法,发现还是使用了LockSupport:
限于篇幅,jdk里的更多应用就不再追下去了。
五、示例
LockSupport是用来创建锁和其他同步类的基本线程阻塞。
LockSupport中的 park() 和 unpark() 的作用分别是 阻塞线程和解除阻塞线程,而且park()和unpark() 不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。
WaitTest1
对比下面的“示例1”和“示例2”可以更清晰的了解LockSupport的用法。
package com.aop8.locksupport;
public class WaitTest1 {
public static void main(String[] args) {
ThreadA ta = new ThreadA("ta");
synchronized(ta) { // 通过synchronized(ta)获取“对象ta的同步锁”
try {
System.out.println(Thread.currentThread().getName()+" start ta");
ta.start();
System.out.println(Thread.currentThread().getName()+" block");
// 主线程等待
ta.wait();
System.out.println(Thread.currentThread().getName()+" continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ThreadA extends Thread{
public ThreadA(String name) {
super(name);
}
public void run() {
synchronized (this) { // 通过synchronized(this)获取“当前对象的同步锁”
System.out.println(Thread.currentThread().getName()+" wakup others");
notify(); // 唤醒“当前对象上的等待线程”
}
}
}
}
LockSupportTest1
package com.aop8.locksupport;
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest1 {
private static Thread mainThread;
public static void main(String[] args) {
ThreadA ta = new ThreadA("ta");
// 获取主线程
mainThread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+" start ta");
ta.start();
System.out.println(Thread.currentThread().getName()+" block");
// 主线程阻塞
LockSupport.park(mainThread);
System.out.println(Thread.currentThread().getName()+" continue");
}
static class ThreadA extends Thread{
public ThreadA(String name) {
super(name);
}
public void run() {
System.out.println(Thread.currentThread().getName()+" wakup others");
// 唤醒“主线程”
LockSupport.unpark(mainThread);
}
}
}
运行结果:
main start ta
main block
ta wakup others
main continue
说明:park和wait的区别。wait让线程阻塞前,必须通过synchronized获取同步锁。