首页 文章 精选 留言 我的

精选列表

搜索[并发编程],共10000篇文章
优秀的个人博客,低调大师

Java并发编程专题系列之从底层分析LockSupport原理机制

从底层分析LockSupport原理机制 知识点 LockSupport的介绍 LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数,而仅仅两个简单的接口,就为上层提供了强大的同步原语,先来解析下两个函数是做什么的。 public native void unpark(Thread jthread); public native void park(boolean isAbsolute, long time); park:阻塞当前线程(Block current thread),字面理解park,就算占住,停车的时候不就把这个车位给占住了么?起这个名字还是很形象的。 isAbsolute参数是指明时间是否属于绝对。 time参数是指时间值 线程调用park函数则等待"许可"。 unpark: 使给定的线程停止阻塞(Unblock the given thread blocked)。 thread参数是指对相应的线程进行解除阻塞。 线程调用unpark函数为线程提供"许可(permit)"。 这个有点像信号量,但是这个"许可"是不能叠加的,"许可"是一次性的。 比如,线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个"许可",如果线程A再次调用park,则进入等待状态。 注意,unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个"许可",那么当线程A调用park时,它发现已经有"许可"了,那么它会马上再继续运行。(此部分比wait/notify(notifyAll))要好很多。 park和unpark的灵活之处 unpark函数可以先于park调用,这个正是它们的灵活之处。 一个线程它有可能在别的线程unPark之前,或者之后,或者同时调用了park,那么因为park的特性,它可以不用担心自己的park的时序问题,否则,如果park必须要在unpark之前。 考虑一下,两个线程同步,要如何处理? 在Java5里是用wait/notify/notifyAll来同步的。wait/notify机制有个很蛋疼的地方是,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。 另外,是调用notify,还是notifyAll? notify只会唤醒一个线程,如果错误地有两个线程在同一个对象上wait等待,那么又悲剧了。为了安全起见,貌似只能调用notifyAll了。 park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。 拓展延伸 HotSpot里park/unpark的实现,每个java线程都有一个Parker实例,Parker类是这样定义的: class Parker : public os::PlatformParker { private: volatile int _counter ; ... public: void park(bool isAbsolute, jlong time); void unpark(); ... } class PlatformParker : public CHeapObj<mtInternal> { protected: pthread_mutex_t _mutex [1] ; pthread_cond_t _cond [1] ; ... } 可以看到Parker类实际上用Posix的mutex,condition来实现的。 在Parker类里的_counter字段,就是用来记录所谓的“许可”的。 当调用park时,先尝试直接能否直接拿到"许可",即_counter>0时,如果成功,则把_counter设置为0,并返回:(和信号量的思路很像!) void Parker::park(bool isAbsolute, jlong time) { // Ideally we'd do something useful while spinning, such // as calling unpackTime(). // Optional fast-path check: // Return immediately if a permit is available. // We depend on Atomic::xchg() having full barrier semantics // since we are doing a lock-free update to _counter. if (Atomic::xchg(0, &_counter) > 0) return; 如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回: ThreadBlockInVM tbivm(jt); if (_counter > 0) { // no wait needed _counter = 0; status = pthread_mutex_unlock(_mutex); 否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回: if (time == 0) { status = pthread_cond_wait (_cond, _mutex) ; } _counter = 0 ; status = pthread_mutex_unlock(_mutex) ; assert_status(status == 0, status, "invariant") ; OrderAccess::fence(); 当unpark时,则简单多了,直接设置_counter为1,再unlock mutext返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程: void Parker::unpark() { int s, status ; status = pthread_mutex_lock(_mutex); assert (status == 0, "invariant") ; s = _counter; _counter = 1; if (s < 1) { 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") ; } } 用mutex和condition保护了一个_counter的变量,当park时,这个变量置为了0,当unpark时,这个变量置为1。 值得注意的是在park函数里,调用pthread_cond_wait时,并没有用while来判断,所以posix condition里的"Spurious wakeup"一样会传递到上层Java的代码里。 if (time == 0) { status = pthread_cond_wait (_cond, _mutex) ; } 这也就是为什么Java dos里提到,当下面三种情况下park函数会返回: Some other thread invokes unpark with the current thread as the target; or Some other thread interrupts the current thread; or The call spuriously (that is, for no reason) returns. 相关的实现代码在: http://hg.openjdk.java.net/build-infra/jdk7/hotspot/file/52c4a1ae6adc/src/share/vm/runtime/park.hpp http://hg.openjdk.java.net/build-infra/jdk7/hotspot/file/52c4a1ae6adc/src/share/vm/runtime/park.cpp http://hg.openjdk.java.net/build-infra/jdk7/hotspot/file/52c4a1ae6adc/src/os/linux/vm/os_linux.hpp http://hg.openjdk.java.net/build-infra/jdk7/hotspot/file/52c4a1ae6adc/src/os/linux/vm/os_linux.cpp 其它的一些东东: Parker类在分配内存时,使用了一个技巧,重载了new函数来实现了cache line对齐。 // We use placement-new to force ParkEvent instances to be // aligned on 256-byte address boundaries. This ensures that the least // significant byte of a ParkEvent address is always 0. void * operator new (size_t sz) ; Parker里使用了一个无锁的队列在分配释放Parker实例: volatile int Parker::ListLock = 0 ; Parker * volatile Parker::FreeList = NULL ; Parker * Parker::Allocate (JavaThread * t) { guarantee (t != NULL, "invariant") ; Parker * p ; // Start by trying to recycle an existing but unassociated // Parker from the global free list. for (;;) { p = FreeList ; if (p == NULL) break ; // 1: Detach // Tantamount to p = Swap (&FreeList, NULL) if (Atomic::cmpxchg_ptr (NULL, &FreeList, p) != p) { continue ; } // We've detached the list. The list in-hand is now // local to this thread. This thread can operate on the // list without risk of interference from other threads. // 2: Extract -- pop the 1st element from the list. Parker * List = p->FreeNext ; if (List == NULL) break ; for (;;) { // 3: Try to reattach the residual list guarantee (List != NULL, "invariant") ; Parker * Arv = (Parker *) Atomic::cmpxchg_ptr (List, &FreeList, NULL) ; if (Arv == NULL) break ; // New nodes arrived. Try to detach the recent arrivals. if (Atomic::cmpxchg_ptr (NULL, &FreeList, Arv) != Arv) { continue ; } guarantee (Arv != NULL, "invariant") ; // 4: Merge Arv into List Parker * Tail = List ; while (Tail->FreeNext != NULL) Tail = Tail->FreeNext ; Tail->FreeNext = Arv ; } break ; } if (p != NULL) { guarantee (p->AssociatedWith == NULL, "invariant") ; } else { // Do this the hard way -- materialize a new Parker.. // In rare cases an allocating thread might detach // a long list -- installing null into FreeList --and // then stall. Another thread calling Allocate() would see // FreeList == null and then invoke the ctor. In this case we // end up with more Parkers in circulation than we need, but // the race is rare and the outcome is benign. // Ideally, the # of extant Parkers is equal to the // maximum # of threads that existed at any one time. // Because of the race mentioned above, segments of the // freelist can be transiently inaccessible. At worst // we may end up with the # of Parkers in circulation // slightly above the ideal. p = new Parker() ; } p->AssociatedWith = t ; // Associate p with t p->FreeNext = NULL ; return p ; } void Parker::Release (Parker * p) { if (p == NULL) return ; guarantee (p->AssociatedWith != NULL, "invariant") ; guarantee (p->FreeNext == NULL , "invariant") ; p->AssociatedWith = NULL ; for (;;) { // Push p onto FreeList Parker * List = FreeList ; p->FreeNext = List ; if (Atomic::cmpxchg_ptr (p, &FreeList, List) == List) break ; } }

优秀的个人博客,低调大师

Java并发编程专题系列之深入分析synchronized(基础篇)

synchronized同步关键字简介 synchronized是属于JVM层面的一个关键字,底层是通过一个monitor对象(管程对象)来完成,由于wait()/notify()等方法也依赖于monitor对象,所以只有在同步的块或者方法中才能调用wait/notify等方法 synchronized同步代码块底层实现 synchronized同步语句块的实现使用monitorenter和 monitorexit 指令。 monitorenter:指令指向同步代码块的开始位置 monitorexit:指令则指明同步代码块的结束位置 synchronized获取锁 当执行monitorenter指令时,当前线程将试图获取objectref(即对象锁) 所对应的 monitor的持有权,当 objectref的monitor 的进入计数器**为0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。 如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。 倘若其他线程已经拥有objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放monitor(锁)并设置计数器值为0,其他线程将有机会持有 monitor ;** 为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。 从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor的指令; monitorenter(进入管程对象) 每个对象有一个管程对象(monitor),当monitor被占用时就会处于锁定状态;线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 如果monitor的进入计数器为0,则该线程进入monitor,然后将进入计数器设置为1,该线程即为monitor的所有者; 如果线程已经之前占用了该monitor,本次只是重新进入,则将monitor的进入计数器加1; 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入计数器为0,再重新尝试获取monitor的所有权; monitorexit(退出管程对象) 执行monitorexit的线程必须是objectref(即对象锁)所对应的monitor的所有者; 指令执行时,monitor的进入计数器减1,如果减1后进入计数器为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权. 同步方法中synchronized底层实现 方法级的同步是隐式,即无需通过字节码指令(monitorenter和monitorexit)来控制的。 它实现在方法调用和返回操作之中。 JVM可以从方法常量池中的方法表结构(method_info Structure) 中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词)。 然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor。 在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

