LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现
如果只是LockSupport在使用起来比Object的wait/notify简单,
那还真没必要专门讲解下LockSupport。最主要的是灵活性。
上边的例子代码中,主线程调用了Thread.sleep(1000)方法来等待线程A计算完成进入wait状态。如果去掉Thread.sleep()调用,代码如下:
public class TestObjWait { public static void main(String[] args)throws Exception { final Object obj = new Object(); Thread A = 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(sum); } }); A.start(); //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法 //Thread.sleep(1000); synchronized (obj){ obj.notify(); } } }多运行几次上边的代码,有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。原因就在于:主线程调用完notify后,线程A才进入wait方法,
导致线程A一直阻塞住。由于线程A不是后台线程,所以整个程序无法退出。 那如果换做LockSupport呢?LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞吗?是的,没错。代码如下:
public class TestObjWait { public static void main(String[] args)throws Exception { final Object obj = new Object(); Thread A = new Thread(new Runnable() { @Override public void run() { int sum = 0; for(int i=0;i<10;i++){ sum+=i; } LockSupport.park(); if (Thread.currentThread().isInterrupted()) { System.out.println("被中断了"); } System.out.println("继续执行"); System.out.println(sum); } }); A.start(); //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法 //Thread.sleep(1000); LockSupport.unpark(A); } }不管你执行多少次,这段代码都能正常打印结果并退出。这就是LockSupport最大的灵活所在。
LockSupport比Object的wait/notify有几点优势:
LockSupport.park和unpark不需要在同步代码块中,wait和notify是需要的。 LockSupport的pork和unpark是针对线程的,而wait和notify是可以是任意对象。 LockSupport的unpark可以让指定线程被唤醒,但是notify是随机唤醒一个,notifyAll是全部唤醒,不够灵活。
同时LockSupport还有一点需要注意: park() 在中断或唤醒后,不会像sleep()一样去清除interrupt标志位,也不清楚自己是否是由于中断被唤醒,所以需要自己手动去查看是否被中断标记。
那park和unpark却不会出现死锁,这是为什么呢?
unpark调用时,如果当前线程还未进入park,则许可为true park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true。
注意:LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。
blocker 对象参数 此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。在锁实现中提供的作为 blocker 的普通参数是 this。
park原理流程图:
unpark原理流程图:
park() 源码:
void Parker::park(bool isAbsolute, jlong time) { if (Atomic::xchg(0, &_counter) > 0) return; // 若 _counter > 0 直接设置 _counter=0并返回 ThreadBlockInVM tbivm(jt); // mutex加锁 if (_counter > 0) { // no wait needed _counter = 0; // 如果 _counter > 0,直接设置 _counter=0解锁mutex,并返回 status = pthread_mutex_unlock(_mutex); //解锁mutex return; } if (time == 0) { status = pthread_cond_wait (_cond, _mutex) ; // 否则,condition条件等待,直到unpark()调用pthread_cond_signal() 唤醒后,继续向下执行 } _counter = 0 ; //唤醒后,直接设置_counter为0 status = pthread_mutex_unlock(_mutex) ; //结束mutex assert_status(status == 0, status, "invariant") ; OrderAccess::fence();unpark() 源码:
void Parker::unpark() { int s, status ; status = pthread_mutex_lock(_mutex); // 互斥量mutex加锁 assert (status == 0, "invariant") ; //是否成功判断 s = _counter; _counter = 1; //直接设置_counter为1 /** * 此时 * 若 _counter = 0,说明park() 方法此时可能在睡眠中等待一个permit,需要unpark的signal唤醒 * 若 _counter = 1,说明本身就是有permit的,没有park()在此时睡眠,不用做额外操作 */ if (s < 1) { // 判断_counter是否为0 if (WorkAroundNPTLTimedWaitHang) { status = pthread_cond_signal (_cond) ; assert (status == 0, "invariant") ; status = pthread_mutex_unlock(_mutex); assert (status == 0, "invariant") ; } else { status = pthread_mutex_unlock(_mutex); assert (status == 0, "invariant") ; status = pthread_cond_signal (_cond) ; assert (status == 0, "invariant") ; } } else { pthread_mutex_unlock(_mutex); assert (status == 0, "invariant") ; } // 无论 _counter 旧值为几,最后都是要mutex解锁的,因为unpark最开始加了mutex锁。不做会导致mutex锁一直存在无法被其他线程获取到mutex锁 }注意:park不会像wait()一样睡眠释放当前锁,park更类似sleep() 睡眠,所以需要自我管理当前锁的释放与否。
LockSupport中主要的两个成员变量:
// Hotspot implementation via intrinsics API private static final sun.misc.Unsafe UNSAFE; private static final long parkBlockerOffset;unsafe:全名sun.misc.Unsafe可以直接操控内存,被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是不建议在生产环境中使用这个类。因为这个API十分不安全、不轻便、而且不稳定。 LockSupport的方法底层都是调用Unsafe的方法实现。
再来看parkBlockerOffset: parkBlocker就是前面说到的用于记录线程被谁阻塞的,用于线程监控和分析工具来定位原因的,可以通过LockSupport的getBlocker获取到阻塞的对象。
static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> tk = Thread.class; parkBlockerOffset = UNSAFE.objectFieldOffset (tk.getDeclaredField("parkBlocker")); } catch (Exception ex) { throw new Error(ex); } }从这个静态语句块可以看的出来,先是通过反射机制获取Thread类的parkBlocker字段对象。然后通过sun.misc.Unsafe对象的objectFieldOffset方法获取到parkBlocker在内存里的偏移量,parkBlockerOffset的值就是这么来的.
JVM的实现可以自由选择如何实现Java对象的“布局”,也就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。 sun.misc.Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java 对象的某个字段。
为什么要用偏移量来获取对象?干吗不要直接写个get,set方法。多简单? 仔细想想就能明白,这个parkBlocker就是在线程处于阻塞的情况下才会被赋值。线程都已经阻塞了,如果不通过这种内存的方法,而是直接调用线程内的方法,线程是不会回应调用的。
设置和读取parkBlocker方法
private static void setBlocker(Thread t, Object arg) { // Even though volatile, hotspot doesn't need a write barrier here. UNSAFE.putObject(t, parkBlockerOffset, arg); } public static Object getBlocker(Thread t) { if (t == null) throw new NullPointerException(); return UNSAFE.getObjectVolatile(t, parkBlockerOffset); }