从代码入手,先看下代码
public class SyncTest {
public int count;
public void get(){
//同步代码块
synchronized(this) {
count = count + 1;
}
}
public static void main(String[] args) {
}
}
注意这里我们加锁的方式是同步代码块,然后反编译下class文件,看一下get方法
public void get();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, 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:I
9: iconst_1
10: iadd
11: putfield #2 // Field count:I
14: aload_1
15: monitorexit //重点
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
这里我们可以看到这两行,第3行monitorenter和第15行monitorexit,如果我们把锁去掉,这两行代码就不存在了,这就是加锁的秘诀,编译器会在代码中为我们插入这两个指令。
我们可以将其看成一个对象,也就是我们拿锁所拿到的对象。synchronized就是通过这两个指令实现的。
下面我们将上面的同步代码块改为:
public synchronized void get() {
count = count + 1;
}
然后在看下编译后的class文件:
public synchronized void get();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //重点
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: aload_0
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return
我们发现那两个指令不见了,flags中多了ACC_SYNCHRONIZED,将方法标记成了一个同步方法。虽然在字节码中我们看不到那两条指令了,但是底层实现也是同样的原理。
原理
使用monitorenter和monitorexit指令实现的
- monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
- 每个monitorenter必须有对应的monitorexit与之配对
- 任何对象都有一个monitor与之关联
synchronized的优化(JDK1.6开始)
重量级锁
访问同步代码块的时候,竞争失败的线程会被挂起,发生上下文切换,每次上下文切换大约耗费3-5微秒,挂起一起,唤醒一次。
缺点:线程阻塞,响应时间慢。
使用场景:适用于追求吞吐量,同步代码块执行速度较长。
轻量级锁
CPU执行一条指令的时间基本在0.6纳秒,假设我们的同步代码块中的逻辑很简单,执行速度很快,则不将线程挂起,通过CAS操作来加锁和解锁,避免上下文切换。
自适应自旋锁(概念):过度的自旋操作也会造成CPU的资源浪费,控制自旋次数,由虚拟机自行进行判定,动态进行调整,不会超过一个上下文切换所需的时间,如果超过,则将升级为重量级锁。
缺点:如果始终得不到锁,竞争的线程使用自旋会消耗CPU。
使用场景:适用于追求响应时间,同步代码块执行速度非常快。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。第一次拿锁通过CAS操作,之后在拿锁判断上次拿锁的是不是自己,如果是自己就直接用,不需要在进行CAS操作。如果有竞争则升级为轻量级锁,会导致用户线程全部被挂起,竞争激烈时不可用。
缺点:如果线程间存在竞争,会带来额外的锁撤销的消耗。
使用场景:适用于只有一个线程访问同步块的场景。