本笔记来源于 :尚硅谷JUC并发编程(对标阿里P6-P7)b站视频
文章来自: https://www.yuque.com/gongxi-wssld/csm31d/ln0mq1w7wp1oy99g https://www.yuque.com/liuyanntes/vx9leh/fpy93i https://blog.csdn.net/dolpin_ink/category_11847910.html
脑图 本地:尚硅谷JUC并发编程
在线:尚硅谷JUC并发编程
在线脑图加载时间超长。
1、原子类 何为原子类
即为java.util.concurrent.atomic包下的所有相关类和API
2、没有CAS之前
多线程环境不使用 原子类保证线程安全i++(基本数据类型)
常用synchronized
锁,但是它比较重 ,牵扯到了用户态和内核态的切换,效率不高。
1 2 3 4 5 6 7 8 9 10 11 public class T3 { volatile int number = 0 ; public int getNumber () { return number; } public synchronized void setNumber () { number++; } }
3、使用CAS之后
多线程情况下使用原子类 保证线程安全(基本数据类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class T3 { volatile int number = 0 ; public int getNumber () { return number; } public synchronized void setNumber () { number++; } AtomicInteger atomicInteger = new AtomicInteger(); public int getAtomicInteger () { return atomicInteger.get(); } public void setAtomicInteger () { atomicInteger.getAndIncrement(); } }
4、CAS是什么 CAS基本知识 compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置
、预期原值
及更新值
。
执行CAS操作的时候,将内存位置的值与预期原值比较: 如果相匹配 ,那么处理器会自动将该位置值更新 为新值, 如果不匹配 ,处理器不做任何操作,多个线程同时执行CAS操作只有一个 会成功。
CAS原理 CAS (CompareAndSwap) CAS有3个操作数,位置内存值V
,旧的预期值A
,要修改的更新值B
。 当且仅当旧的预期值A
和内存值V
相同 时,将内存值V
修改 为B
,否则什么都不做或重来
当它重来重试的这种行为成为—自旋!
eg 线程A读取了值为5,想要更新为6,想要将值写回的时候发现线程B和C都进行了操作,已经变成了7,这个时候A不能成功,可能会发生自旋
CASDemo代码 多线程情况下使用原子类 保证线程安全(基本数据类型)
1 2 3 4 5 6 7 8 9 10 public class CASDemo { public static void main (String[] args) throws InterruptedException{ AtomicInteger atomicInteger = new AtomicInteger (5 ); System.out.println(atomicInteger.compareAndSet(5 , 2020 )+"\t" +atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(5 , 1024 )+"\t" +atomicInteger.get()); } }
硬件级别保证 对总线加锁,效率比synchronized效率高。
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了 比较-更新的原子性。
它是非阻塞的且自身原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令 (cmpxchg指令
),不会造成所谓的数据不一致问题,Unsafe
提供的CAS方法
(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁 ,只有一个 线程会对总线加锁成功 ,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的 , 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好
源码分析 1 2 3 4 5 6 7 8 9 public final boolean compareAndSet (int expect, int update) { return unsafe.compareAndSwapInt(this , valueOffset, expect, update); }public final native boolean compareAndSwapInt (Object var1, long var2, int var4, int var5) ;
1 2 3 4 5 6 public final native boolean compareAndSwapObject (Object var1, long var2, Object var4, Object var5) ;public final native boolean compareAndSwapInt (Object var1, long var2, int var4, int var5) ;public final native boolean compareAndSwapLong (Object var1, long var2, long var4, long var6) ;
上面三个方法都是类似的,主要对4个参数做一下说明。 var1:表示要操作的对象 var2:表示要操作对象中属性地址的偏移量 var4:表示需要修改数据的期望的值 var5/var6:表示需要修改为的新值
引出来一个问题:Unsafe 类是什么?
5、CAS底层原理?如果知道,谈谈你对UnSafe的理解 UnSafe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class AtomicInteger extends Number implements java .io.Serializable { private static final long serialVersionUID = 6214790243416807050L ; private static final Unsafe unsafe = Unsafe.getUnsafe(); static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value" )); } catch (Exception ex) { throw new Error (ex); } } private volatile int value; }
1 Unsafe
CAS这个理念 ,落地就是Unsafe
类
它是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门 ,基于该类可以 直接操作特定内存\ 的数据 。Unsafe类存在于sun.misc
包中,其内部方法操作可以像C的指针 一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native 修饰 的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务 。
打开rt.jar包(最基本的包)
2 变量valueOffset
,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
1 2 3 public final int getAndIncrement () { return unsafe.getAndAddInt(this , valueOffset, 1 ); }
3 变量value用volatile修饰
我们知道i++线程不安全的,那atomicInteger.getAndIncrement() CAS的全称为Compare-And-Swap,它是一条CPU并发原语。 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。 AtomicInteger 类主要利用 CAS (compare and swap)
+ volatile
和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令 。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种 系统原语 ,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
源码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 new AtomicInteger ().getAndIncrement();public final int getAndIncrement () { return unsafe.getAndAddInt(this , valueOffset, 1 ); }public final int getAndAddInt (Object var1, long var2, int var4) { int var5; do { var5 = this .getIntVolatile(var1, var2); } while (!this .compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }public final native boolean compareAndSwapInt (Object var1, long var2, int var4, int var5) ;
若在OpenJDK源码中查看Unsafe.java
这里while体现了自旋的思想
假如是ture,取反false退出循环;假如是false,取反true要继续循环。
原理
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):
1 AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。 2 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起 。 3 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。 4 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。 5 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
底层汇编
(非计算机专业的,不要求懂,可以不听,需要汇编知识) 了解即可
Unsafe
类中的compareAndSwapInt
,是一个本地方法,该方法的实现位于unsafe.cpp
中
核心(Atomic::cmpxchg(x, addr, e)) == e;
1 2 3 4 5 6 7 8 9 10 11 12 UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv* env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt" ); oop p = JNIHandles::resolve(obj); jint* addr = (jint* ) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
1 2 return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
1 2 3 4 5 unsigned Atomic::cmpxchg(unsigned int exchange_value,volatile unsigned int * dest, unsigned int compare_value) { assert (sizeof(unsigned int ) == sizeof(jint), "more work to do" ); return (unsigned int )Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value); }
不同的操作系统下会调用不同的compxchg重载函数,例如win10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
总结 你只需要记住:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性 实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg
指令。 核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。
6、自定义原子引用 譬如AtomicInteger原子整型,可否有其他原子类型?比如AtomicBook、AtomicOrder 可以! 丢入泛型中Class AtomicReference<V>
eg1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import lombok.AllArgsConstructor;import lombok.Getter;import lombok.ToString;import java.util.concurrent.atomic.AtomicReference;@Getter @ToString @AllArgsConstructor class User { String userName; int age; }public class AtomicReferenceDemo { public static void main (String[] args) { User z3 = new User ("z3" ,24 ); User li4 = new User ("li4" ,26 ); AtomicReference<User> atomicReferenceUser = new AtomicReference <>(); atomicReferenceUser.set(z3); System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t" +atomicReferenceUser.get().toString()); System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t" +atomicReferenceUser.get().toString()); } }
7、CAS与自旋锁,借鉴CAS思想 CAS落地的重要应用-自旋锁
是什么 自旋锁(spinlock):是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式 去尝试获取锁 ,
当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU 。
若在OpenJDK源码中查看Unsafe.java
这里while体现了自旋的思想
假如是ture,取反false退出循环;假如是false,取反true要继续循环。
自己实现一个自旋锁SpinLockDemo
1 2 3 4 5 题目:实现一个自旋锁 自旋锁好处:循环比较获取没有类似wait的阻塞。 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5 秒钟,B随后进来后发现 当前有线程持有锁,不是null ,所以只能通过自旋等待,直到A释放锁后B随后抢到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class SpinLockDemo { AtomicReference<Thread> atomicReference = new AtomicReference <>(); public void Lock () { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"\t" +"-----come in" ); while (!atomicReference.compareAndSet(null ,thread)) { } } public void UnLock () { Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread,null ); System.out.println(Thread.currentThread().getName()+"\t" +"-------task over,unLock....." ); } public static void main (String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo (); new Thread (() -> { spinLockDemo.Lock(); try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.UnLock(); },"A" ).start(); try { TimeUnit.MILLISECONDS.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread (() -> { spinLockDemo.Lock(); spinLockDemo.UnLock(); },"B" ).start(); } }
8、CAS缺点 1 循环时间长开销很大 do while
如果它一直自旋会一直占用CPU时间,造成较大的开销
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2 引出来ABA问题 CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B, 然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
AtomicStampedReference
版本号 (注意区分前面的Class AtomicReference<V>
)
Class AtomicStampedReference<V>
相关API
1 2 AtomicStampedReference(V initialRef, int initialStamp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean weakCompareAndSet (V expectedReference,//旧值 V newReference,//新值 int expectedStamp,//旧版本号 int newStamp) 以原子方式设置该引用和邮票给定的更新值的值,如果当前的参考是==至预期的参考,并且当前标志等于预期标志。 May fail spuriously and does not provide ordering guarantees ,所以只是很少适合替代compareAndSet 。 参数 expectedReference - 参考的预期值 newReference - 参考的新值 expectedStamp - 邮票的预期值 newStamp - 邮票的新值 结果true 如果成功
基本情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @NoArgsConstructor @AllArgsConstructor @Data class Book { private int id; private String bookName; }public class AtomicStampedDemo { public static void main (String[] args) { Book javaBook = new Book (1 , "javaBook" ); AtomicStampedReference<Book> stampedReference = new AtomicStampedReference <>(javaBook,1 ); System.out.println(stampedReference.getReference()+"\t" +stampedReference.getReference()); Book mysqlBook = new Book (2 , "mysqlBook" ); boolean b; b= stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1 ); System.out.println(b+"\t" +stampedReference.getReference()+"\t" +stampedReference.getStamp()); } }
ABA复现(单线程情况下)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class AtomicStampedDemo { public static void main (String[] args) { Book javaBook = new Book (1 , "javaBook" ); AtomicStampedReference<Book> stampedReference = new AtomicStampedReference <>(javaBook,1 ); System.out.println(stampedReference.getReference()+"\t" +stampedReference.getReference()); Book mysqlBook = new Book (2 , "mysqlBook" ); boolean b; b= stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1 ); System.out.println(b+"\t" +stampedReference.getReference()+"\t" +stampedReference.getStamp()); b= stampedReference.compareAndSet(mysqlBook,javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1 ); System.out.println(b+"\t" +stampedReference.getReference()+"\t" +stampedReference.getStamp()); } }
ABA复现(多线程情况下)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class ABADemo { static AtomicInteger atomicInteger = new AtomicInteger (100 ); static AtomicStampedReference atomicStampedReference = new AtomicStampedReference (100 ,1 ); public static void main (String[] args) { new Thread (() -> { atomicInteger.compareAndSet(100 ,101 ); atomicInteger.compareAndSet(101 ,100 ); },"t1" ).start(); new Thread (() -> { try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); }; System.out.println(atomicInteger.compareAndSet(100 , 2022 )+"\t" +atomicInteger.get()); },"t2" ).start(); try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("============以下是ABA问题的解决=============================" ); new Thread (() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 首次版本号:" +stamp); try { TimeUnit.MILLISECONDS.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100 ,101 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 2次版本号:" +atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101 ,100 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 3次版本号:" +atomicStampedReference.getStamp()); },"t3" ).start(); new Thread (() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 首次版本号:" +stamp); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = atomicStampedReference.compareAndSet(100 ,2019 ,stamp,stamp+1 ); System.out.println(Thread.currentThread().getName()+"\t" +result+"\t" +atomicStampedReference.getReference()); },"t4" ).start(); } }
总结:版本号 +比较 要一起上