天天看点

白话java锁--LockSupportLockSupport方法

通过前面几篇文章的分析,知道AQS中使用的线程阻塞方法是LockSupport.park(this);方法,接下来就继续分析一下这个方法具体是实现的

先来看一下这个类中的几个方法

LockSupport方法

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;
    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); }
    }
           

是在静态代码块中实例化的,是通过Unsafe.getUnsafe();获得到UNSAFE实例,通过UNSAFE实例去控制线程的(以后会专门研究Unsafe的用途的)

关于上述的用法,可以简单的理解为,获取parkBlocker字段在内存中的实际值(因为内存中的实际值在多线程的情况下值可能不同)

所以获得LockSupport的过程实际上就是获取UNSAFE的过程

LockSupport中阻塞的方法

public static void park()

public static void park(Object blocker) 

public static void parkNanos(long nanos)

public static void parkNanos(Object blocker, long nanos)

public static void parkUntil(long deadline)

public static void parkUntil(Object blocker, long deadline)
           

通过观察这几个方法,我们可以发现,加锁方法可以分为几类

LockSupport阻塞–是否可以设置blocker

public static void park()

public static void park(Object blocker) 
           

park()有两个重载方法,一个是可以无参的方法,一个是可以设置blocker的方法,那么blocker又是什么?

其实blocker就是一个监控器,用来打印详细日志的,用于问题排查的,具体分析时可以提供当前线程阻塞在哪个对象上

历史缘由:Java 5时,如果使用synchronized关键字阻塞在一个某一个对象上的时候,通过dump能够看到这个线程的阻塞在哪个对象上,而Java 5推出的Lock等并发工具却遗漏了这一点,导致在线程dump时无法提供阻塞在对象上的信息。因此,在Java 6中,LockSupport新增了上述3个含有监控对象的方法,用来替代原有的park方法

LockSupport阻塞–设置超时方式

public static void park()

public static void parkNanos(long nanos)

public static void parkUntil(long deadline)
           

通过设置超时方式可以分为三种

  • park():永远阻塞
  • parkNanos(long nanos):阻塞当前线程,不能超过nanos时间的限制
  • parkUntil(long deadline):阻塞当前线程,直到deadline时间点唤醒

LockSupport阻塞源码分析

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

通过上述几个方法的源码,我们发现实际上调用的还是UNSAFE类中的方法,通过(boolean var1, long var2)两个参数控制线程执行等待的策略

但是,实际上调用的是native的方法,我们都知道native方法是本地方法,是用本地代码(C++)来做的,因此性能也会很快

而且我们发现blocker实际上不是UNSAFE类中的方法,是在LockSupport类中的属性,每次都需要重新设置blocker属性

LockSupport阻塞底层实现

因为我对于C++也不是那么太熟悉,所以我这里就简单的引用一下别人的分析

白话java锁--LockSupportLockSupport方法

它调用了线程的Parker类型对象的park方法

白话java锁--LockSupportLockSupport方法

类中定义了一个int类型的_counter变量,实际上就是通过这个变量实现的线程阻塞和唤醒

白话java锁--LockSupportLockSupport方法

park方法会调用Atomic::xchg方法,这个方法会原子性的将_counter赋值为0,并返回赋值前的值。如果调用park方法前,_counter大于0,则说明之前调用过unpark方法(因为unpark方法会将_counter的值设置大于0),然后park方法直接返回。将_counterf 数值置为0

白话java锁--LockSupportLockSupport方法

再次判断_counter是否大于零,如果大于零则将_counter设置为零。最后调用pthread_mutex_unlock解锁(也就是之前调用过unpark方法了)执行完退出同步代码块。

如果_counter不大于零,则继续往下执行pthread_cond_wait方法,实现当前线程的阻塞。

LockSupport中唤醒的方法

我们发现LockSupport的唤醒方法十分简单,直接将线程对象传递进去就可以解锁了

LockSupport唤醒源码分析

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

实际上调用的也是UNSAFE中的方法

同样是native类型的本地方法,是C++实现的

LockSupport唤醒底层实现

白话java锁--LockSupportLockSupport方法

代码2将_counter设置为1

判断先前_counter的值是否小于1,即这段代码:if(s<1) ,如果大于等于1,则就不会有线程被park,所以方法直接执行完毕,如果小于1 说明有线程被 park 了 就会执行代码3,来唤醒被阻塞的线程

通过对以上代码的分析,我们可以发现解锁只是将_counter的值设置为1,那么,假如现在有一个线程同时调用了两次unpark方法,那么再对线程进行两次park的时候线程会阻塞,因为park仅仅是将_counter设置为0,第二次调用的时候还是会将这个线程阻塞住,但是如果调用两次park后,再次调用一次unpark就可以将线程解锁,所以控制起来就很灵活,只要理解LockSupport实际上是通过_counter控制线程的阻塞和唤醒的,可以把_counter理解成一个许可,当调用park的时候就是不允许的许可放到那,当调用unpark的时候就是允许的许可放到那,即使先调用unpark把允许的许可放那了,再调用park的时候,发现可以的许可已经在了,那么就不用阻塞了,直接往下进行

所以说park和unpark对线程的阻塞和唤醒起到了很灵活的作用,所以每次都要拿来和线程的wait和notify方法进行对比

LockSupport与wait/notify比较

首先,LockSupport的park/unpark并不和wait/notify交叉,unpark不会对wait起作用,notify也不会对park起作用

其次,LockSupport不需要在同步代码块里。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。而wait/notify需要在同一锁对象中,如果不在同一锁对象中,会抛出异常

再次,unpark函数可以优先于park调用,所以不需要担心线程间的执行先后顺序。而wait/notify中,notify需要在wait之后,否则会发生线程饥饿的情况,而且使用notify只能唤醒一个线程,一般需要使用notifyAll方法

其实总结起来就是:LockSupport更新灵活的方式控制线程的阻塞和唤醒,不需要锁对象的控制,我们可以理解java底层已经对这个类似于锁对象的东西进行封装了,实际上就是这个_counter(许可),而且可以不需要注意调用的先后顺序

LockSupport的interrupt

对于LockSupport中的park中的中断处理为,如果线程调用park方法后,此时线程被中断,此时这个线程不会抛出InterruptedException异常,而是会继续进行,然后设置当前线程的中断状态为true,直到当前线程执行完毕,效果就和调用这个线程的unpark一样。

所以LockSupport的park/unpark对中断的处理也是非常完美的。