005-多线程-锁-JUC锁-LockSupport【使用、Unsafe、对比Object的wait、底层源码】
一、概述
在Java多线程中,当需要阻塞或者唤醒一个线程时,都会使用LockSupport工具类来完成相应的工作。LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也因此成为了构建同步组件的基础工具。
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。
实现的阻塞和解除阻塞是基于”许可(permit)”作为关联,permit相当于一个信号量(0,1),默认是0. 线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态.
在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。
1.1、支持的函数
// 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。 static Object getBlocker(Thread t) // 为了线程调度,禁用当前线程,除非许可可用。 static void park() // 为了线程调度,在许可可用之前禁用当前线程。 static void park(Object blocker) // 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。 static void parkNanos(long nanos) // 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。 static void parkNanos(Object blocker, long nanos) // 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。 static void parkUntil(long deadline) // 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。 static void parkUntil(Object blocker, long deadline) // 如果给定线程的许可尚不可用,则使其可用。 static void unpark(Thread thread)
在Java 6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker, long nanos)、parkUntil(Object blocker, long deadline)这3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。下面的示例中,将对比parkNanos(long nanos)和parkNanos(Object blocker, long nanos)方法来展示阻塞对象blocker的用处。
采用parkNanos(long nanos)阻塞线程:
public class LockSupportTest { public static void main(String[] args) { LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(20)); } }
可以通过命令:jstack -l pid查看dump
"main" #1 prio=5 os_prio=31 tid=0x00007fae82000000 nid=0x2603 waiting on condition [0x000070000eccb000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:338) at com.github.bjlhx15.common.thread.juc.collection.LockSupportTest.main(LockSupportTest.java:8)
采用parkNanos(Object blocker, long nanos)阻塞线程:
public class LockSupportTest1 { public static void main(String[] args) { LockSupport.parkNanos(new Object(), TimeUnit.SECONDS.toNanos(20)); } }
可以通过命令:jstack -l pid查看dump
"main" #1 prio=5 os_prio=31 tid=0x00007fb3b5813000 nid=0x2603 waiting on condition [0x000070000861d000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000076acfb350> (a java.lang.Object) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) at com.github.bjlhx15.common.thread.juc.collection.LockSupportTest1.main(LockSupportTest1.java:8)
这两段代码都是 阻塞当前线程20秒,从上面的dump结果可以看出,有阻塞对象的parkNanos方法能够传递给开发人员更多的现场信息。这是由于在Java 5之前,当线程使用synchronized关键字阻塞在一个对象上时,通过线程dump能够看到该线程的阻塞对象,而Java 5推出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息。因此,在Java 6中,LockSupport新增了上述3个含有阻塞对象的方法,用以替代原有的park方法。
通过源码可以发现,LockSupport的park和unpark方法都是通过sun.misc.Unsafe类的park和unpark方法实现的,那下面我们对sun.misc.Unsafe类的源码进行进一步解析。
1.2、LockSupport源码分析
从源码中得到初步信息:
1、不能被实例化(构造函数是私有的)
2、方法都是静态方法
3、核心方法实现基于Unsafe类中的park和unpark方法
1.2.1、UNSAFE
JDK内部用的工具类, 可以直接操控内存,被JDK广泛用于自己的包中.它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。该类不应该在JDK核心类库之外使用。
Unsafe类就和它的名字一样,是一个比较危险的类,它主要用于执行低级别、不安全的方法。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。如果我们要使用Unsafe类,首先需要获取Unsafe类的对象,但是它的构造函数是private的:
private Unsafe() {}
只能通过Unsafe的getUnsafe()方法获取该类的对象:
@CallerSensitive public static Unsafe getUnsafe() { Class<?> caller = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(caller.getClassLoader())) throw new SecurityException("Unsafe"); return theUnsafe; }
Unsafe类是比较危险的,它只有在授信代码中才会返回theUnsafe对象,否则,抛出SecurityException异常,那什么是授信的代码呢?我们看一看getClassLoader()方法:
@CallerSensitive public ClassLoader getClassLoader() { ClassLoader cl = getClassLoader0(); if (cl == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass()); } return cl; }
该方法返回加载该类的类加载器,如果是被Bootstrap ClassLoader加载的类,则cl为null,然后我们再看VM.isSystemDomainLoader(ClassLoader)方法:
public static boolean isSystemDomainLoader(ClassLoader loader) { return loader == null; }
若类加载器为null,则返回true,即该代码为授信代码。所以,只要代码是被Bootstrap ClassLoader类加载器加载的类就是授信代码了。
我们知道Bootstrap ClassLoader类加载器会加载-Xbootclasspath参数所指定的路径中的类,所以,我们可以修改-Xbootclasspath参数,将我们的代码所在的路径添加进去,那我们的代码就可以使用Unsafe类了;或者也可以使用反射从 Unsafe类上得到它私有的Unsafe实例。如下所示:
public class UnsafeTest { public static void main(String[] args) throws Exception { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); } }
先看一下Unsafe的park方法和unpark方法:
public native void park(boolean isAbsolute, long time); public native void unpark(Object thread);
这两个类都是声明native的,其具体实现要去看Java虚拟机的源代码,接下来看一看Unsafe类的其他方法,看一看Unsafe还能做什么危险操作。
1.2.2、Unsafe修改内存
Unsafe类的putInt()和getInt()方法可以直接修改内存,如下面这一段代码:
public static void main(String[] args) throws Exception { unsafePutGetInt(); } private static Unsafe getUnsafe(){ try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); return unsafe; } catch (Exception e) { e.printStackTrace(); } return null; } public static void unsafePutGetInt() throws Exception { Unsafe unsafe = getUnsafe(); class Student { private int age = 5; public int getAge() { return age; } } Student student = new Student(); System.out.println(student.getAge()); Field field = student.getClass().getDeclaredField("age"); unsafe.putInt(student, unsafe.objectFieldOffset(field), 10); System.out.println(student.getAge()); } }
输出
5 10
通过Unsafe直接修改了类的private变量值。类似的还有getBoolean()、putBoolean()、getChar()、putChar()等方法。
1.2.3、Unsafe在非Java堆中分配内存
使用new关键字分配的内存会在堆中,并且对象的生命周期内,会被垃圾回收器管理。Unsafe类通过allocateMemory(long)方法分配的内存,不受Integer.MAX_VALUE的限制,并且分配在非堆内存,使用它时,需要非常谨慎,该部分内存需要手动回收,否则会产生内存泄露;非法的地址访问时,会导致Java虚拟机崩溃。在需要分配大的连续区域、实时编程时,可以使用该方式,java的nio使用了这一方法。
public class UnsafeTest3 { public static void main(String[] args) throws Exception { unsafeAllocateMemory(); } private static Unsafe getUnsafe(){ try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); return unsafe; } catch (Exception e) { e.printStackTrace(); } return null; } public static void unsafeAllocateMemory() throws Exception { Unsafe unsafe = getUnsafe(); int BYTE = 1; long address = unsafe.allocateMemory(BYTE); unsafe.putByte(address, (byte) 10); byte num = unsafe.getByte(address); System.out.println(num); unsafe.freeMemory(address); } }
输出
10
1.2.4、Unsafe提供CAS原子操作
Unsafe类中提供了compareAndSwapObject()、compareAndSwapInt()和compareAndSwapLong()这三个方法用来实现对应的CAS原子操作。在Java的并发编程中用到的CAS操作都是调用的Unsafe类的相关方法。我们以Unsafe实现一个自定义原子类:
public class UnsafeTest4 { public static void main(String[] args) throws Exception { unsafeCAS(); } private static Unsafe getUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); return unsafe; } catch (Exception e) { e.printStackTrace(); } return null; } public static void unsafeCAS() throws Exception { Unsafe unsafe = getUnsafe(); class MyAutomicInteger { private volatile int value = 0; private Unsafe unsafe; private long offset; public MyAutomicInteger(Unsafe unsafe) throws Exception { this.unsafe = unsafe; this.offset = unsafe.objectFieldOffset(MyAutomicInteger.class.getDeclaredField("value")); } public void increment() { int oldValue = value; for (;;) { if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue + 1)) { break; } oldValue = value; } } public int getAndIncrement() { int oldValue = value; for (;;) { if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue + 1)) { return oldValue; } oldValue = value; } } public int getValue() { return value; } } MyAutomicInteger myAutomicInteger = new MyAutomicInteger(unsafe); myAutomicInteger.increment(); System.out.println(myAutomicInteger.getValue()); for (int i = 0; i < 5; i++) { System.out.println(myAutomicInteger.getAndIncrement()); } System.out.println(myAutomicInteger.getValue()); } }
结果
1 1 2 3 4 5 6
可以看到,通过Unsafe类可以实现很多有趣的功能,这些方法都是比较底层的方法,而且效率比较高,但是使用起来却比较危险,因为Unsafe类中的方法与我们通常的用法相悖,比如,通过Unsafe类直接修改其他类的parivate变量,直接分配堆外内存等等,这很像c语言的malloc()方法。在平常的开发当中,并不建议直接使用Unsafe类。
1.3、LockSupport的park(),unPark()与Thread的wait(),notify()区别
1.面向的主体不一样。LockSuport主要是针对Thread进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程。
2.实现机制不同。虽然LockSuport可以指定monitor的object对象,但和object.wait(),两者的阻塞队列并不交叉。
3.实现机制和wait/notify有所不同,面向的是线程
4.LockSupport不需要依赖监视器
5.与wait/notify没有交集
6.LockSupport使用起来方便灵活
LockSupport比Object的wait/notify有两大优势:
①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
1.4、示例
1.4.1、使用Object的wait和notify
public static void objWaitNotify() throws InterruptedException { 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 { obj.wait(); } catch (Exception e) { e.printStackTrace(); } System.out.println(sum); } }); A.start(); //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法 Thread.sleep(1000); obj.notify(); }
出现错误
java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at com.github.bjlhx15.datastructure.algorithm.thread.LockSupportDemo$1.run(LockSupportDemo.java:18) at java.lang.Thread.run(Thread.java:748) 45 Exception in thread "main" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method) at com.github.bjlhx15.datastructure.algorithm.thread.LockSupportDemo.objWaitNotify(LockSupportDemo.java:28) at com.github.bjlhx15.datastructure.algorithm.thread.LockSupportDemo.main(LockSupportDemo.java:5)
原因,wait和notify/notifyAll方法只能在同步代码块里用。所以将代码修改为如下就可正常运行了:
public static void objWaitNotifySynchronized() throws InterruptedException { 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(); } }
1.4.2、使用LockSupport
public static void lockSupport() throws InterruptedException { Thread A = 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(sum); } }); A.start(); //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法 Thread.sleep(1000); LockSupport.unpark(A); }
二、应用
2.1、ThreadPoolExecutor中使用
public static void testFeture() throws ExecutionException, InterruptedException { 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方法,同步阻塞等待线程池的执行结果。
问题1、get方法是如何组塞住当前线程?线程池执行完任务后又是如何唤醒线程的呢?
等待
查看submit方法
public <T> Future<T> submit(Callable<T> task) { if (task == null) throw new NullPointerException(); RunnableFuture<T> ftask = newTaskFor(task); execute(ftask); return ftask; }
在submit方法里,线程池将我们提交的基于Callable实现的任务,封装为基于RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。
进入newTaskFor方法,就一句话:return new FutureTask<T>(callable);
所以,咱们主线程调用future的get方法就是FutureTask的get方法,线程池执行的任务对象也是FutureTask的实例。
接下来看看FutureTask的get方法的实现:
public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false, 0L); return report(s); }
判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。
private int awaitDone(boolean timed, long nanos) throws InterruptedException { final long deadline = timed ? System.nanoTime() + nanos : 0L; WaitNode q = null; boolean queued = false; for (;;) { if (Thread.interrupted()) { removeWaiter(q); throw new InterruptedException(); } int s = state; if (s > COMPLETING) { if (q != null) q.thread = null; return s; } else if (s == COMPLETING) // cannot time out yet Thread.yield(); else if (q == null) q = new WaitNode(); else if (!queued) queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); else if (timed) { nanos = deadline - System.nanoTime(); if (nanos <= 0L) { removeWaiter(q); return state; } LockSupport.parkNanos(this, nanos); } else LockSupport.park(this); } }
awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程。
唤醒
提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:
public void run() { if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { Callable<V> c = callable; if (c != null && state == NEW) { V result; boolean ran; try { result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); } if (ran) set(result); } } finally { // runner must be non-null until state is settled to // prevent concurrent calls to run() runner = null; // state must be re-read after nulling runner to prevent // leaked interrupts int s = state; if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); } }
c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,唤醒线程的工作就在这,
private void finishCompletion() { // assert state > COMPLETING; for (WaitNode q; (q = waiters) != null;) { if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { for (;;) { Thread t = q.thread; if (t != null) { q.thread = null; LockSupport.unpark(t); } WaitNode next = q.next; if (next == null) break; q.next = null; // unlink to help gc q = next; } break; } } done(); callable = null; // to reduce footprint }
先是通过cas操作将所有等待的线程拿出来,然后便使用LockSupport的unpark唤醒每个线程。
三、实现原理
3.1、park
进入LockSupport的park方法,可以发现它是调用了Unsafe的park方法,这是一个本地native方法,只能通过openjdk的源码看看其本地实现了。
它调用了线程的Parker类型对象的park方法,如下是Parker类的定义:
类中定义了一个int类型的_counter变量,咱们上文中讲灵活性的那一节说,可以先执行unpark后执行park,就是通过这个变量实现,看park方法的实现代码
park方法会调用Atomic::xchg方法,这个方法会原子性的将_counter赋值为0,并返回赋值前的值。如果调用park方法前,_counter大于0,则说明之前调用过unpark方法,所以park方法直接返回。接着往下看:
实际上Parker类用Posix的mutex,condition来实现的阻塞唤醒。如果对mutex和condition不熟,可以简单理解为mutex就是Java里的synchronized,condition就是Object里的wait/notify操作。
park方法里调用pthread_mutex_trylock方法,就相当于Java线程进入Java的同步代码块,然后再次判断_counter是否大于零,如果大于零则将_counter设置为零。最后调用pthread_mutex_unlock解锁,相当于Java执行完退出同步代码块。如果_counter不大于零,则继续往下执行pthread_cond_wait方法,实现当前线程的阻塞。
最后再看看unpark方法的实现吧,这块就简单多了,直接上代码:
图中的1和4就相当于Java的进入synchronized和退出synchronized的加锁解锁操作,代码2将_counter设置为1,同时判断先前_counter的值是否小于1,即这段代码:if(s<1)。如果不小于1,则就不会有线程被park,所以方法直接执行完毕,否则就会执行代码3,来唤醒被阻塞的线程。
通过阅读LockSupport的本地实现,我们不难发现这么个问题:多次调用unpark方法和调用一次unpark方法效果一样,因为都是直接将_counter赋值为1,而不是加1。简单说就是:线程A连续调用两次LockSupport.unpark(B)方法唤醒线程B,然后线程B调用两次LockSupport.park()方法, 线程B依旧会被阻塞。因为两次unpark调用效果跟一次调用一样,只能让线程B的第一次调用park方法不被阻塞,第二次调用依旧会阻塞。