synchronized优化原理

本文详细阐述了Java中synchronized锁的底层原理和多种优化策略,包括轻量级锁的工作流程、锁膨胀、自旋优化、偏向锁及其状态管理和锁消除。着重讲解了如何通过JVM优化减少锁的开销,提升并发性能。
摘要由CSDN通过智能技术生成

1. 底层原理

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

在这里插入图片描述

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
    在这里插入图片描述
  2. 让锁记录中的Object Reference 指向锁对象,并尝试使用lock record交换(CAS)Object的Mark Word(交换成功了表示上锁成功吗,如果是01表示可以交换),将Mark Word的值存入锁记录

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

在这里插入图片描述
4. 如果交换成功,对象头中存储了锁记录地址和状态00,表示该线程给对象加锁

在这里插入图片描述
5. 如果交换失败(CAS失败),有两种情况:

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

在这里插入图片描述
7. 当退出synchronized代码块(解锁)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象头

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

(2) 锁膨胀

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

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

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

在这里插入图片描述
2. 此时Thread-1加轻量级锁失败,进入锁膨胀过程

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

在这里插入图片描述

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

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

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

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

4. 优化方案三:偏向锁

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

在这里插入图片描述在这里插入图片描述

(1) 偏向状态

回忆一下对象头格式:
在这里插入图片描述

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,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

在这里插入图片描述

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

### 回答1: "synchronized" 关键字的底层原理是基于 Java 的内置锁(Monitor)机制。 Java 中的锁是通过对象的 monitor 实现的,每个对象都有一个对应的 monitor。当线程进入一个对象的 synchronized 代码块时,它会请求该对象的 monitor,如果该 monitor 没有被其他线程占用,则该线程获得该 monitor 并执行代码块,否则该线程阻塞,直到该 monitor 被释放。 通过使用 monitor,Java 可以保证同一时刻只有一个线程能够访问该对象的 synchronized 代码块,从而实现线程同步。 因此,"synchronized" 关键字实现了线程同步的功能,并且可以避免因竞争访问共享资源导致的线程安全问题。 ### 回答2: synchronizedJava中的关键字,用于实现多线程同步。它的底层原理是通过对象的内部锁(也称为监视器锁)来实现线程的互斥访问。 在Java中,每个对象都有一个与之关联的内部锁。当一个线程尝试进入被synchronized修饰的代码块时,它会首先尝试获得该对象的内部锁。如果锁没有被其他线程所占用,那么该线程就会获取到锁,并且进入临界区执行代码。如果锁已经被其他线程所占用,那么该线程就会进入阻塞状态,直到锁被释放。 在synchronized的实现中,锁的状态有两种:被线程占用和未被占用。当一个线程获得锁后,它会将锁的状态设置为已被占用。其他线程在尝试获取该锁时,会发现锁已被占用,它们会进入锁的等待队列中,等待获取锁的线程释放锁。 在Java语言规范中,对synchronized关键字进行了优化,包括偏向锁、轻量级锁和重量级锁三种状态,这样可以在不同场景下提高并发性能。 总结来说,synchronized的底层原理是通过对象的内部锁来实现线程的互斥访问。通过获取和释放锁的机制,保证了同一时间只有一个线程能够访问被synchronized修饰的代码块,从而保证了线程安全。这种机制虽然简单,但在多线程编程中起着重要的作用。 ### 回答3: synchronizedJava 中用来实现线程同步的关键字。它的底层原理主要是通过对象的监视器锁来实现的。具体来说,Java 中的每个对象都有一个与之相关联的监视器锁,也称为内部锁或互斥锁。 当线程进入一个 synchronized 代码块或方法时,它会尝试获取对象的监视器锁。如果该锁没有被其他线程占用,那么当前线程就可以获取到锁,并进入临界区。如果该锁已经被其他线程占用,则当前线程就会被阻塞,并且进入等待队列。 一旦当前线程进入临界区,它就可以执行 synchronized 代码块或方法中的内容。其他线程如果想要执行该 synchronized 代码块或方法,就必须等待当前线程释放锁。只有当当前线程执行完 synchronized 代码块或方法,且释放了锁,其他线程才有机会获取到锁并执行相应的代码。 synchronized原理可以用实例来解释。假设有一个共享资源,例如一个变量,多个线程同时修改该变量的值。如果没有同步机制,可能会导致不可预期的结果。但是当我们使用 synchronized 关键字对修改该变量的代码进行同步,每次只有一个线程能够获取到锁并修改变量的值,这样就保证了线程安全。 总结来说,synchronized 底层原理是通过对象的监视器锁来实现线程之间的同步。它确保了同一时刻只有一个线程能够获取到锁,并且其他线程需要等待锁的释放才能继续执行。这样可以有效地保护共享资源,避免多个线程同时对共享资源进行修改导致的数据不一致性和不可预测性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值