synchronized在底层是如何实现加锁的呢?
使用monitorenter和monitorexit指令实现的
//count进行累加
public void incCount(){
synchronized (this){
count = count + 1;//count++;
}
}
synchronized关键字包围的同步块,查看.class
public void incCount();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=5, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: aload_0
6: getfield #2 // Field count:J
9: lconst_1
10: ladd
11: putfield #2 // Field count:J
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
3到15之间是实现+1
在实现count = count + 1; 的前后加了monitorenter和monitorexit,这是编译器帮我们插入的
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
每个monitorenter必须有对应的monitorexit与之配对
任何对象都有一个monitor与之关联
如果不用同步代码块,而是把synchronized加到方法上
//count进行累加
public synchronized void incCount(){
// synchronized (this){
count = count + 1;//count++;
// }
}
public synchronized void incCount();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=5, locals=1, args_size=1
0: aload_0
1: aload_0
2: getfield #2 // Field count:J
5: lconst_1
6: ladd
7: putfield #2 // Field count:J
10: return
改之前:flags: ACC_PUBLIC
incCount()方法是public方法
改之后:flags: ACC_PUBLIC, ACC_SYNCHRONIZED
多了一个ACC_SYNCHRONIZED,表示是一个同步方法,在底层还是monitorenter和monitorexit,只不过在字节码中不能体现,在执行中使用这两个指令实现的
锁存放的位置
synchronized关键字放到java的对象头里面
在代码中 new Object();每一个对象在内存中被存放时还要有对方头
对象头包含一些关键的信息,
1.GC年龄…,MarkWord
2.对象类(类型指针) KlassPoint
3.数组长度(一般对象没有)
对象头的内容会随着对象的运行会有所改变的
可能加锁范围之内操作的指令30个*0.6ns = 18ns
上下文切换2w个周期 3~5个微秒
上下文切换费事很亏,于是引入轻量级锁和偏向锁
轻量级锁
重量级锁的情况下,没有拿到锁的线程都要被挂起的,
轻量级锁没拿到锁的不挂起,进行自旋,通过CAS操作来加锁和解锁。
自旋锁:不停地尝试,不去阻塞上下文切换
如果加锁代码块很快能执行完,用轻量级锁就很好
如果加锁代码块需要长的时间去执行,比如等待拿数据,那不停自旋就很亏,所以又引入适应性自旋锁
适应性自旋锁: 自旋次数控制(默认定义为10次),在1.6中自选次数不再是固定的值了,由虚拟机动态进行调整(自旋的时间大概是一个线程的上下文切换的时间)
偏向锁
大部分的情况下同一把锁总是同一个线程获得
在线程拿锁的过程中,锁的获得者总是偏向于第一个拿到锁的线程
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。
CAS不想做了,直接测试下拿锁的是不是我自己
偏向锁第一次还是需要CAS操作,把线程的ID和时间戳…替换掉
第二次开始检查对象头线程ID是不是自己就行了
有第二个线程来竞争,那就锁就升级到轻量级锁
一旦发生竞争就要升级为轻量级锁,把偏向锁撤销,换成轻量级锁
偏向锁的撤销会发生 STW(Stop The Word)
线程1是持有偏向锁的线程,当线程2准备去撤销偏向锁的时候,是会修改线程1的堆栈上的内容的,线程2会修改线程1的相关的数据,偏向锁引入STW,停止线程进行修改
java后台一般会禁止偏向锁,因为一般是多线程并发竞争
总结
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
不同锁的比较:
偏向锁适用场景是没有竞争
一旦发生竞争,偏向锁就会撤销,晋升为轻量级锁
轻量级锁用的是自旋的CAS操作,自选次数太多性能会下降
轻量级锁会膨胀为重量级锁,引入阻塞状态,让CPU做更多有效的事情
锁被膨胀后不能降级
偏向锁、轻量级锁、重量级锁全部是synchronized底层实现,是JVM里面C++代码实现的