LockSupport 使用总结和注意事项

转载: 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方法的实现:

img

在submit方法里,线程池将我们提交的基于 Callable 实现的任务,封装为基于 RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。

进入newTaskFor方法,就一句话:return new FutureTask<T>(callable);

所以,咱们主线程调用 future 的 get 方法就是 FutureTask 的get方法,线程池执行的任务对象也是 ~的实例。

接下来看看 FutureTask 的get方法的实现:

img

比较简单,就是判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。

img

awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程。

上边已经说完了阻塞等待任务结果的逻辑,接下来再看看线程池执行完任务,唤醒等待线程的逻辑实现。

前边说了,咱们提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:

img

c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,想必唤醒线程的工作就在这里边了,看看代码实现吧:

img

没错就在这里边,先是通过 CAS 操作将所有等待的线程拿出来,然后便使用LockSupport的unpark唤醒每个线程。

在使用线程池的过程中,不知道你有没有这么一个疑问:线程池里没有任务时,线程池里的线程在干嘛呢?

看过我的这篇文章《线程池的工作原理与源码解读》的读者一定知道,线程会调用队列的take方法阻塞等待新任务。那队列的take方法是不是也跟Future的get方法实现一样呢?咱们来看看源码实现。

以ArrayBlockingQueue为例,take方法实现如下:

img

与想象的有点出入,他是使用了Lock的Condition的await方法实现线程阻塞。但当我们继续追下去进入await方法,发现还是使用了LockSupport:

img

限于篇幅,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获取同步锁。

猜你喜欢

转载自blog.csdn.net/xiaojin21cen/article/details/89918452