Synchronized(1) - 偏向锁

本文深入探讨了Java中synchronized关键字的实现原理及其在JAVASE1.6及更高版本中的优化,特别是偏向锁的机制。包括synchronized的使用方式、与对象头的关系、偏向锁的实现流程以及批量重偏向和撤销机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Synchronized(1) - 偏向锁

在多线程并发编程中,synchronized一直扮演重要的角色,在JAVA SE 1.6版本之前被称为重量级锁。在JAVA SE 1.6中synchronized得到了优化,为了减少锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁。本文重点介绍偏向锁的实现。

1 synchronized实现原理与使用

利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下三种形式。

  • 对于普通同步方法,锁是当前实例对象

  • 对于静态同步方法,锁是当前类的Class的对象

  • 对于同步方法块,锁是synchronized括号里配置的对象

如下代码,分别对普通方法以及同步方法块进行加锁。

1
2
3
4
5
6
7
8
9
10
public class SyncTest {
  public void syncBlock(){
    synchronized (this){
      System.out.println("hello block");
    }
  }
  public synchronized void syncMethod(){
    System.out.println("hello method");
  }
}

当SyncTest.java被编译成class文件的时候,我们可以用javap -v命令查看class文件对应的JVM字节码信息,部分信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
  public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter        // monitorenter指令进入同步块
         4: getstatic           // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc                 // String hello block
         9: invokevirtual       // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit         // monitorexit指令退出同步块
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit         // monitorexit指令退出同步块
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  //添加了ACC_SYNCHRONIZED标记
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic          // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc                // String hello method
         5: invokevirtual      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

对同步方法块而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

对普通同步方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁。

2 synchronized 与对象头

在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:Mark Word和类型指针,另外对于数组而言还会有一份记录数组长度的数据。其中Mark Word用于存储对象的HashCode、分代年龄、锁状态等信息,类型指针是指向该对象所属类对象的指针。如下图所示,为Mark Word的默认存储结构,可以看到锁信息也是存在于对象的Mark Word中的。

在运行期间,Mark Word里存储的数据会随着锁标志位变化而变化,如下图所示。当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;当状态为轻量级锁时,Mark Word存储的是指向线程栈中锁记录(Lock Record)的指针;当状态为重量级锁时,为指向堆中的互斥量对象的指针。

所以,锁的状态有4种,级别从低到高分别为:无锁状态、偏向锁状态、轻量级锁、重量级锁,这几个状态会随着竞争情况逐渐升级,而不会降级。

3 synchronized - 偏向锁

当JVM启用了偏向锁模式时,当新创建一个对象的时候,那新创建对象的mark word将是可偏向状态,新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

下面将依据偏向锁的流程进行介绍,如下图所示:


按照上图所示,从线程A访问同步块的操作流程如下:

  1. 当线程A第一次访问同步块时,先检测对象头Mark Word中的锁标志位(最后两位)是否为01,依此判断此时对象锁是否处于无锁状态或者偏向锁状态;
  2. 然后判断偏向锁标志位(倒数第三位)是否为1,如果不是,则进入轻量级锁逻辑,如果是,则进入下一步流程;
  3. 判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈帧中添加一条Displaced Mark Word为空的Lock Record,用来统计重入的次数。当退出同步块的时候会释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id;
  4. 如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。如果当前对象锁状态处于匿名偏向锁状态,则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈帧中找到内存地址最高的可用Lock Record,将线程ID存入),获取到锁,执行同步代码块;
  5. 如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;
  6. 偏向锁的撤销需要等待全局安全点(safe point,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;
  7. 如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态,然后升级为轻量级锁,进行CAS竞争锁;
  8. 如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);
  9. 唤醒暂停的线程,从安全点继续执行代码。


下图展示了锁状态的转换流程:

  • 分配对象:若偏向锁可用,则对象头Mark Word为匿名偏向锁状态(Thread Id为0),否则对象头Mark Word为无锁状态
  • 初始锁定:如上步骤4操作
  • 锁定/解锁:锁定即指重入时的处理,解锁即指退出同步块的处理,如上步骤3操作
  • 重偏向:如上步骤8操作
  • 撤销偏向:如果对象已锁定,如上步骤6操作;如果对象未锁定,如上步骤7操作

 



批量重偏向与撤销(bulk rebias/revocation):

 

