美文网首页
synchronized

synchronized

作者: 奋斗的蛐蛐 | 来源:发表于2021-06-17 14:44 被阅读0次

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等信息。

MarkWord.png(参考Java并发编程艺术)

自旋锁、适应性自旋锁

由于线程阻塞和唤醒给系统的性能会带来很大的影响,而且很多时候,对象被占有其实是很短的一段时间,为了很短的一段时间,频发阻塞唤醒就很不值当了,所以引入了自旋锁。

自旋锁:

就是线程不会发现对象被其他线程持有的时候,该线程并不会立即挂起,而是执行一段无意义的循环,但是这种循环也会给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对象头实现的

目的: 在无线程的竞争下减少不必要的获取轻量级锁的执行

获取过程:

  1. 检查对象头的mark word 是否可为偏向的状态。就是判断mark word 里面的是否为偏向锁的值是否为1,锁标志位的值是否为01.

  2. 若锁标志位为01,则判断对象头中的线程ID是否为当前线程ID。如果是,执行同步代码块。

  3. 若对象头中的线程ID不是当前ID,则通过CAS替换对象头中的线程ID,替换成功,执行同步代码块

  4. 若没有替换成功,表明有多个线程竞争,当到达全局安全点时,获取到偏向锁的线程挂起,偏向锁升级为轻量级锁,被挂起在全局安全点的线程继续执行。

偏向锁的释放:

线程不会主动释放偏向锁,而是采用了多个线程竞争时,释放偏向锁。偏向锁的释放需要等待全局安全点

全局安全点

在这个时间点上没有执行的代码。

轻量级锁

基于Java对象头实现的
当关闭偏向锁或偏向锁升级的时候,会升级为轻量级锁

适应的场景:线程交替执行同步块

获取过程:

  1. 判断当前对象是否是无锁状态?如果是,则JVM首先将当前现在的栈帧中,建立一个名字为锁记录(Lock Record)的空间,用于存储锁对象目前的mark word 的拷贝,即 Displaced Mark Word。如果不是,执行步骤 3。
  2. JVM利用CAS操作将对象头对的mark word 更新为指向Lock Record,如果成功,表示竞争到锁,将锁的标志位改为00(表示轻量级锁)执行同步操作。如果失败,执行步骤 3。
  3. 判断对象的mark word 是否之前当前线程的栈帧,如果是,表示已经持有当前对象的锁,执行同步代码块,如果不是,说明当前对象已经被其他对象持有,则当前线程尝试自旋来获取锁,如果没有获取到,则将锁的标识为改为10,表示锁膨胀成重量级锁,当前线程阻塞。

轻量级锁的释放

也是通过CAS进行的释放。

多线程竞争的情况下,轻量级锁的效率比重量级效率低,因为还要又一个CAS的操作

重量级锁

通过对象内部监视器,monitor 实现的。

相关文章

网友评论

      本文标题:synchronized

      本文链接:https://www.haomeiwen.com/subject/axscgctx.html