Synchronized原理

概述

synchronized是Java提供的一种原子性内置锁,也叫作监视器锁,它是一种排他锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问会被阻塞;拿到内部锁的线程会在正常退出同步代码块或者抛出异常或者同步代码块内调用了该内置锁资源的wait方法系列方法时释放该锁。

内存语义

对于前文提到的共享变量内存不可见问题,synchronized在内存中是把synchronized块内用到的变量从线程的工作内存中清除,这样在synchronized使用到该共享变量时就不会从线程的工作内存中获取,而是直接从主内存中获取;退出synchronized块的内存语义就是把在synchronized块内对共享变量的修改刷新到主内存。

synchronized的使用

  • 普通方法和代码块
  //对象锁 加锁的是调用该类的对象(相同的对象调用会存在阻塞)
  public synchronized void say(){
      System.out.println("普通方法加锁");
  }
  • 静态方法和静态代码块
  //类锁 (所有调用该方法的代码都会存在阻塞)
  public static synchronized void sayHello(){
      System.out.println("静态方法加锁");
  }

锁的原理

public class SynchronizedDemo {

    public  void say(){
        synchronized (this){
            System.out.println("--------");
        }

    }
}

SynchronizedDemo进行javac编译成.class文件后,再通过javap -verbose SynchronizedDemo.class命令查看字节码文件,如下:

 Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                    //加锁
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String --------
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1                            
        13: monitorexit                        //释放锁
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return

我们可以看到字节码文件中包含monitorenter(加锁)和monitorexit(释放锁)两个步骤。任何对象都有一个监视器锁(monitor)关联,线程执行monitorenter指令时尝试获取monitor的所有权。

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程为monitor的所有者
  • 如果线程已经占有该monitor,重新进入,则monitor的进入数加1
  • 线程执行monitorexitmonitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit
  • 如果其他线程已经占用monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

锁优化

  • 锁消除

锁消除即删除不必要的加锁操作。虚拟机即时编辑器在运行时,对一些“代码上要求同步,但是被检测到不可能存在共享数据竞争”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

  public class SynchronizedTest {

      public static void main(String[] args) {
          SynchronizedTest test = new SynchronizedTest();

          for (int i = 0; i < 100000000; i++) {
              test.append("abc", "def");
          }
      }
      //append方法中StringBuffer中的append方法虽然加了锁,但是sb对象不会被共享(每次调用都会new一个),所以jdk会进行锁消除
      public void append(String str1, String str2) {
          StringBuffer sb = new StringBuffer();
          sb.append(str1).append(str2);
      }
  }    
  • 锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

  //这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
  public class StringBufferTest {
      StringBuffer stringBuffer = new StringBuffer();

      public void append(){
          stringBuffer.append("a");
          stringBuffer.append("b");
          stringBuffer.append("c");
      }
  }

锁升级

java中锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量锁状态以及重量级锁状态。锁的的状态可以升级但不能降级。另外锁的状态是由对象头存储的信息决定的,所以我们先来了解下什么是java对象头。

java对象头

java中每个对象都拥有对象头,对象头由Mark World 、指向类的指针、以及数组长度三部分组成,我们只需要关心Mark World 即可,Mark World 记录了对象的HashCode、分代年龄和锁标志位信息。锁的升级变化,体现在锁对象的对象头Mark World部分,也就是说Mark World的内容会随着锁升级而改变。下图分别是对象头在32位和64位操作系统中的信息,不同的标志位代表着不同的锁。

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁是为了在只有一个线程执行同步块时提高性能。引入偏向锁是为了减少轻量级锁一直自旋带来的消耗,偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。

偏向锁获取过程:

  • 无锁状态,存储内容「是否为偏向锁(0)」,锁标识位01
    • CAS设置当前线程ID到Mark Word存储内容中
    • 是否为偏向锁0=> 是否为偏向锁1
    • 执行同步代码或方法
  • 偏向锁状态,存储内容「是否为偏向锁(1)、线程ID」,锁标识位01
    • 对比线程ID是否一致,如果一致执行同步代码或方法,否则进入下面的流程
      • 如果不一致,CASMark Word的线程ID设置为当前线程ID,设置成功,执行同步代码或方法,否则进入下面的流程
        • CAS设置失败,证明存在多线程竞争情况,触发撤销偏向锁,当到达全局安全点,偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后在安全点的位置恢复继续往下执行。

关闭偏向锁:

可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,持有锁时间也不长的场景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失,所以干脆不阻塞这个线程,让它自旋一段时间等待锁释放。当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。轻量级锁的获取主要有两种情况:① 当关闭偏向锁功能时;② 多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

轻量级锁获取过程:

  • 无锁状态,存储内容「是否为偏向锁(0)」,锁标识位01

    • 关闭偏向锁功能时

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容

    • 锁标识位设置为00

    • 执行同步代码或方法

    • 释放锁时,还原Mark Word内容

  • 轻量级锁状态,存储内容「线程栈中锁记录的指针」,锁标识位00(存储内容的线程是指"持有轻量级锁的线程")

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容,设置成功获取轻量级锁,执行同步块代码或方法,否则执行下面的逻辑

      • 设置失败,证明多线程存在一定竞争,线程自旋上一步的操作,自旋一定次数后还是失败,轻量级锁升级为重量级锁
        • Mark Word存储内容替换成重量级锁指针,锁标记位10

重量级锁

轻量级锁所适应的场景是线程近乎交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。Mark Word的锁标记位更新为10,Mark Word指向互斥量(重量级锁)。

锁升级的过程总结

参考地址:https://zhuanlan.zhihu.com/p/29866981

参考地址:https://www.zhihu.com/question/267980537

参考书籍:【java并发编程之美】

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值