首页 文章 精选 留言 我的

精选列表

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

伪共享和缓存行填充,Java并发编程还能这么优化!

前言 关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题。否则不仅无法发挥多线程的优势,还可能比单线程性能还差。随着JAVA版本的更新,再各个版本上减少伪共享的做法都有区别,一不小心代码可能就失效了,要注意进行测试。这篇文章总结一下。 什么是伪共享关于伪共享讲解最清楚的是这篇文章:http://developer.51cto.com/art/201306/398232.htm,我这里就直接摘抄其对伪共享的解释: 缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。 为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。 图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。 JAVA 6下的方案 解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小刚好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。这篇文章http://developer.51cto.com/art/201306/398232.htm提供了缓存行填充的例子: public final class FalseSharing implements Runnable { public final static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs = new VolatileLong[NUM_THREADS]; static { for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } } public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { final long start = System.nanoTime(); runTest(); System.out.println("duration = " + (System.nanoTime() - start)); } private static void runTest() throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } public final static class VolatileLong { public volatile long value = 0L; public long p1, p2, p3, p4, p5, p6; // comment out } } VolatileLong通过填充一些无用的字段p1,p2,p3,p4,p5,p6,再考虑到对象头也占用8bit, 刚好把对象占用的内存扩展到刚好占64bytes(或者64bytes的整数倍)。这样就避免了一个缓存行中加载多个对象。但这个方法现在只能适应JAVA6 及以前的版本了。 (注:如果我们的填充使对象size大于64bytes,比如多填充16bytes– public long p1, p2, p3, p4, p5, p6, p7, p8;。理论上同样应该避免伪共享问题,但事实是这样的话执行速度同样慢几倍,只比没有使用填充好一些而已。还没有理解其原因。所以测试下来,必须是64bytes的整数倍) JAVA 7下的方案上面这个例子在JAVA 7下已经不适用了。因为JAVA 7会优化掉无用的字段,可以参考:http://ifeve.com/false-shareing-java-7-cn/。 因此,JAVA 7下做缓存行填充更麻烦了,需要使用继承的办法来避免填充被优化掉,这篇文章http://ifeve.com/false-shareing-java-7-cn/里的例子我觉得不是很好,于是我自己做了一些优化,使其更通用: public final class FalseSharing implements Runnable { public static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { Thread.sleep(10000); System.out.println("starting...."); if (args.length == 1) { NUM_THREADS = Integer.parseInt(args[0]); } longs = new VolatileLong[NUM_THREADS]; for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } final long start = System.nanoTime(); runTest(); System.out.println("duration = " + (System.nanoTime() - start)); } private static void runTest() throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } } public class VolatileLongPadding { public volatile long p1, p2, p3, p4, p5, p6; // 注释 } public class VolatileLong extends VolatileLongPadding { public volatile long value = 0L; } 把padding放在基类里面,可以避免优化。(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。不过,这种办法怎么看都有点烦,借用另外一个博主的话:做个java程序员真难。 JAVA 8下的方案在JAVA 8中,缓存行填充终于被JAVA原生支持了。JAVA 8中添加了一个@Contended的注解,添加这个的注解,将会在自动进行缓存行填充。以上的例子可以改为: public final class FalseSharing implements Runnable { public static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { Thread.sleep(10000); System.out.println("starting...."); if (args.length == 1) { NUM_THREADS = Integer.parseInt(args[0]); } longs = new VolatileLong[NUM_THREADS]; for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } final long start = System.nanoTime(); runTest(); System.out.println("duration = " + (System.nanoTime() - start)); } private static void runTest() throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } } @Contended public class VolatileLong { public volatile long value = 0L; } 执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。很多文章把这个漏掉了,那样的话实际上就没有起作用。 @Contended注释还可以添加在字段上,今后再写文章详细介绍它的用法。 (后记:以上代码基于32位JDK测试,64位JDK下,对象头大小不同,有空再测试一下) 参考 http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html http://mechanical-sympathy.blogspot.hk/2011/08/false-sharing-java-7.html http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html 原文发布时间为:2018-07-12本文作者:Binhua本文来自云栖社区合作伙伴“Java架构沉思录”,了解相关信息可以关注“Java架构沉思录”。

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

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的对象必须是他们所处同步块的锁对象。

资源下载

更多资源
Mario

Mario

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

Nacos

Nacos

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

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册