原文来自于:公众号三不猴子
什么是cas?
cas:compare and swap 比拟而后替换,它在没有锁的状态下能够保障多线程的对值得更新。咱们能够看一下在jdk中对cas的利用:
/** * Atomically increments by one the current value. * * @return the updated value */public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 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;}
在Atomic原子类中的自增操作中就应用到了compareAndSwapInt,这里的cas的实现应用的native办法。用一张流程图来了解什么是cas。
咱们先会存一下要批改的值,再批改之后再去看一下要批改的值是不是还是咱们存的值如果是统一的则批改,咱们在更新数据罕用的乐观锁就是用的cas的机制。
在这外面有个ABA的问题:所谓ABA就是在线程A存了值之后,有个线程B对这个值进行批改,B批改了屡次最初后果还是原来那个值,这就是ABA问题,此时须要依据业务场景判断这个值得批改是否须要感知。如果须要感知就能够给这个值再加上一个版本号。
咱们用一段代码演示一下cas中ABA的问题吧
import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/** * create by yanghongxing on 2020/5/8 11:03 下午 */public class ABA { private static AtomicInteger atomicInt = new AtomicInteger(100); public static void main(String[] args) throws InterruptedException { // 对一个AtomicInteger的值该两次,最初后果与之前雷同 Thread intT1 = new Thread(() -> { atomicInt.compareAndSet(100, 101); atomicInt.compareAndSet(101, 100); }); Thread intT2 = new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } boolean c3 = atomicInt.compareAndSet(100, 101); // true,执行胜利 System.out.println(c3); }); intT1.start(); intT2.start(); }}
应用jdk中的AtomicStampedReference能够解决这个问题。最初咱们看一下cas实现原理,看一下最初native办法的源码 jdk8u: atomic\_linux\_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value) : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) : "cc", "memory"); return exchange_value;
汇编指令 咱们看这一条
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
\_\_asm\_\_示意汇编指令,lock示意锁,if如果 mp(%4)示意cpu是多核, cmpxchgl示意 cmp exchange 全称 compare and exchange。最终实现:
lock cmpxchg 指令
这条汇编指令(硬件指令)示意如果是多核CPU则加上锁。
Java对象在内存的布局
咱们先理解一下Java对象在内存中的(具体)布局,这个布局与Java锁的实现非亲非故。应用工具:JOL = Java Object Layout
<dependencies> <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency></dependencies>
应用示例
public class ShowJOL { public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); }}
输入
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total
OFFSET:从第几个地位开始
size:大小,单位字节,TYPE DESCRIPTION:类型形容,下面的示例就是object header对象头,
VALUE:值
loss due to the next object alignment: 因为下一个对象对齐而造成的损失,咱们看上面这张图。
markword:对于锁的信息。
class pointer: 示意对象是属于哪个类的。
instance data:字面了解实例数据,比方在在对象中创立了一个int a 就占4个字节,long b就占8个字节。
padding data:如果下面的数据所占用的空间不能被8整除,padding则占用空间凑齐使之能被8整除。被8整除在读取数据的时候会比拟快。
对着这张图咱们再看看下面JOL打印进去的数据,第一个和第二个都是markword各 4个字节,第三个是class pointer4个字节,原本还有 instance data 用来存成员变量的然而咱们写的没有所以为0,这些总共加起来12个字节不能被8整除,所以咱们要对齐加4个字节。(注这里的内存占用是默认开启字节压缩XX:+UseCompressedClassPointers -XX:+UseCompressedOops)
看完了这些货色咱们再来执行一下上面的代码
/** * create by yanghongxing on 2020/5/11 11:52 下午 */public class ShowJOL { public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); }}
执行后果:
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total
比照这个的输入和第一次咱们打印的输入,咱们能够得出结论synchronized锁的信息是记录在markword上。
咱们做Java开发的常常听到的一句话就是synchronized是个重量级的锁,事实上肯定是这样吗?咱们能够通过剖析markword看看synchronized加锁过程,在晚期jdk1.0的时候jdk每次申请的就是重量级的锁,性能比拟差,随着前面jdk的降级synchronized的性能有所晋升,synchronized并不是一开始就加重量级的锁,而是有个缓缓降级的过程。先来看表格
偏差锁Biased Locking:Java6引入的一项多线程优化,偏差锁,顾名思义,它会偏差于第一个拜访锁的线程,如果在运行过程中,同步锁只有一个线程拜访,不存在多线程争用的状况,则线程是不须要触发同步的,这种状况下,就会给线程加一个偏差锁。如果在运行过程中,遇到了其余线程抢占锁,则持有偏差锁的线程会被挂起,JVM会打消它身上的偏差锁,将锁复原到规范的轻量级锁。
自旋锁:自旋锁的目标是为了占着CPU的资源不开释,等到获取到锁立刻进行解决。始终在自旋也是占用CPU的,如果自旋的线程十分多,自旋次数也十分大CPU可能会跑满,所以须要降级。
重量级锁:内核态的锁,资源开销较大。外部会将期待中的线程进行wait解决,避免耗费CPU。
联合这张表格咱们再写一个示例看看synchronized在没有锁竞争的状况下默认是怎么样的。
/** * create by yanghongxing on 2020/5/11 11:52 下午 */public class ShowJOL { public static void main(String[] args) { Object o = new Object(); System.out.println(Integer.toHexString(o.hashCode())); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.out.println(Integer.toHexString(o.hashCode())); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }}
而后看输入:
5f8ed237java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 37 d2 8e (00000001 00110111 11010010 10001110) (-1898825983) 4 4 (object header) 5f 00 00 00 (01011111 00000000 00000000 00000000) (95) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totaljava.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 90 29 7d 06 (10010000 00101001 01111101 00000110) (108865936) 4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalDisconnected from the target VM, address: '127.0.0.1:62501', transport: 'socket'Process finished with exit code 0
咱们在第一行打印了这个Object的hashcode的16进制编码,比照没有加锁的输入这hashcode是存在对象的markword中的。咱们再看这个未加锁的markword的二级制值:00000001 00110111 11010010 10001110,看前8位的倒数3位也就001(书面语形容不晓得是不是精确)比照下面的表格也就是无锁状态,咱们再看第二个markword的值000,对应表格就是轻量锁、自旋锁。咱们再应用一个存在锁竞争的例子看看是怎么样的。
/** * create by yanghongxing on 2020/5/12 7:13 下午 */public class MarkwordMain { private static Object OBJ = new Object(); private static void printf() { System.out.println(ClassLayout.parseInstance(OBJ).toPrintable()); } private static Runnable RUNNABLE = () -> { synchronized (OBJ) { printf(); } }; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 3; i++) { new Thread(RUNNABLE).start(); } Thread.sleep(Integer.MAX_VALUE); }}
这段代码中咱们应用了三个线程去竞争打印这个内存散布的操作,看看输入后果,
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 5a 59 82 ef (01011010 01011001 10000010 11101111) (-276670118) 4 4 (object header) f9 7f 00 00 (11111001 01111111 00000000 00000000) (32761) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totaljava.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 5a 59 82 ef (01011010 01011001 10000010 11101111) (-276670118) 4 4 (object header) f9 7f 00 00 (11111001 01111111 00000000 00000000) (32761) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totaljava.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 5a 59 82 ef (01011010 01011001 10000010 11101111) (-276670118) 4 4 (object header) f9 7f 00 00 (11111001 01111111 00000000 00000000) (32761) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total
咱们看到是010,对应表格就是重量级锁。
synchronized 锁降级时依照,new - 偏差锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁的过程降级的。偏差锁 - markword 上记录以后线程指针,下次同一个线程加锁的时候,不须要争用,只须要判断线程指针是否同一个,所以,偏差锁,偏差加锁的第一个线程 。
有争用 - 锁降级为轻量级锁 - 每个线程有本人的LockRecord在本人的线程栈上,用CAS去争用markword的LockRecord的指针,指针指向哪个线程的LockRecord,哪个线程就领有锁
自旋超过10次,降级为重量级锁 - 如果太多线程自旋 CPU耗费过大,不如降级为重量级锁,进入期待队列(不耗费CPU)-XX:PreBlockSpin
自旋锁在 JDK1.4.2 中引入,应用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的工夫(次数)不再固定,而是由前一次在同一个锁上的自旋工夫及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋期待刚刚胜利取得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次胜利,进而它将容许自旋期待继续绝对更长的工夫。如果对于某个锁,自旋很少胜利取得过,那在当前尝试获取这个锁时将可能省略掉自旋过程,间接阻塞线程,避免浪费处理器资源。
synchronized实现原理
Java源代码级别
synchronized(对象)
字节码层级
应用idea插件jclasslib插件查看字节码,咱们以之前代码为例
public class ShowJOL { public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); }}
public class ShowJOL { public static void main(String[] args) { Object o = new Object(); synchronized (o) { System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }}
在字节码层面是以monitorenter作为开始锁的开始,以moniterexit作为完结。
汇编级别
咱们应用hsdis工具对Java源码进行反编译为汇编代码
/** * create by yanghongxing on 2020/5/12 11:45 下午 */public class SynchronizedTest { private static int c; public static synchronized void sync() { } public static void noSynchronized() { int a = 1; int b = 2; c = a + b; } public static void main(String[] args) { for (int j = 0; j < 1000_000; j++) { sync(); noSynchronized(); } }}`````` 0x00000001195d2e4e: lock cmpxchg %r11,(%r10) 0x00000001195d2e53: je 0x00000001195d2da0 0x00000001195d2e59: mov %r13,(%rsp) 0x00000001195d2e5d: movabs $0x79578d830,%rsi ; {oop(a 'java/lang/Class' = 'com/example/demo/SynchronizedTest')} 0x00000001195d2e67: lea 0x10(%rsp),%rdx 0x00000001195d2e6c: data32 xchg %ax,%ax 0x00000001195d2e6f: callq 0x0000000119525860 ; OopMap{off=404} ;*synchronization entry ; - com.example.demo.SynchronizedTest::sync@-1 (line 11)
咱们看到了开篇提到的lock cmpxchg这条汇编命令,论断是synchronized底层也是应用cas的形式来实现锁。
锁打消 lock eliminate
public void add(String str1,String str2){ StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2);}
咱们都晓得 StringBuffer 是线程平安的,因为它的要害办法都是被 synchronized 润饰过的,但咱们看下面这段代码,咱们会发现,sb 这个援用只会在 add 办法中应用,不可能被其它线程援用(因为是局部变量,栈公有),因而 sb 是不可能共享的资源,JVM 会主动打消 StringBuffer 对象外部的锁。
锁粗化 lock coarsening
public String test(String str){ int i = 0; StringBuffer sb = new StringBuffer(): while(i < 100){ sb.append(str); i++; } return sb.toString():}
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范畴粗化到这一连串的操作的内部(比方 while 空幻体外),使得这一连串操作只须要加一次锁即可。
volatile实现利用和原理
首先理解一下volatile的作用:
- 禁止指令重拍
保障内存的可见性
先看个看个示例
public class VolatileExample { // 可见性参数 /*volatile*/ static boolean flag = false; public static void main(String[] args) { new Thread(() -> { try { // 暂停 0.5s 执行 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag 被批改成 true"); }).start(); // 始终循环检测 flag=true while (true) { if (flag) { System.out.println("检测到 flag 变为 true"); break; } } }}
在不加volatile的时候,在子线程中批改了flag为true,然而父线程中是不可见的,咱们加上volatile润饰时”检测到 flag 变为 true“能够输入。再看一个指令重排的例子。
public class VolatileExample1 { // 指令重排参数 private static int a = 0, b = 0; private static int x = 0, y = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < Integer.MAX_VALUE; i++) { Thread t1 = new Thread(() -> { // 有可能产生指令重排,先 x=b 再 a=1 a = 1; x = b; }); Thread t2 = new Thread(() -> { // 有可能产生指令重排,先 y=a 再 b=1 b = 1; y = a; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("第 " + i + "次,x=" + x + " | y=" + y); if (x == 0 && y == 0) { // 产生了指令重排 break; } // 初始化变量 a = 0; b = 0; x = 0; y = 0; } }}
程序进行的时候只有先执行, x = b;而后执行 y = a;最初执行 a = 1和b = 1语句时,即产生了指令重排。咱们再说一个禁止指令重排的利用。单例模式中保障多线程环境下的单例咱们通常会应用双重校验的机制,实现代码如下:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getInstance() { if (lazyDoubleCheckSingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazyDoubleCheckSingleton == null) { lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; }}
对于要保障线程平安的单例最容易想到的形式就是在getInstance办法上加上synchronized就好啦,然而这种形式锁的力度太大,性能不是很好,所以咱们在getInstance办法上先判断一下lazyDoubleCheckSingleton这个变量是否为空,如果为空咱们就进行加锁。在再进行一次判断如果为空就创立一个对象。这里进行了两次判断所谓通常被称为双重校验。这里的成员变量为什么要加volatile?不加volatile会怎么样?为弄明确这个问题咱们先理解一下创立一个对象的过程。
以lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton()为例,
- 分配内存给这个对象
- 初始化对象
设置lazyDoubleCheckSingleton 指向刚调配的内存地址
如果咱们不应用volatile润饰这个lazyDoubleCheckSingleton的话可能会呈现,1-3-2的执行流程,当执行1-3步之后,此时lazyDoubleCheckSingleton变量曾经不为空了,他的值是new出对象的内存地址,此时有个线程过去了 到了if (lazyDoubleCheckSingleton == null) 这一步,判断不为空,就间接return进来了,这个线程拿到的就是一个未初始化的线程。所以咱们要应用volatile润饰,保障指令依照1-2-3的程序执行。上面加张图不便直观理解这个过程。
对于在多线程中的执行就变成上面的形式了。多线程.png