近期遇到一个常见的问题:synchronized的工作机制,联想到volatile以及锁在不同场景下会自动升级的情况,对其进行探索并记录.
目录
synchronized的特性
原子性
monitor监视器(详见第三部分)
可见性
靠操作系统内核的Mutex Lock(互斥锁)实现,相当于 JMM 中的 lock、unlock。退出代码块时刷新变量到主内存
获取锁时,会清空当前线程工作内存中共享变量的副本值,重新从主内存中获取变量最新的值;
释放锁时,会将工作内存的值重新刷新回主内存;
有序性
程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序
Java对象的创建过程:类加载检查→分配内存→初始化零值→设置对象头→执行init(),接着设置实例对象指向刚分配的内存地址,然而,当实例对象还没初始化时可能内存地址已经分配完成.
synchronized使用了内存屏障,在 monitorenter 指令和 Load 屏障之后,会加一个 Acquire屏障,这个屏障的作用是禁止同步代码块里面的读操作和外面的读写操作之间发生指令重排,在 monitorexit 指令前加一个Release屏障,也是禁止同步代码块里面的写操作和外面的读写操作之间发生重排序。
可重入性
计数器
volatile的特性
有序性
volatile 的底层实现原理是内存屏障,Memory Barrier
在每个volatile写操作的前面插入一个StoreStore屏障,将保障上面所有的普通写操作结果在volatile写之前会被刷新到主内存->普通写操作对其他线程可见
在每个volatile写操作的后面插入一个StoreLoad屏障,避免volatile写操作与后面可能有的volatile读/写操作重排序
可见性
原理:MESI缓存一致性
在源码层面,先调用了C++里面的volatile,接着实际上是在汇编阶段加入了lock前缀,会将本地内存写入主内存当一个线程修改变量的值后,会立即触发cpu的嗅探机制,调用store、write等原子操作修改主内存的值(如果不加Lock前缀不会马上同步),同时及时失效其他线程变量副本。
cpu总线嗅探机制
cpu总线嗅探机制监听到这个变量被修改,就会把其他线程的变量副本由共享S置为无效I,当其他线程在使用变量副本时,发现其已经无效,就回去主内存中拿一个最新的值。
lock前缀的作用
- 使CPU缓存数据立即写回主内存(Volatile修饰的变量会带lock前缀)
- 触发总线嗅探机制和缓存一致性协议MESI来失效其他线程的变量
synchronized实现原理
对于代码块:
monitorenter指令+monitorexit指令
每个对象都和一个monitor关联,monitorenter尝试获取当前对象的monitor,若获取成功则获得锁,否则会被阻塞,同一个线程可以多次获得monitor(可重入性原理);
monitorexit将该对象的monitor的进入数减1,为0则释放锁,有两个monitorexit指令
第二个monitorexit指令,是在程序发生异常时候用到的,也就说明了synchronized在发生异常时,会自动释放锁,防止死锁
对于普通方法和静态方法:
使用ACC_SYNCHRONIZED 标识隐式实现,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。
区别:
synchronized修饰代码块,锁的对象就是代码块中的对象;修饰普通方法的时候,锁的对象就是当前对象this;修饰静态方法的时候,锁的对象就是当前类的Class字节码对象(类对象实例)
对象内存布局:
锁对象在对象头的MarkWord标记字中
锁升级原理
java代码层面:synchronized关键字
字节码层面:monitorenter指令和monitorexit指令,进入锁状态和退出锁状态
JVM层面:锁升级
汇编层面:lock comxchg这条指令
升级过程
new→偏向锁→轻量级锁(无锁,自旋锁,自适应自旋)→重量级锁
jdk1.6之后,默认开启偏向锁的检查,当锁是偏向锁时,被另一个线程访问时,偏向锁会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,默认自旋10次,如果自旋失败会升级为重量级锁
无锁态:头三位代表一个没有锁的状态。
偏向锁:头三位代表它为偏向锁。
更重量级的锁:只用两位代表就行了。
当出现第二个线程的时候,就升级为轻量级锁。当A、B两个线程就开始争抢,过程为:首先撤销偏向锁状态,每个线程都有自己的线程栈了,在各自的线程栈里生成自己的一个对象,这个对象叫做LockRecord(锁记录)。然后A、B线程开始抢,看谁能把各自LR指针给贴在坑位。也就说如果有任意一个线程抢到了这把轻量级锁的时候,这把轻量级锁里面就记录了指向线程栈中LockRecord的指针,占62位。这个抢的过程是采用自旋的方式,即CAS。用CAS操作把里面62位修改为指向我自己线程栈的LR指针。
竞争加剧后(JDK1.6之前指有线程超过10次自旋,-XX:PreBlockSpin,或者自旋线程数超过CPU核数的一半。JDK1.6之后,加入自适应自旋 Adapative Self Spinning,JVM自己控制。)升级为重量级锁。重量级锁存在于内核态,本质就是mutex数据结构,有数量限制的。用户态想申请重量级锁的时候,需要向内核申请。内核给了我这把锁,我才拥有这把锁,这个时候在markword中占62位来记录指向重量级锁的指针。
为什么要从轻量级锁升级到重量级锁呢?
轻量级锁本质就是执行一个循环,运行在用户态,效率高。但是这个循环是需要不断消耗CPU,如果我们竞争特别激烈,一个线程始终占着,10000个线程都在自旋,那CPU就很快100%了。升级为重量级锁后,维护有一个队列,而在这个队列里,如果没有轮到我执行的时候,是不消耗CPU的,处于wait状态。