Synchronized底层实现
1.功能
synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

2.了解synchronized所需的基础知识
- CAS(compare and swap)
如上图所示,长方形的内存中有一个值为1,现在线程A想实现想在它的基础上加1并将结果写回到到内存中,回经历上图中的三个步骤:
① 访问内存读取内存值1
② 计算结果
③ 将结果写回内存中时先对比该此时内存中的值是否与之前取出的值是否一致,一致则将结果写入内存,不 一致(中途线程B修改了内存中的值)则重复三个步骤直到写入结果成功为止
ABA问题:其他线程修改数次最后值与原值相同(线程B在A进行步骤三之前多次修改了内存值,但修改完的最终值仍然是1,此时A在步骤三进行比较时判定结果未被其他线程修改,直接将结果写入),在一些特殊的场景是需要区分内存是否被其他线程修改的情况,可采用版本号的方式进行判断
-
CAS底层实现
在Java中CAS最底层实现是 lock cmpxchg
cpu中有一条汇编指令是 cmpxchg,cpu底层支持compare and exchange操作,但该指令不是原子的,cpu在执行这条命令的中途也会被打断,所以在多核场景下需要使用指令lock将锁定一个北桥信号(可以简单理解为内存总线)锁住,使得cmpxchg不会被其他cpu打断
-
Java对象的内存分布
如下图所示,一个普通的Java对象由四部分组成

1.markword
主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode
2.Klass pointer
是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例
3.instance data
用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型
4.对齐字节是为了减少堆内存的碎片空间
-
markword的细节
下图为64位markword的具体细节功能,其中针对synchronized来说,synchronized本质上就是对markword里面的值进行操作,最需要关注的是最后三位的值,用于表示不同级别的锁

3.synchronized的升级过程(偏向锁->轻量级锁->重量级锁)
synchronized在JDK1.6以前被开发者经常诟病,原因是因为一旦使用synchronized就必须向操作系统内核申请锁资源,这样会造成严重的性能损耗,之后对其进行了一系列优化,在大部分情况下使用synchronized就能满足需要,下面是锁的大致升级过程:

首先我们需要先了解一下什么是偏向锁、轻量级锁和重量级锁?
假设现在有一个只提供一人的自习室,目前自习室中没有人在使用
偏向锁:
这时同学A(线程A)要使用该自习室,同学A便把自己的名字(线程id)贴到了自习室的门口(markword),告诉大家现在自习室正在使用。
为什么需要要有偏向锁呢?
因为有多数的synchronized方法,在很多情况下,只有一个一个线程或者在同一时刻只会有一个线程运行(如StringBuffer的一些sync方法),如果此时仍需要向内核获取锁代价太大,所以首先初始化一个对象,首先会使用CAS的方式将创建这个对象的线程id写到markword上,标识为偏向锁(具体可见上表markword细节中偏向锁一行)
轻量级锁:
假设同学A线程A)和同学B(线程B)同时来到了自习室门口,说要使用该自习室,这时两者之间就产生了竞争,这时将直接升级成轻量级锁,每个线程在线程栈中生成LockRecord,用CAS方式尝试把做自己的指针更新到markword,所以轻量级锁有被称为自旋锁
为什么轻量级锁不使用线程id而是用LockRecord?
使用LockRecord可使轻量级锁实现锁重入
重量级锁:
重量级锁重量级锁依赖于操作系统的互斥量(mutex) 实现, 其具体的详细机制此处暂不展开, 日后可能补充。 此处暂时只需要了解该操作会导致进程从用户态与内核态之间的切换, 是一个开销较大的操作
那么自旋锁什么时候升级成重量级锁呢?由于自旋锁自旋是需要占用CPU,线程数量过多的时使用自旋不太合适,以下是升级成重量级锁的条件:
1.老的方式:有线程自旋10次;自旋线程达到cpu核数的一半
2.新的方式:jvm中采用自适应自旋(Adaptive CAS)自行判断何时升级成重量级锁
待补充问题(留个坑之后补充):
1.当有线程已经已经使用某对象,给对象标识为偏向锁,此时偏向锁是如何升级成轻量级锁
2.重量级锁具体实现
3.各种锁是如何撤销,在什么时间点撤销
网友评论