JDK源码阅读计划(Day15) j.u.c 之 LockSupport

基于JDK11

LockSupport介绍

LockSupport是JUC中用于创建锁和其他同步类的基本线程阻塞原语。

其提供的park和unpark类似于Object类的wait和notify语义,但是前者能够针对指定线程进行阻塞和唤醒操作

我们参考啃透JAVA并发先跑一个demo

public static void main(String[] args) {

        Thread t = new Thread(()->{
            LockSupport.park();
            System.out.println("Start working");
        });

        //t线程开始运行
        t.start();

        try {
            System.out.println("Sleeping");
            t.sleep(500);
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            //主线程运行到这里,唤醒t线程,t线程再执行打印
            LockSupport.unpark(t);
        }

        try {
            //等待t线程执行完之后,主线程再继续执行,防止主线程结束后t线程还没结束
            t.join();
        } catch (InterruptedException e){
            e.printStackTrace();
        }

    }

可以看到LockSupport提供的park和unpark方法其实和object类的wait和notify方法相似,都是起到阻塞线程和唤醒线程的作用。

如果是用wait和notify实现上面的逻辑:

public static void main(String[] args) {

        Object object = new Object();

        Thread t = new Thread(()->{
            synchronized (object){
                try{
                    //当前线程挂起
                    object.wait();
                } catch (InterruptedException e){
                    e.printStackTrace();
                }finally {
                    System.out.println("Start working");
                }
            }
        });

        //t线程开始运行
        t.start();

        try{
            System.out.println("Sleeping");
            Thread.sleep(1000);
            synchronized (object){
                //唤醒持有object为Monitor的线程t
                object.notifyAll();
            }
        } catch (InterruptedException e){
            e.printStackTrace();
        }

        try {
            //等待t线程执行完之后,主线程再继续执行,防止主线程结束后t线程还没结束
            t.join();
        } catch (InterruptedException e){
            e.printStackTrace();
        }

    }

我们可以比较下两者不同:

  • LockSupport的话,不需要在同步块中调用方法,而Object类的wait和notify需要在synchronize同步块中
  • LockSupport的unpark可以让指定线程被唤醒,参数就是指定线程,但是notify是随机唤醒一个,notifyAll是全部唤醒,不够灵活
  • park与unpark是针对线程,而wait和notify针对对象
  • 在调用notify之前必须先调用wait,否则会报错,然而如果先调用unpark再调用park并不会报错,相当于提前给线程“发放通行证”
  • park会响应中断,但不会抛出异常,即如果该线程在park阻塞过程中被中断,函数会立即返回。

我们了解完用法之后正式进入源码部分。注释给出一个使用park和unpark实现的FIFO Mutex的代码,可以学习一波:

public class FIFOMutex {

    static {
        Class<?> ensuredLoaded = LockSupport.class;
    }

    private final AtomicBoolean locked = new AtomicBoolean(false);

    private final Queue<Thread> waitingQueue = new ConcurrentLinkedDeque<>();

    public void lock(){

        boolean isInterrupted = false;

        //当前线程入队
        waitingQueue.add(Thread.currentThread());

        //加入当前线程不是队头或者设置lock失败,即上一个获得锁的线程还没解锁成功 就park阻塞线程
        while(waitingQueue.peek()!=Thread.currentThread()||!locked.compareAndSet(false, true)){
            LockSupport.park(this);
            //要考虑中断的问题
            if(Thread.interrupted()){
                isInterrupted = true;
            }
        }

        //移除队头
        waitingQueue.remove();
        if(isInterrupted){
            //设置中断
            Thread.currentThread().interrupt();
        }

    }

    public void unlock(){
        locked.set(false);
        LockSupport.unpark(waitingQueue.peek());
    }
    
}

重要成员

// Hotspot implementation via intrinsics API
    private static final Unsafe U = Unsafe.getUnsafe();
    //获得这些字段在线程对象的偏移量
    private static final long PARKBLOCKER = U.objectFieldOffset(Thread.class, "parkBlocker");
    private static final long SECONDARY   = U.objectFieldOffset(Thread.class, "threadLocalRandomSecondarySeed");
    private static final long TID         = U.objectFieldOffset(Thread.class, "tid");

构造函数

构造函数是私有的

    private LockSupport() {
    } // Cannot be instantiated.

唤醒线程 Unpark

 /*
     * 发给目标线程一个许可证,该许可证被park消费,用于唤醒被park阻塞的线程
     *
     * 该许可证可以提前发给线程备用,也可以等线程陷入阻塞后,在等待许可证时再(由另一个线程)给它,进而唤醒线程。
     * 连续重复发给线程的许可证只被视为一个许可证。
     */
    public static void unpark(Thread thread) {
        if(thread != null) {
            U.unpark(thread);
        }
    }

具体是UnSafe类的unpark本地方法实现的,在C++中加锁解锁分别是用的是pthread_mutex_lockpthread_mutex_unlock,唤醒线程则是pthread_cond_signal

阻塞线程

该线程在下列情况发生之前都会被阻塞:① 调用unpark函数,释放该线程的许可。② 该线程被中断。③ 设置的时间到了

该操作对中断标记位为中断的线程无效

有两种重载的方法:

// 作用同park(blocker)方法,只是不设置阻塞标记
    public static void park() {
        U.park(false, 0L);
    }

这种方法不会设置阻塞标记

/*
     * 等待消费一个许可证,这会使线程陷入阻塞。blocker参数仅作为线程阻塞标记。
     *
     * 如果提前给过许可,则线程继续执行。
     * 如果陷入阻塞后等待许可,则可由别的线程发给它许可(在别的线程中调用unpark)。
     * 使用线程中断也可以唤醒陷入阻塞的线程。
     *
     * 注:对标记为中断的线程使用阻塞无效
     */
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        
        // 标记线程陷入了阻塞
        setBlocker(t, blocker);
        
        // 使线程一直陷入阻塞,直到被唤醒
        U.park(false, 0L);
        
        // 标记线程脱离了阻塞
        setBlocker(t, null);
    }

我们来看看这个设置阻塞标记的方法,这个方法的作用在于方便调试,方便看到线程在哪个对象阻塞了。

// 获取线程的阻塞标记
    public static Object getBlocker(Thread t) {
        if(t == null) {
            throw new NullPointerException();
        }
        return U.getObjectVolatile(t, PARKBLOCKER);
    }
    
    // 设置线程t的parkBlocker字段为arg,用来标记该线程陷入了阻塞
    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        U.putObject(t, PARKBLOCKER, arg);
    }

park具体也是UnSafe类的park本地方法实现的,核心就是C++有一个int变量_count,如果_count>0则可以通行,不阻塞。即park方法会直接返回,另外park方法返回后,_counter会被赋值为0,unpark方法可以将_counter置为1,并且唤醒当前等待的线程。所以本质上park,unpark底层用的是二元信号量来实现。所以:

如果在park()之前执行了unpark()会怎样?

线程不会被阻塞,直接跳过park(),继续执行后续内容

一些面试问题总结:

  • park会释放掉线程的锁吗?

不会,释放锁是在condition.await()释放,但是await方法又是依赖park来实现阻塞线程的。

  • park和sleep,wait有何区别?

park,wait语义都是线程间通信,而sleep的语义只是让线程失去CPU时间片,并不会释放锁。而wait会释放掉锁。但是sleep与wait,park都是进入线程的WAITING状态.我理解WAITING状态并不是自旋,因为自旋其实是还是占有CPU时间片的。可以理解为线程进入了等待队列,

ref

https://www.cnblogs.com/tong-yuan/p/11768904.html

你可能感兴趣的:(JAVA)