synchronized优化原理

文章目录

    • 1. 底层原理
    • 2. 优化方案一:轻量级锁
      • (1) 轻量级锁工作流程
      • (2) 锁膨胀
    • 3. 优化方案二:自旋优化
    • 4. 优化方案三:偏向锁
      • (1) 偏向状态
      • (2) 批量重偏向
      • (3) 批量重偏向撤销
    • 5. 优化方案四:锁消除

1. 底层原理

首先我们需要知道synchronized这个重量级锁的底层原理。synchronized是一种对象锁,它锁的对象是某个类的对象实例或某个类。在JVM中,每个类实例对象的对象头中都有一个monitor关键字,也就是所谓的管程,获得一个锁就等于获得了一个对象的管程,而每个对象只有一个管程,没有获得锁的线程会被操作系统阻塞,如下阻塞的线程会放入到管程的一个Entrylist集合。而我们要知道,管程是操作系统所有的,所以使用它的成本是很大的,所以我们需要对synchronized进行优化。

synchronized优化原理_第1张图片

2. 优化方案一:轻量级锁

(1) 轻量级锁工作流程

使用场景:如果一个对象虽然有多线程访问,但所现场访问的时间是错开的(也就是没有竞争,有竞争轻量级锁会升级为重量级锁),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,例如下面案例:

static final Object obj=new Object();
public static void method1(){
	synchronized(obj){
		//同步块A
		method2();
	}
}

public static void method2(){
   synchronized(obj){
       //同步块B
   }
}
  1. 创建锁记录(Lock Record)对象,每个现场的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarK Word
    synchronized优化原理_第2张图片
  2. 让锁记录中的Object Reference 指向锁对象,并尝试使用lock record交换(CAS)Object的Mark Word(交换成功了表示上锁成功吗,如果是01表示可以交换),将Mark Word的值存入锁记录

CAS是一种乐观锁机制,也被称为无锁机制。全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。

synchronized优化原理_第3张图片
4. 如果交换成功,对象头中存储了锁记录地址和状态00,表示该线程给对象加锁

synchronized优化原理_第4张图片
5. 如果交换失败(CAS失败),有两种情况:

  • 如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record 作为重入计数
    synchronized优化原理_第5张图片
  1. 当退出synchronized代码块(解锁时),如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1

synchronized优化原理_第6张图片
7. 当退出synchronized代码块(解锁)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象头

  • 成功:解锁成功
  • 失败:说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

(2) 锁膨胀

如果在尝试轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为了重量级锁。

static Object obj=new Object()
public static  void method1(){

}
  1. 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

synchronized优化原理_第7张图片
2. 此时Thread-1加轻量级锁失败,进入锁膨胀过程

  • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
  • 然后自己进入Monitor的EntryList队列进行自我阻塞

synchronized优化原理_第8张图片

  • 当Thread-0退出同步代码块(此时它还是拿着轻量级锁),使用cas操作将Mark Word的值恢复给对象头,失败。此时会进入重量级解锁流程,即按照monitor地址找到monitor对象,设置Owner为null,唤醒EntryList中被阻塞的线程

3. 优化方案二:自旋优化

重量级锁竞争时,还可以采用自旋来进行优化,如果当前线程自旋成功(即这时候持有锁的现场已经退出了同步块,释放了锁),此时当前现场就可以避免阻塞(前面我们所到,当一个现场申请Monitor锁时,若发现owner非空,就会进入EntryList进行阻塞,自旋优化的目的就是,若线程发现owner不为空,就会原地进行一定数量的自旋而不直接进入EntryList进行阻塞,如果自旋期间Monitor锁被释放了,自旋的线程就可以获得Monitor锁,这样就避免了阻塞-即避免了上下文切换)。
自旋重试成功的情况
synchronized优化原理_第9张图片
自旋重试失败的情况
synchronized优化原理_第10张图片

  • 在Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功后,那么认为这次自旋成功的可能性会高,就会多自旋几次,反之就会少自旋甚至是不自旋。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
  • Java7之后不能控制是否开启自旋操作

4. 优化方案三:偏向锁

轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作。Java 6引入了偏向锁来做进一步优化:只有一次使用CAS将线程ID设置到对象头的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不重新CAS。以后只要不发生竞争,这个对象就归线程所有。

synchronized优化原理_第11张图片synchronized优化原理_第12张图片

(1) 偏向状态

回忆一下对象头格式:
synchronized优化原理_第13张图片

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword为0x05即最后3位为101,这时它的thread、epoch、age都默认为0
  • 偏向锁时默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以使用VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建之后,Markword值为0x01即最后3位为001,这是它的hashcode、age都为0,第一次使用到hashcode才会赋值

注意:

  1. hashcode会禁用一个对象的偏向锁,这是因为hashcode被调用后,线程ID在对象头中就没有多余的位置存储线程ID了,所以就会让偏向锁失效
  2. 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  3. 调用wait/notify会使偏向锁失效(因为这个机制只有重量级锁中才有,所以偏向锁会升级为重量级锁)

(2) 批量重偏向

如果对象虽然被多个线程访问,但没有竞争(即一种时间错开的访问),这时偏向了现场1的对象仍有机会偏向线程2,重偏向会重置对象的线程ID。当偏向锁失效的阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会给这些对象加锁时重新偏向至加锁线程(这是对偏向锁情况失效的优化)。
案例场景

1. 创建一个user类
2. 初始化一个集合
3. 线程t0给这个集合循环添加30个user对象,然后分别给每个对象加锁,此时每个user对象头的信息都会显示偏向现线程t0
4. 然后线程t2再给集合中的30个对象再次加锁,会发现前20个对象,偏向锁会被撤销,会使用轻量级锁。而后10个对象由于jvm进行了批量重偏向,所以对象user类的对象重偏向到了t2

(3) 批量重偏向撤销

当偏向锁失效超过40次后(说明有很多现场会访问该对象),jvm会觉得,自己确实偏向错了,根本不应该偏向。于是整个类对象都会变得不可偏向,新建的对象也是不可偏向的。
案例场景

1. 创建一个user类
2. 初始化一个集合
3. 线程t0给这个集合循环添加40个user对象,然后分别给每个对象加锁,此时每个user对象头的信息都会显示偏向现线程t0
4. 然后线程t2再给集合中的30个对象再次加锁,会发现前19个对象,偏向锁会被撤销,会使用轻量级锁。而后11个对象由于jvm进行了批量重偏向,所以对象user类的对象重偏向到了t2
5. 然后线程t3重新给这40个对象加锁,会发现前19个对象由于t2撤销了重偏向所以前面19个对象还是撤销重偏向状态,,而后面出现批量重偏向撤销,而从20个对象开始的对象时偏向t2线程所以t3同样会进行批量撤销重定向操作,一直到第40个对象时已经有39次撤销操作了,所以user类以后所有对象会被设置为不可重偏向

5. 优化方案四:锁消除

首先我们使用JMH对下面代码进行一个基准测试:

  1. 创建一个maven项目,导入相关jar包,并写入下面代码
//总共做几轮测试
@Fork(1)
//采用吞吐量的模式
@BenchmarkMode(Mode.AverageTime)
//执行预热的次数
@Warmup(iterations = 3)
//正式测试的次数
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class jvmtestMain {
    static  int x=0;
    @Benchmark
    public void a()throws Exception {
        x++;
    }
    @Benchmark
    public void b() throws Exception{
        Object o=new Object();
        synchronized (o){
            x++;
        }
    }
}

打包运行

 java -jar benchmarks.jar

synchronized优化原理_第14张图片

最后结果发现加锁的b和没加锁的a性能(score)几乎差不多(按道理来说加锁会对程序性能有很大影响),这是因为JIT的存在,它会对我们字节码进行进一步优化,JIT会发现局部变量o不会逃逸出b方法的作用域(逃逸分析),即它是线程私有的不会出现并发安全问题,所以JIT对取消对变量o加锁。这中JIT的优化行为就称为锁消除,我们可以通过-XX: -Eliminatelocks来关闭JVM进行锁消除优化。

你可能感兴趣的:(JUC,java)