从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。而在该状态下所有线程都是暂停的,所以偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

解决场景:

  1. 一个线程创建了大量对象(属于同一类型)并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作,这样会导致大量的偏向锁撤销操作。
  2. 存在明显多线程竞争的场景下使用偏向锁是不合适的。
    批量重偏向机制是为了解决第一种场景,批量撤销则是为了解决第二种场景。


原理:
对象所属的类 class 中, 会保存一个 epoch 值,每一次该class的对象发生偏向撤销操作时,该值+1。当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。当这个值达到批量撤销阈值(默认40)时,就会执行批量撤销。此外还有一个time阈值(默认25s)用来重置epoch 值,如果自从上次执行批量重偏向已经超过了这个阈值时间,就会发生epoch 重置。


批量重偏向:
发生批量重偏向时,将class中的epoch值+1,同时遍历JVM中所有线程栈, 找到该class所有正处于加锁状态的偏向锁对象,将其对象的epoch字段改为class中epoch的新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等(说明该对象目前没有线程在执行同步块),所以算当前对象已经偏向了其他线程,也不会执行撤销操作,而是可以直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id


批量撤销:
当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。


偏向锁JVM参数设置:

  • -XX:-UseBiasedLocking=false 关闭偏向锁,默认进入轻量级锁
  • -XX:BiasedLockingStartupDelay=0 关闭偏向锁延时



参考
[1] https://github.com/farmerjohngit/myblog/issues/13
[2] https://www.liangzl.com/get-article-detail-124090.html

### Java 中 `synchronized` 关键字相关的机制 #### 偏向 (Biased Locking) 偏向旨在减少无竞争情况下的同步开销。当一个线程访问同步块并获取时,JVM会假设该只会被这个线程再次获取。因此,在第一次获得之后,后续对该的操作几乎没有任何成本。 - **工作原理**: JVM会在对象头的标记字段中存储首次定此对象的线程ID[^1]。 - **特点**: -和解无需额外消耗资源。 - 如果发生争用,则需要撤销偏向模式,转换成其他形式的- 对于单一线程频繁调用的情况特别有效。 ```java public class BiasedLockExample { private final Object monitor = new Object(); public void performAction() { synchronized(monitor) { // 初始状态下monitor处于未定状态;一旦某个线程获得了它,就会成为偏向 System.out.println(Thread.currentThread().getName()); } } } ``` #### 轻量级 (Lightweight Locking) 轻量级用于处理短时间内的轻微并发冲突。在这种情况下,多个线程可以轮流快速地占有同一把而不会造成严重的性能损失。 - **工作原理**: 当检测到有第二个线程试图进入临界区时,当前持有的会被提升至轻量级级别,并且尝试通过循环比较交换(CAS)的方式让后来者取得所有权而不必挂起自己[^4]。 - **特性**: - 非阻塞性质使得等待中的线程能够继续运行而不是立即停止。 - 若长时间无法得到则可能退化为重量级- 更适合那些预计会有短暂延迟但又确实会发生少量竞态条件的应用场合。 ```java // 这里展示的是伪代码逻辑, 实际上由JVM内部实现 if (!isLocked()) { acquireInExclusiveMode(); } else if (canBeUpgradedToLightWeight()) { upgradeAndTryAcquireViaCAS(); } else { escalateToHeavyWeightLock(); } ``` #### 重量级 (Heavyweight Locking) 这是最传统的互斥机制,通常意味着较高的上下文切换代价以及较低的整体效率。然而对于某些特定的任务来说却是必要的选择。 - **运作方式**: 所有的请求都将排队等候直至前序事务完成释放为止。在此期间任何新的参与者都得被迫休眠直到轮到了它们才行[^5]。 - **属性**: - 可能导致更高的CPU利用率因为涉及到操作系统层面的过程调度。 - 不过也正因为如此才得以保证绝对的安全性和顺序一致性。 - 主要应用于长期占用共享资源的情形下。 ```java class HeavyWeightLockDemo { static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args)throws InterruptedException{ Thread t1=new Thread(()->{try{lock.lock();System.out.println("Thread 1 holds the lock");Thread.sleep(20);}finally{lock.unlock();}}); Thread t2=new Thread(()->{try{lock.lock();System.out.println("Thread 2 holds the lock");}finally{lock.unlock();}}); t1.start();t2.start(); t1.join();t2.join(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值