synchronized实现原理

本文详细解析了Java中`synchronized`的底层实现机制,涉及monitorenter/monitorexit、对象头的MarkWord、监听器、偏斜锁、轻量级锁和重量级锁的升级与降级,以及JVM如何优化锁性能以提升多线程环境下的效率。

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

目录

概述

一、synchronized底层实现

二、监听器

1、监听器

2、JVM中的对象结构

3、Mark Word

三、锁升级 

1、偏斜锁/偏向锁(Biased Locking)

2、轻量级锁(Lightweight Locking)

3、重量级锁

总结


概述

synchronized是Java内建的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能阻塞在那里。

在Java 5以前,synchronized是仅有的线程同步手段。在代码中,synchronized可以用来修饰方法,也可以使用在特定的代码块上,本质上synchronized方法等同于把方法全部语句用 synchronized块包起来。

synchronized同步锁在加锁和解锁的过程中,依赖于操作系统互斥锁(Mutex Lock)所实现的锁,消耗资源,属于重量级锁(在Java1.6以后进行了优化)。另外在获取锁时,必须一直等待,没有额外的尝试机制。

一、synchronized底层实现

使用synchronized保证线程安全,就是保证原子性,简单说就是执行过程中不会被其他线程干扰。

当我们对下面这段使用synchronized同步代码块且使用this作为锁包起来的代码进行javap反编译,可以看到类似片段:

synchronized (this) {
    while (sharedState < 100000) {
        int former = sharedState++;
        int latter = sharedState;
        if (former != latter - 1) {
            System.out.printf("数据观察结果: former is %d,latter is %d", former, latter);
        }
    }
}

/* 反编译结果:
11: astore_1
12: monitorenter
13: aload_0
14: dup
15: getfield  	#2              	// Field sharedState:I
18: dup_x1
…
56: monitorexit
*/

从结果可以看出synchronized底层利用monitorenter/monitorexit对实现了同步的语义。

所以,synchronized代码块是由一对monitorenter/monitorexit指令实现,synchronized是通过对象内部的叫做监视器(monitor)来实现的,线程通过执行monitorenter指令尝试获取monitor的所有权,当monitor被占用时就会处于锁定状态。

二、监听器

1、监听器

每个对象都有一个监视器(monitor),线程通过执行monitorenter指令尝试获取monitor的所有权,当monitor被占用时就会处于锁定状态。

当线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

(1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者,代表持有锁;

(2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;

(3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

当线程执行monitorexit指令时:

(1)执行monitorexit的线程必须是objectref所对应的monitor的所有者;

(2)指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者,即释放锁。其它被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

2、JVM中的对象结构

synchronized同步和monitor对象有关,而monitor则和对象头有关。 

在JVM中的对象是分成三部分存在的:对象头实例数据对齐填充。

(1)对象头:对象头包括两部分,Mark Word和类型指针,如果是数组的话还有额外的部分存放数组长度;

(2)实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;

(3)对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

3、Mark Word

可以看到锁信息也是存在于对象的mark word中的,不同的锁状态,mark word信息不同:

(1)偏斜锁时:当对象状态为偏斜锁(biasable)时,mark word存储的是偏向的线程ID;

(2)轻量级锁时:当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;

(3)重量级锁时:当状态为重量级锁(inflated)时(通常说synchronized的对象锁),为指向堆中的monitor对象的指针(monitor对象的起始地址)。

三、锁升级 

在JVM底层实现锁的过程中,有三种类型的锁:偏斜锁/偏向锁轻量级锁重量级锁

在Java 6之前,synchronized的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,非常消耗系统资源。

在Java 6之后,在Oracle JDK中,JVM提供了三种不同的monitor实现,也就是三种不同的锁:偏斜锁、轻量级锁和重量级锁,大大改进了其性能。

所谓锁的升级、降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

1、偏斜锁/偏向锁(Biased Locking)

       偏斜锁的核心思想是“假设加锁的代码从始至终就只有一个线程在调用,如果发现有多于一个线程调用,再升级成轻量级锁”。

       偏斜锁原理是当线程请求到锁对象后,将锁对象的状态标志位改为01,即偏向模式。然后使用CAS操作将线程的ID记录在锁对象的Mark Word中。以后该线程可以直接进入同步块,连CAS操作都不需要。

       偏斜锁是为了在单线程(没有出现多个线程并发)执行情况下,尽量减少不必要的轻量级锁执行路径,该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。因为轻量级锁的加锁与释放锁,也需要多次执行CAS原子指令。而偏向锁只需要在切换线程设置ThreadID的时候,执行一次CAS原子指令。所以,偏向锁的作用是在只有一个线程执行同步块时,进一步提高性能。

        当没有线程并发出现时,默认会使用偏斜锁。JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

       如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

2、轻量级锁(Lightweight Locking)

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

轻量级锁的加锁过程:

(1)在代码进入同步块的时候,如果对象锁状态为无锁状态(lock标志位“01”,biased_lock标志位“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
(2)拷贝对象头中的Mark Word复制到锁记录(Lock Record)中。
(3)拷贝成功后,虚拟机将尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向到对象的Mark Word。如果更新成功,则执行步骤4,否则执行步骤5。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的lock标志位设置为“00”,即表示此对象处于轻量级锁定状态。

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否已经指向当前线程的栈帧。如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争该对象的锁,轻量级锁就要升级为重量级锁,lock标志位的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的解锁过程:
(1)通过CAS指令,尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁,该锁已升级为重量级锁,那就要在释放锁的同时,通知其它线程重新参与锁的竞争。

3、重量级锁

依赖于操作系统互斥锁所实现的锁。操作系统的互斥锁实现线程之间的切换,需要从用户态转换到核心态,切换成本非常高,状态之间的转换需要相对比较长的时间,这是早期Synchronized效率低的原因。因此,这种依赖于操作系统互斥锁所实现的锁,称之为“重量级锁”。

用户态和核心态,代表两种不同的CPU状态。内核态(Kernel Mode)用于运行操作系统程序,用户态(User Mode)用于运行用户程序。

总结

JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的互斥锁来实现的,但是由于使用互斥锁需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用互斥锁,那么将严重的影响程序的性能。因此,JVM使用偏斜锁、轻量化锁等对锁进行了优化,大大改进了其性能。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值