synchronized
synchroized 是一个重量级锁,但是现在经过优化后,引入了自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁后已经不是很笨重了。
synchroized实现原理
底层是使用monitorenter 和 monitorexit 指令实现的。
public class Demo {
...
public void add() {
synchronized (this) {
index++;
}
}
...
}
public void add();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: getfield #2 // Field index:Ljava/lang/Integer;
8: astore_2
9: aload_0
10: aload_0
11: getfield #2 // Field index:Ljava/lang/Integer;
14: invokevirtual #3 // Method java/lang/Integer.intValue:()I
17: iconst_1
18: iadd
19: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: dup_x1
23: putfield #2 // Field index:Ljava/lang/Integer;
26: astore_3
27: aload_2
28: pop
29: aload_1
30: monitorexit
31: goto 41
34: astore 4
36: aload_1
37: monitorexit
38: aload 4
40: athrow
41: return
从上面可以看到,monitorenter 会插入到同步代码块开始的位置,monitorexit 会插入到同步代码块结束的位置,JVM保证每一个monitorenter都有一个monitorexit对应。
Java对象头
sychronized是存放在Java 对象头中的,Java对象头分为两个部分,分别是mark word 与 klass poniter,klass poniter 存放的是对象指向类元数据的指针,JVM通过这个知道该对象是哪个类的实例。mark word 存放的是 哈希码、GC年龄分代、锁状态标志位、线程持有锁、偏向锁ID等信息。
自旋锁、适应性自旋锁
由于线程阻塞和唤醒给系统的性能会带来很大的影响,而且很多时候,对象被占有其实是很短的一段时间,为了很短的一段时间,频发阻塞唤醒就很不值当了,所以引入了自旋锁。
自旋锁:
就是线程不会发现对象被其他线程持有的时候,该线程并不会立即挂起,而是执行一段无意义的循环,但是这种循环也会给CPU带来压力,所有就限制循环的次数,但是有一个问题就是,假设设置自旋的次数为10次,执行完10后并没有获得,所以挂起,假设在第11次或第12次获得,这个就很尴尬了,所以引入了适应性自旋锁。
适应性自旋锁:
旋转的次数不在固定,而是由它前一次获取该锁的自旋时间及锁的状态决定。
- 线程如果自旋成功,获得锁了,那么下一次的自旋此时就会增加,JVM会认为上次已经成了,那么这次成功的可能性也会很大
- 如果很少成功或没有成功,JVM会减少自旋的次数或不进行自旋。
锁消除
利用逃逸分析来确定是否进行锁消除,也就是当JVM检测到不可能存在共享数据的竞争的时候,也就没有的加锁的意义。
/**
* 在StringAppend方法中不可能存在共享数据竞争的时候
*/
public class Demo {
public String StringAppend(){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("1");
stringBuffer.append("2");
return stringBuffer.toString();
}
}
锁粗化
将多个连续加锁、解锁的操作连在一起后,扩大成一个更大的锁。
例如在一个for循环,Vector 的add操作就会频繁的加锁、解锁,这个时候就适合锁粗化。
public class Demo {
private Vector<Integer> vector = new Vector<>();
public void add() {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
}
}
锁升级
锁有四种状态:无锁状态、偏向锁、轻量级锁、重量级锁,根据竞争锁的程度依次升级,只允许升级,不允许降级。
偏向锁
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变成偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word锁标志是否为偏向锁已经当前线程ID等于Mark Word的ThreadId即可。
基于Java对象头实现的
目的: 在无线程的竞争下减少不必要的获取轻量级锁的执行
获取过程:
检查对象头的mark word 是否可为偏向的状态。就是判断mark word 里面的是否为偏向锁的值是否为1,锁标志位的值是否为01.
若锁标志位为01,则判断对象头中的线程ID是否为当前线程ID。如果是,执行同步代码块。
若对象头中的线程ID不是当前ID,则通过CAS替换对象头中的线程ID,替换成功,执行同步代码块
若没有替换成功,表明有多个线程竞争,当到达全局安全点时,获取到偏向锁的线程挂起,偏向锁升级为轻量级锁,被挂起在全局安全点的线程继续执行。
偏向锁的释放:
线程不会主动释放偏向锁,而是采用了多个线程竞争时,释放偏向锁。偏向锁的释放需要等待全局安全点
全局安全点
在这个时间点上没有执行的代码。
轻量级锁
基于Java对象头实现的
当关闭偏向锁或偏向锁升级的时候,会升级为轻量级锁
适应的场景:线程交替执行同步块
获取过程:
- 判断当前对象是否是无锁状态?如果是,则JVM首先将当前现在的栈帧中,建立一个名字为锁记录(Lock Record)的空间,用于存储锁对象目前的mark word 的拷贝,即 Displaced Mark Word。如果不是,执行步骤 3。
- JVM利用CAS操作将对象头对的mark word 更新为指向Lock Record,如果成功,表示竞争到锁,将锁的标志位改为00(表示轻量级锁)执行同步操作。如果失败,执行步骤 3。
- 判断对象的mark word 是否之前当前线程的栈帧,如果是,表示已经持有当前对象的锁,执行同步代码块,如果不是,说明当前对象已经被其他对象持有,则当前线程尝试自旋来获取锁,如果没有获取到,则将锁的标识为改为10,表示锁膨胀成重量级锁,当前线程阻塞。
轻量级锁的释放
也是通过CAS进行的释放。
多线程竞争的情况下,轻量级锁的效率比重量级效率低,因为还要又一个CAS的操作
重量级锁
通过对象内部监视器,monitor 实现的。