优秀的个人博客,低调大师

Java并发编程笔记之Unsafe类和LockSupport类源码分析

一.Unsafe类的源码分析 JDK的rt.jar包中的Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是native方法,通过使用JNI的方式来访问本地C++实现库。 rt.jar 中 Unsafe 类主要函数讲解, Unsafe 类提供了硬件级别的原子操作,可以安全的直接操作内存变量,其在 JUC 源码中被广泛的使用,了解其原理为研究 JUC 源码奠定了基础。 首先我们先了解Unsafe类中主要方法的使用,如下: 1.long objectFieldOffset(Field field) 方法:返回指定的变量在所属类的内存偏移地址,偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用unsafe获取AtomicLong中变量value在AtomicLong对象中的内存偏移,代码如下: static { try { valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } 2.int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址 3.int arrayIndexScale(Class arrayClass)方法:获取数组中单个元素占用的字节数 3.boolean compareAndSwapLong(Object obj,long offset,long expect,long update)方法:比较对象obj中偏移量offset的变量的值是不是和expect相等,相等则使用update值更新,然后返回true,否则返回false。 4.public native long getLongVolative(Object obj,long offset)方法:获取对象obj中偏移量offset的变量对应的volative内存语义的值。 5.void putOrderedLong(Object obj, long offset, long value) 方法:设置 obj 对象中 offset 偏移地址对应的 long 型 field 的值为 value。这是有延迟的 putLongVolatile 方法,并不保证值修改对其它线程立刻可见。变量只有使用 volatile 修饰并且期望被意外修改的时候使用才有用。 6.void park(boolean isAbsolute, long time) 方法:阻塞当前线程,其中参数 isAbsolute 等于 false 时候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞线程会被唤醒,这个 time 是个相对值,是个增量值,也就是相对当前时间累加 time 后当前线程就会被唤醒。 如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的时间点后会被唤醒,这里 time 是个绝对的时间,是某一个时间点换算为 ms 后的值。另外当其它线程调用了当前阻塞线程的 interrupt 方法中断了当前线程时候,当前线程也会返回,当其它线程调用了 unpark 方法并且把当前线程作为参数时候当前线程也会返回。 7.void unpark(Object thread)方法: 唤醒调用 park 后阻塞的线程,参数为需要唤醒的线程。 在JDK1.8中新增加了几个方法,这里简单的列出Long类型操作的方法如下: 8.long getAndSetLong(Object obj, long offset, long update) 方法: 获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量 volatile 语义的值为 update。使用方法如下代码: public final long getAndSetLong(Object obj, long offset, long update) { long l; do { l = getLongVolatile(obj, offset);//(1) } while (!compareAndSwapLong(obj, offset, l, update)); return l; } 从代码中可以内部代码(1)处使用了getLongVolative获取当前变量的值,然后使用CAS原子操作进行设置新值,这里使用while循环是考虑到多个线程同时调用的情况CAS失败后需要自旋重试。 9.long getAndAddLong(Object obj, long offset, long addValue) 方法 :获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量值为原始值 +addValue。使用方法如下代码: public final long getAndAddLong(Object obj, long offset, long addValue) { long l; do { l = getLongVolatile(obj, offset); } while (!compareAndSwapLong(obj, offset, l, l + addValue)); return l; } 类似于getAndSetLong的实现,只是这里使用CAS的时候使用了原始值+传递的增量参数addValue的值。 那么如何使用Unsafe类呢? 看到 Unsafe 这个类如此牛叉,是不是很想进行练习,好了,首先看如下代码所示: package com.hjc; import sun.misc.Unsafe; /** * Created by cong on 2018/6/6. */ public class TestUnSafe { //获取Unsafe的实例(2.2.1) static final Unsafe unsafe = Unsafe.getUnsafe(); //记录变量state在类TestUnSafe中的偏移值(2.2.2) static final long stateOffset; //变量(2.2.3) private volatile long state = 0; static { try { //获取state变量在类TestUnSafe中的偏移值(2.2.4) stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state")); } catch (Exception ex) { System.out.println(ex.getLocalizedMessage()); throw new Error(ex); } } public static void main(String[] args) { //创建实例,并且设置state值为1(2.2.5) TestUnSafe test = new TestUnSafe(); //(2.2.6) Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1); System.out.println(sucess); } } 代码(2.2.1)获取了Unsafe的一个实例,代码(2.2.3)创建了一个变量state初始化为0. 代码(2.2.4)使用unsafe.objectFieldOffset 获取 TestUnSafe类里面的state变量 在 TestUnSafe对象里面的内存偏移量地址并保存到stateOffset变量。 代码(2.2.6)调用创建的unsafe实例的compareAndSwapInt方法,设置test对象的state变量的值,具体意思是如果test对象内存偏移量为stateOffset的state的变量为0,则更新改值为1 上面代码我们希望输入true,然而执行后会输出如下结果: 为什么会这样呢?必然需要进入getUnsafe代码中如看看里面做了啥: private static final Unsafe theUnsafe = new Unsafe(); public static Unsafe getUnsafe(){ //(2.2.7) Class localClass = Reflection.getCallerClass(); //(2.2.8) if (!VM.isSystemDomainLoader(localClass.getClassLoader())) { throw new SecurityException("Unsafe"); } return theUnsafe; } //判断paramClassLoader是不是BootStrap类加载器(2.2.9) public static boolean isSystemDomainLoader(ClassLoader paramClassLoader){ return paramClassLoader == null; } 代码(2.2.7)获取调用getUnsafe这个方法的对象的Class对象,这里是TestUnSafe.calss。 代码(2.2.8)判断是不是Bootstrap类加载器加载的localClass,这里关键要看是不是Bootstrap加载器加载了TestUnSafe.class。看过Java虚拟机的类加载机制的人,很明显看出是由于TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛出了异常。 那么问题来了,为什么需要有这个判断呢? 我们知道Unsafe类是在rt.jar里面提供的,而rt.jar里面的类是使用Bootstrap类加载器加载的,而我们启动main函数所在的类是使用AppClassLoader加载的,所以在main函数里面加载Unsafe类时候鉴于双亲委派机制会委托给Bootstrap去加载Unsafe类。 如果没有代码(2.2.8)这个鉴权,那么我们应用程序就可以随意使用Unsafe做事情了,而Unsafe类可以直接操作内存,是很不安全的,所以JDK开发组特意做了这个限制,不让开发人员在正规渠道下使用Unsafe类,而是在rt.jar里面的核心类里面使用Unsafe功能。 问题来了,如果我们真的想要实例化Unsafe类,使用Unsafe的功能,那该怎么办呢? 我们不要忘记了反射这个黑科技,使用万能的反射来获取Unsafe的实例方法,代码如下: package com.hjc; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * Created by cong on 2018/6/6. */ public class TestUnSafe { static final Unsafe unsafe; static final long stateOffset; private volatile long state = 0; static { try { // 反射获取 Unsafe 的成员变量 theUnsafe(2.2.10) Field field = Unsafe.class.getDeclaredField("theUnsafe"); // 设置为可存取(2.2.11) field.setAccessible(true); // 获取该变量的值(2.2.12) unsafe = (Unsafe) field.get(null); //获取 state 在 TestUnSafe 中的偏移量 (2.2.13) stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state")); } catch (Exception ex) { System.out.println(ex.getLocalizedMessage()); throw new Error(ex); } } public static void main(String[] args) { TestUnSafe test = new TestUnSafe(); Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1); System.out.println(sucess); } } 如果上面的代码(2.2.10 2.2.11 2.2.12)反射获取unsafe的实例,运行结果如下: 二.LockSupport类源码探究 JDK中的rt.jar里面的LockSupport是一个工具类,主要作用是挂起和唤醒线程,它是创建锁和其他同步类的基础。 LockSupport类与每个使用他的线程都会关联一个许可证,默认调用LockSupport 类的方法的线程是不持有许可证的,LockSupport内部使用Unsafe类实现。 这里要注意LockSupport的几个重要的函数,如下: 1.void park() 方法: 如果调用 park() 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,否者调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。例子如下代码: package com.hjc; import java.util.concurrent.locks.LockSupport; /** * Created by cong on 2018/6/6. */ public class LockSupportTest { public static void main( String[] args ) { System.out.println( "park start!" ); LockSupport.park(); System.out.println( "park stop!" ); } } 如上面代码所示,直接在main函数里面调用park方法,最终结果只会输出park start! 然后当前线程会被挂起,这是因为默认下调用线程是不持有许可证的。运行结果如下: 在看到其他线程调用unpark(Thread thread)方法并且当前线程作为参数时候,调用park方法被阻塞的线程会返回,另外其他线程调用了阻塞线程的interrupt()方法,设置了中断标志时候或者由于线程的虚假唤醒原因后阻塞线程也会返回,所以调用 park() 最好也是用循环条件判断方式。 需要注意的是调用park()方法被阻塞的线程被其他线程中断后阻塞线程返回时候并不会抛出InterruptedException 异常。 2.void unpark(Thread thread) 方法 当一个线程调用了 unpark 时候,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有。如果 thread 之前调用了 park() 被挂起,则调用 unpark 后,该线程会被唤醒。 如果 thread 之前没有调用 park,则调用 unPark 方法后,在调用 park() 方法,会立刻返回,上面代码修改如下: package com.hjc; import java.util.concurrent.locks.LockSupport; /** * Created by cong on 2018/6/6. */ public class LockSupportTest { public static void main( String[] args ) { System.out.println( "park start!" ); //使当前线程获取到许可证 LockSupport.unpark(Thread.currentThread()); //再次调用park LockSupport.park(); System.out.println( "park stop!" ); } } 运行结果如下: 接下来我们在看一个例子来加深对 park,unpark 的理解,代码如下: import java.util.concurrent.locks.LockSupport; /** * Created by cong on 2018/6/6. */ public class LockSupportTest { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("子线程 park start!"); // 调用park方法,挂起自己 LockSupport.park(); System.out.println("子线程 unpark!"); } }); //启动子线程 thread.start(); //主线程休眠1S Thread.sleep(1000); System.out.println("主线程 unpark start!"); //调用unpark让thread线程持有许可证,然后park方法会返回 LockSupport.unpark(thread); } } 运行结果如下: 上面的代码首先创建了一个子线程thread,启动后子线程调用park方法,由于默认子线程没有持有许可证,会把自己挂起 主线程休眠1s 目的是主线程在调用unpark方法让子线程输出 子线程park start! 并阻塞。 主线程然后执行unpark方法,参数为子线程,目的是让子线程持有许可证,然后子线程调用的park方法就返回了。 park方法返回时候不会告诉你是因为何种原因返回,所以调用者需要根据之前是处于什么目前调用的park方法,再次检查条件是否满足,如果不满足的话,还需要再次调用park方法。 例如,线程在返回时的中断状态,根据调用前后中断状态对比就可以判断是不是因为被中断才返回的。 为了说明调用 park 方法后的线程被中断后会返回,修改上面例子代码,删除 LockSupport.unpark(thread); 然后添加 thread.interrupt(); 代码如下: import java.util.concurrent.locks.LockSupport; /** * Created by cong on 2018/6/6. */ public class LockSupportTest { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("子线程 park start!"); // 调用park方法,挂起自己,只有中断才会退出循环 while (!Thread.currentThread().isInterrupted()) { LockSupport.park(); } System.out.println("子线程 unpark!"); } }); //启动子线程 thread.start(); //主线程休眠1S Thread.sleep(1000); System.out.println("主线程 unpark start!"); //中断子线程 thread.interrupt(); } } 运行结果如下: 正如上面代码,也就是只有当子线程被中断后子线程才会运行结束,如果子线程不被中断,即使你调用unPark(Thread) 子线程也不会结束。 3.void parkNanos(long nanos)方法:和 park 类似,如果调用 park 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,不同在于如果没有拿到许可调用线程会被挂起 nanos 时间后在返回。 park 还支持三个带有blocker参数的方法,当线程因为没有持有许可证的情况下调用park 被阻塞挂起的时候,这个blocker对象会被记录到该线程内部。 使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取该blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker设置为this,这样当内存dump排查问题时候就能知道是哪个类被阻塞了。 例子如下: import java.util.concurrent.locks.LockSupport; /** * Created by cong on 2018/6/6. */ public class TestPark { public void testPark(){ LockSupport.park();//(1) } public static void main(String[] args) { TestPark testPark = new TestPark(); testPark.testPark(); } } 运行结果如下: 可以看到运行在阻塞,那么我们要使用JDK/bin目录下的工具看一下了,如果不知道的读者,建议去先看一下JVM的监控工具。 运行后使用jstack pid 查看线程堆栈的时候,可以看到的结果如下: 然后我们进行上面的代码(1)进行修改如下: LockSupport.park(this);//(1) 再次运行,再用jstack pid 查看的结果如下: 可以知道,带blocker的park方法后,线程堆栈可以提供更多有关阻塞对象的信息。 那么我们接下来进行park(Object blocker) 函数的源代码查看,源码如下: public static void park(Object blocker) { //获取调用线程 Thread t = Thread.currentThread(); //设置该线程的 blocker 变量 setBlocker(t, blocker); //挂起线程 UNSAFE.park(false, 0L); //线程被激活后清除 blocker 变量,因为一般都是线程阻塞时候才分析原因 setBlocker(t, null); } Thread类里面有个变量volatile Object parkBlocker 用来存放park传递的blocker对象,也就是把blocker变量存放到了调用park方法的线程的成员变量里面 4.void parkNanos(Object blocker, long nanos) 函数 相比 park(Object blocker) 多了个超时时间。 5.void parkUntil(Object blocker, long deadline) parkUntil源代码如下: public static void parkUntil(Object blocker, long deadline) { Thread t = Thread.currentThread(); setBlocker(t, blocker); //isAbsolute=true,time=deadline;表示到 deadline 时间时候后返回 UNSAFE.park(true, deadline); setBlocker(t, null); } 可以看到是一个设置deadline,时间单位为milliseconds,是从1970到现在某一个时间点换算为毫秒后的值,这个和parkNanos(Object blocker,long nanos)区别是后者是从当前算等待nanos时间的,而前者是指定一个时间点, 比如我们需要等待到2018.06.06 日 20:34,则把这个时间点转换为从1970年到这个时间点的总毫秒数。 我们再来看一个例子,代码如下: import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.LockSupport; /** * Created by cong on 2018/6/6. */ public class FIFOMutex { private final AtomicBoolean locked = new AtomicBoolean(false); private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>(); public void lock() { boolean wasInterrupted = false; Thread current = Thread.currentThread(); waiters.add(current); // 只有队首的线程可以获取锁(1) while (waiters.peek() != current || !locked.compareAndSet(false, true)) { LockSupport.park(this); if (Thread.interrupted()) // (2) wasInterrupted = true; } waiters.remove(); if (wasInterrupted) // (3) current.interrupt(); } public void unlock() { locked.set(false); LockSupport.unpark(waiters.peek()); } } 可以看到这是一个先进先出的锁,也就是只有队列首元素可以获取所,代码(1)如果当前线程不是队首或者当前锁已经被其他线程获取,则调用park方法挂起自己。 接着代码(2)做判断,如果park方法是因为被中断而返回,则忽略中断,并且重置中断标志,只做个标记,然后再次判断当前线程是不是队首元素或者当先锁是否已经被其他线程获取,如果是则继续调用park方法挂起自己。 然后代码(3)中如果标记为true 则中断该线程,这个怎么理解呢?其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他线程对该标志不感兴趣,所以要恢复下。

优秀的个人博客,低调大师

java程序员面试——Java并发编程知识点总结 ...

线程的状态 初始态:NEW 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。 运行态:RUNNABLE 在Java中,运行态包括就绪态 和 运行态。 就绪态 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。 所有就绪态的线程存放在就绪队列中。 运行态 获得CPU执行权,正在执行的线程。 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。 阻塞态 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。 而在Java中,阻塞态专指请求锁失败时进入的状态。 由一个阻塞队列存放所有阻塞态的线程。 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。 PS:锁、IO、Socket等都资源。 等待态 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。 也有一个等待队列存放所有等待态的线程。 线程处于等待态表示它需要等待其他线程的指示才能继续运行。 进入等待态的线程会释放CPU执行权,并释放资源(如:锁) 超时等待态 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态; 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒; 进入该状态后释放CPU执行权 和 占有的资源。 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。 终止态 线程执行结束后的状态。 线程状态转换图 初始态——>就绪态 当线程对象调用start()方法时就会进入就绪态,若就绪队列没有线程,则直接进入运行态。 就绪态——>运行态 由系统调用完成。 就绪态<——运行态 调用Thread.yield()函数 由系统调用完成(当线程时间片用完) 运行态——>阻塞态 当线程请求锁失败时进入阻塞态。 阻塞态——>就绪态 阻塞队列中的线程会不断检查锁是否可用,一旦可用就进入就绪队列。 运行态——>等待态 调用Object.wait()方法 wait方法必须在同步块内部; 必须由同步块的锁对象调用; 必须由notify方法和wait方法必须由同一个锁对象调用 调用Thread.join()方法 调用LockSupport.park()方法 等待态——>就绪态 某一个线程调用了 锁对象.notify()方法,并且等待的线程并不需要锁 等待态——>阻塞态 锁对象.notify()方法,并且等待的线程需要锁同步。 注意点 wait()方法会释放CPU执行权 和 占有的锁。 sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列,与yield相比,它会使线程较长时间得不到运行。 yield()方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。 wait和notify必须配套使用,即必须使用同一把锁调用; wait和notify必须放在一个同步块中 调用wait和notify的对象必须是他们所处同步块的锁对象。

优秀的个人博客,低调大师

Java并发编程专题系列之深入分析synchronized(进阶篇)

技术回顾 在此让我们回顾一下Java对象的内存结构,为什么要回顾内存结构?因为synchronized同步锁,采用的底层所机制的monitor就和内存对象有关系,所以我们先回顾一下Java内存结构。 Java对象内存结构 HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头(Object Header) markWord(标记字段) 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits。 new一个空对象在32为系统中占用内存大小是8byte(对象头,在堆中)+4byte(对象的引用地址,在栈中)=12byte; new一个空对象在64为系统中占用内存大小是16byte(对象头,在堆中)+8byte(对象的引用地址,在栈中)=24byte;如果开启了压缩指针机制 那么就是 8byte(对象头)+4byte的链接指针+4byte(栈中的引用地址)。 Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。 无锁状 加锁状态 其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。 这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。 每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。 在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的) ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; \_WaitSet = NULL; //处于wait状态的线程,会被加入到\_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } ObjectMonitor中有两个队列,WaitSet 和 EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时 首先会进入 EntryList 集合,当线程获取到对象的monitor后进入Owner区域,并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1。 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。 由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册