Java 源码 - java.util.concurrent.lock.LockSupport

129 阅读6分钟

简介

很多的类,包括ReentrantLock,都离不开LockSuport的支持,所以,就开始分析LockSupport,因为它是锁中的基础,是一个提供锁机制的工具类,所以先对其进行分析。

LockSupport的设计思路

LockSupport的设计思路就是为没一个线程设置一个许可,其实就是一个值,之前我们讲解锁的时候AQS中讲解的是state这个值。LockSupport实现思路也类似,设置一个许可(permit),这个permit就相当于一个开关,0代表关闭,1代表打开。默认是0的状态。调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累,也就是说你可以重复调用多次unpark方法这个值也一直是1,而不会因为多次调用而累加,调用一次park后这个值就会变成0,线程就会被阻塞。你也可以先调用unpark方法,将值先设置为1,然后再调用park方法,调用park方法时这个permit已经时1了那么就会立即返回,不会阻塞线程,并且将permit设置为0.这就是开篇说到的与Thread.suspend()和resume()由于先后顺序而产生的死锁的区别。

构造方法

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

说明:LockSupport只有一个私有构造函数,无法被实例化。

属性

    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    private static final long parkBlockerOffset;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;

一般程序中不允许直接调用,而long型的表示实例对象相应字段在内存中的偏移地址,可以通过该偏移地址获取或者设置该字段的值。

对于parkBlockerOffset,还记得Thread类有个成员叫parkBlocker吗,它表示线程阻塞在哪个对象上,这里取其相对与Thread的偏移地址,方便后续操作。那么这里有个问题,为什么不使用set,get方法进行访问呢?试想如果线程处于阻塞状态,此时要获取这个parkBlocker对象,使用set,get方法,线程是没有办法响应的!所以只能获取到地址,然后获取对象.

静态加载

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }

Static value: SEED, PROBE, SECONDARY are used by below method, called from StampedLock

    /**
     * Returns the pseudo-randomly initialized or updated secondary seed.
     * Copied from ThreadLocalRandom due to package access restrictions.
     */
    static final int nextSecondarySeed() {
        int r;
        Thread t = Thread.currentThread();
        if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
            r ^= r << 13;   // xorshift
            r ^= r >>> 17;
            r ^= r << 5;
        }
        else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)
            r = 1; // avoid zero
        UNSAFE.putInt(t, SECONDARY, r);
        return r;
    }

关于Unsafe的park()/unpark()

在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数,下面给出两个函数的定义。  

public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);

说明:对两个函数的说明如下

① park函数,阻塞线程,并且该线程在下列情况发生之前都会被阻塞:① 调用unpark函数,释放该线程的许可。② 该线程被中断。③ 设置的时间到了。并且,当time为绝对时间时,isAbsolute为true,否则,isAbsolute为false。当time为0时,表示无限等待,直到unpark发生。

② unpark函数,释放线程的许可,即激活调用park后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活。

park函数 

park函数有两个重载版本,方法摘要如下  

public static void park()public static void park(Object blocker)

说明:两个函数的区别在于park()函数没有没有blocker,即没有设置线程的parkBlocker字段。

基本方法

park(Object blocker)

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

说明:调用park函数时,首先获取当前线程,然后设置当前线程的parkBlocker字段,即调用setBlocker函数,之后调用Unsafe类的park函数,之后再调用setBlocker函数。那么问题来了,为什么要在此park函数中要调用两次setBlocker函数呢?原因其实很简单,调用park函数时,当前线程首先设置好parkBlocker字段,然后再调用Unsafe的park函数,此后,当前线程就已经阻塞了,等待该线程的unpark函数被调用,所以后面的一个setBlocker函数无法运行,unpark函数被调用,该线程获得许可后,就可以继续运行了,也就运行第二个setBlocker,把该线程的parkBlocker字段设置为null,这样就完成了整个park函数的逻辑。如果没有第二个setBlocker,那么之后没有调用park(Object blocker),而直接调用getBlocker函数,得到的还是前一个park(Object blocker)设置的blocker,显然是不符合逻辑的。总之,必须要保证在park(Object blocker)整个函数执行完后,该线程的parkBlocker字段又恢复为null。所以,park(Object)型函数里必须要调用setBlocker函数两次。setBlocker方法如下。 

setBlocker(Thread t, Object arg)

    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }

说明:此方法用于设置线程t的parkBlocker字段的值为arg。

park()

public static void park() {
        UNSAFE.park(false, 0L);
    }

说明:调用了park函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行。

  ① 其他某个线程将当前线程作为目标调用 unpark。

  ② 其他某个线程中断当前线程。

  ③ 该调用不合逻辑地(即毫无理由地)返回。

parkNanos(long nanos)

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }

此函数表示在许可可用前禁用当前线程,并最多等待指定的等待时间。

说明:该函数也是调用了两次setBlocker函数,nanos参数表示相对时间,表示等待多长时间。

parkUntil(Object blocker, long deadline)

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

该函数也调用了两次setBlocker函数,deadline参数表示绝对时间,表示指定的时间。

unpark(Thread thread)

    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

参数thread是我们要唤醒的目标线程,先判空,然后调用UNSAFE.unpark,UNSAFE是Unsafe对象,不要被这个名字吓到,这个类提供了很多有用的方法,之前的文章也有提到过,比如获取类对象中属性的内存偏移地址,还有 CAS操作等。但是这个Unsafe对象必须使用反射得到然后才能正常使用,因为getUnsafe方法有判断当前类加载器是不是BootStrapClassLoader。

LockSupport 的简单使用

先来看一个简单的例子

    public static void main(String[] args) {

        Thread worker = new Thread(() -> {
            LockSupport.park();
            System.out.println("start work");
        });

        worker.start();

        System.out.println("main thread sleep");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(worker);

        try {
            worker.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    // 最终控制台输出结果
    main thread sleep
    start work

启动一个worker线程,主线程先sleep 500ms,worker线程因为调用了LockSupport的park,会等待,直到主线程sleep结束,调用unpark唤醒worker线程。那么在JUC之前,我们常用的让线程等待的方法如下

        Object monitor = new Object();
        synchronized (monitor) {
            try {
                monitor.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

主要有三点区别

  1. LockSupport.park和unpark不需要在同步代码块中,wait和notify是需要的。
  2. LockSupport的pork和unpark是针对线程的,而wait和notify是可以是任意对象。
  3. LockSupport的unpark可以让指定线程被唤醒,但是notify是随机唤醒一个,notifyAll是全部唤醒,不够灵活。