synchronized原理及优化

本文深入探讨Java中`synchronized`的工作原理,包括Java对象头的结构、 Monitor监视器、字节码层面的实现,以及优化策略如轻量级锁、自旋优化、偏向锁及其升级过程,并介绍了锁消除等优化手段。
摘要由CSDN通过智能技术生成


参考:黑马程序员

1.Java对象头

以32位虚拟机为例
在这里插入图片描述
普通对象的对象头一共有64位前面的32为是Mark Word后面的32位是Klass Word,这个Klass Word是一个指针他指向了这个对象所从属的Class。
在这里插入图片描述
MarkWord的32位(普通)包括了25位的hashcode,4位的GC分代年龄,1位标识是否偏向锁,最后两位标识了加锁状态。

2. synchronized工作原理

Monitor被翻译成监视器或管程
在这里插入图片描述
当有一个线程进入synchronized代码块中这个obj对象的对象头中MarkWord的头30位会指向Monitor监视器,后两位加锁状态会发生改变然后看一下Owner是否为空,如果为空则会指向进来的线程,说明这个进来的线程已经持有锁,这时候如果其他线程进来也会检查一下Owner是否为空如果为空就会占有这个锁,如果不为空则会被EntryList指向,这个EntryList可以理解为一个阻塞队列,如果Thread2执行结束释放锁,那么阻塞队列的线程会竞争上岗,抢占这个Owner。

3.synchronized工作原理(字节码角度)

来看这一段代码然后我们对他进行一次反编译看看字节码角度synchronized是怎么工作的。

public class Test4 {
   
    static final Object lock = new Object();
    static  int counter = 0;
    public static void main(String[] args) {
   
        synchronized (lock){
   
            counter++;
        }
    }
}

在这里插入图片描述

4.synchronized优化原理

4.1轻量级锁

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

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

在这里插入图片描述
首先每个线程的栈帧里面会有一个锁记录Lock Record,当加锁的时侯会将锁记录里面的ObjectReference指向锁对象,然后通过一个cas操作将锁记录中的lock Record地址和Object的Mark Word进行交换
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。
在这里插入图片描述
此时如图所示。
在CAS操作过程中有可能出现失败的情况一种是这个锁对象已经被别人使用了。这时候Object原来的Mark Word中的内容已经是锁记录地址和状态00。这时候说明锁存在竞争,进入锁膨胀。
还有一种就是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。这个例子就是开头的那段代码method2外面被加了一个锁,method2里面又加了一次锁,这就是锁重入。

在这里插入图片描述
如图所示当发生锁重入时栈帧里面仍然会创建一个锁记录并把自己的Object Reference指向Object这时候还会寻求执行CAS操作不过当他发现Object里面已经有lock Record地址的时候就会失败这个部分就会被置为空值不过这也没事,因为他知道自己线程栈帧中的其他锁记录已经指向了Object,这个锁重入的锁记录主要目的是计数看看加了几层锁。

当解锁的时候会查看这个锁记录一旦发现里面的值是空,说明有锁重入然后就把他删了,表示重入记录减1。当发现这个锁记录的值不为空,这时使用CAS将Mark Word的值恢复给对象头,成功则解锁成功,失败则说明轻量级锁已经升级为重量级锁,进入重量级锁的解锁过程。

4.2锁膨胀

来看下面这个例子。
在这里插入图片描述
Thread-0已经获取了锁对象,这时候Thread-1也想获取这个锁于是他把自己的锁记录中的Object Reference,指向Object,然后他想让自己的锁记录地址和Object的Mark Word做CAS操作,但是这个CAS操作无法成功,已经有其他线程上了轻量级锁有竞争,这时候需要进行锁膨胀,将轻量级锁升级成重量级锁。
这时候Thread-1加轻量级锁失败,进入锁膨胀流程。

  • 为Object对象申请Monitor锁,让Object指向重量级锁地址。Object对象的MarkWord改成Monitor地址(前30位)后2位改成10说明一下这是重量级锁。
  • 然后自己进入Monitor的EntryList BLOCKED阻塞状态。
    在这里插入图片描述
  • 当Thread-0退出同步块解锁的时候,使用CAS将Mark Word的值恢复给对象头,失败。这时会进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner位null,唤醒EntryList中的BLOCKED线程。

5.自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这个后持锁线程已经释放了锁),这时当前线程就可以避免阻塞。
注意事项:

  • java6之后这个自旋是自适应的比如说你这次成功了那么认为自旋成功可能性高,就会多旋几次;反之就少自旋甚至不自旋。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。

6.偏向锁

static final Object obj = new Object();
public static void m1() {
   
 synchronized( obj ) {
   
 // 同步块 A
 m2();
 }
}
public static void m2() {
   
 synchronized( obj ) {
   
 // 同步块 B
 m3();
 }
}
//。。。。。。。。。。。。。。

看这一段代码同一个线程m1首先拿到obj对象锁,然后会执行m2,m2这个方法会做一次CAS操作试图拿到obj这个对象,同理后面的m3,m4可能嵌套一大堆在试图获取obj这个对象时都会做一次CAS操作,但其实他们都是一个线程,所以会进入锁重入,这个表明是同一个线程在获取这个obj对象。
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
下面两张图可以生动形象的说明轻量级锁和偏向锁的区别。
在这里插入图片描述
在这里插入图片描述

6.1偏向状态

在这里插入图片描述
Normal状态前面存储了哈希码,分代年龄等等,倒数第三位biased_lock是0,说明没加偏向锁,Biased状态是加了偏向锁的状态,倒数第三位为1,Biased状态前面存储的也不是hashcode了,存储的是thread线程ID。LightWeight Locked是轻量级锁,最后两位是00,前面的是锁记录的地址,Heavyweight Locked是重量级锁最后两位是10,前面存储的是monitor对象的地址。
一个对象创建时,默认都是开启了偏向锁(将来加锁的时候优先考虑加偏向锁)也就是说它的二进制MarkWord最后3位应该是101,但是偏向锁默认是延迟的不会在程序启动时立即生效,如果想避免延迟,可以加VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。

下面看一个例子我们打印出来一个对象加偏向锁之后的对象头信息。

// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 
pu
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答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
发出的红包

打赏作者

温JZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值