Synchronized实现原理,你知道多少?
1.synchronized的作用是什么
synchronized也叫作同步锁,解决的是多个线程之间对资源的访问一致性。换句话说,就是保证在同一时刻,被synchronized修饰的方法或代码块只有一个线程在执行,其他线程必须等待,解决并发安全问题。
其可以支持原子性、可见性和有序性。三大特性的说明
2.synchronized的应用
2.1锁的分类
synchronized的锁可分为类锁和对象锁。
1)类锁
是用来锁类的,让所有对象共同争抢一把锁。一个类的所有对象共享一个class对象,共享一组静态方法,类锁的作用就是使持有者可以同步地调用静态方法。当synchronized修饰静态方法或者class对象的时候,拿到的就是类锁。
2)对象锁
是用来锁对象的,虚拟机为每个的非静态方法和非静态域都分配了自己的空间,每个对象各有一把锁,即同一个类如果有两个对象就有两把锁。synchronized修饰非静态方法或者this时拿到的就是对象锁。
2.2锁的应用
synchronized可用于方法和代码块。其中方法分为普通方法(非静态方法)和静态方法,而修饰代码块时又可以修饰类(xxx.class)和当前对象(this)。下面通过使用是否使用同步锁的现象来进行说明:
1)静态方法
先看下面的案例,这里使用两个线程对用一个数据进行操作,两次的结果理想值应该分别是10000和20000。(多了效果的直观性,这里循环此时比较大,每个测试方法建议运行多次,避免出现偶然的情况达到理想情况),打印结果一定是什么?
public class TestUtil{
private static int i = 0; //共享资源
//基础方法,数字累加
public static void baseMethod() {
for (int j = 0; j < 10000; j++) {
i++;
}
System.out.println(i);
}
//静态方法,没有使用同步锁
public static void addS() {
baseMethod();
}
}
public class SynchronizedTest {
public static void main(String[] args) {
test1();
}
public static void test1() {
new Thread(() -> {
TestUtil.addS();
}).start();
new Thread(() -> {
TestUtil.addS();
}).start();
}
}
打印结果
,根据结果可以看出,并不是理想值,导致数据错乱。这就是典型的多线程问题,面对这样的问题,不得不加锁。这时,只需要在方法上加同步锁即可。
//TestUtil
public synchronized static void addS2() {
baseMethod();
} //SynchronizedTest
public static void test2() {
new Thread(() -> {
TestUtil.addS2();
}).start();
new Thread(() -> {
TestUtil.addS2();
}).start();
}
运行test2()后结果已符合理想情况。
分析:当对静态方法加锁后,就使用了类锁,这个类的所有对象共享这个静态方法,这个方法就是同步方法。换句话说,就是当一个线程执行一个对象中的同步方法时,其他线程调用同一对象上的同步方法将会被阻塞,直到第一个线程完成使用这个对象。
2)普通方法
原始代码如下,工具类实现了Runnable接口(或继承Thread类,若不做此操作则无法看出效果)重写run方法
public class TestUtil implements Runnable{
private static int i = 0; //共享资源
@Override
public void run() {
add();
}
//基础方法,数字累加
public static void baseMethod() {
for (int j = 0; j < 10000; j++) {
i++;
}
System.out.println(i);
}
//非静态方法,没有使用同步锁
public void add() {
baseMethod();
}
}
public class SynchronizedTest {
public static void main(String[] args) {
test3();
}
public static void test3() {
TestUtil test = new TestUtil();
new Thread(test).start();
new Thread(test).start();
}
public static void test4() {
TestUtil test = new TestUtil();
TestUtil test2 = new TestUtil();
new Thread(test).start();
new Thread(test2).start();
}
}
test3()运行结果
,test4()的运行结果也是相似。这时,是不是只需要在方法上加同步锁就可以了呢?
//TestUtil
public synchronized void add2() {
baseMethod();
} @Override
public void run() {
add2();
}
加锁后并修改run方法中调用的方法名,运行test3()已符合理想情况。但test4()却还是错乱,都已经是同步方法了,这是为什么呢?其实很简单,对非静态方法加锁后,就使用了对象锁,对于test3(),两个线程都使用的同一个对象,自然是对这个对象加锁,从而保证数据的安全。而test4()创建了两个对象,两个线程使用的是两个对象,这两个对象分别只对自己加锁丝毫不影响别的对象的锁。自然会出现问题。那么究竟要怎么办呢?既然是使用的不同的锁,那么让它们使用同一把锁不就好了吗,只需要将这个非静态的同步方法改为静态同步方法即可。
3)同步代码块
所谓的同步代码块就是只对某一部分代码加锁,而不是对整个的方法加锁,因为若方法体比较大,涉及到同步的代码又只是一小部分,如果对方法加锁性能比较差。同步代码块可以对class对象(类锁)或this加锁(对象锁),下面以类锁进行说明
@Override
public void run() {
//其他操作... //对象锁
// synchronized (this) { } //类锁
synchronized (TestUtil.class) {
for (int j = 0; j < 10000; j++) {
i++;
}
}
System.out.println(i);
}
在测试时虽然加了同步锁,但无论是调用test3()还是test4()都无法达到理想结果,第一次输出的值是变化的,第二次的值才是20000。为了解决这个问题,我们可以把需要加锁的那块代码抽出来,封装成静态方法,然后对该方法加锁,如下即可

同步静态方法需要对变量进行返回,原因很简单,就是在加锁的时候,还未释放锁,其他线程去获取的变量值可能不是最新的,只有当前释放锁后才会把最新的值返回。
3.synchronized底层实现原理
synchronized的底层实现是完全依赖JVM虚拟机的,所以先看看对象的存储结构。
3.1对象结构
JVM是虚拟机,是一种标准规范,主要作用就是运行java的类文件的。而虚拟机有很多实现版本,HotSpot就是虚拟机的一种实现,是基于热点代码探测的,有JIT即时编译功能,能提供更高质量的本地代码。HotSpot 虚拟机中对象在内存中可分为对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其组成结构如下图:

1)实例数据:存放类的属性数据信息,包括父类的属性信息。如果是数组,那么实例部分还包括数组的长度,这部分内存按4字节对齐。
2)对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
3)对象头
对于元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
对于标记字段,用于存储对象自身的运行时数据,其组成如下图

(锁信息占3位)在jdk1.6之前只有重量级锁,而1.6后对其进行了优化,就有了偏向锁和轻量级锁。
3.2上锁的原理
对于同步代码块来说,主要使用monitor(监视器)实现的,就相当于一个房间一次只能被允许一个进程进入。主要包含monitorenter和monitorexit。其中monitorenter指向同步代码块开始的位置,monitorexit指向同步代码块结束的位置。当执行monitorenter时,线程试图monitor的持有权,当monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。若其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。当执行monitorexit时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor。
对于同步方法来说,使用的是ACC_SYNCHRONIZED 标识,通过该标识指明该方法是否是一个同步方法。
3.3锁升级
所谓的上锁,就是把锁的信息记录在对象头中,默认是无锁的,当达到一定的条件时会进行锁升级,会按照下面的顺序依次升级。
- 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
- 偏向锁:作用是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步(使用同步锁的情况下)。
- 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞。目的是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,从而提高性能。
- 重量级锁:指的是原始的Synchronized的实现,当使用同步锁后,其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。
如何查看对象的信息呢?下面以User对象进行说明:
①User对象
public class User {
public String name;
private String pwd;
private Integer age;
//get,set等方法在此省略
}
②导入依赖,用于获取对象头信息
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
③打印对象信息
package com.zxh.demo; import com.zxh.demo.entity.User;
import org.openjdk.jol.info.ClassLayout; public class LockTest { public static void main(String[] args) {
User user = new User();
System.out.println("对象结构:(格式化)");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}
打印结果如下:

其中A和B区域是对象头信息,C是对象的成员变量(包含了默认值)。而A就是mark-down的信息,B是元数据指针。对A来说,有两行,也就是64位,但是在看二进制时需要反过来看。比如上述64位正常情况下拼接(00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000),但实际时应该是(00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001),因此最后的八位在上述mark-down图中就是最后面的8位,那么对于锁信息就是001,也就是无锁。
那么怎么将锁升级为偏向锁呢?JVM默认延时超过4s会开启偏向锁,也可以在程序启动时去指定。这里通过程序休眠的方式去开始偏向锁:
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
User user = new User();
System.out.println("启用偏向锁");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
for (int i = 0; i < 2; i++) {
synchronized (user) {
System.out.println("偏向锁101:" + ClassLayout.parseInstance(user).toPrintable());
}
System.out.println("释放偏向锁 :" + ClassLayout.parseInstance(user).toPrintable());
}
}
从下面的运行结果可以看出,启用偏向锁后,升级为偏向锁(锁信息为101),对象头中就记录了线程的id

虽然在代码中释放了偏向锁,但对象头中的信息不会改变,原因是偏向锁不会主动释放,方便同一个线程下次直接去执行被加锁的代码块。
此时,如果再加入线程对对象加锁,那么立马就会变为轻量级锁:
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
User user = new User();
System.out.println("启用偏向锁");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
for (int i = 0; i < 2; i++) {
synchronized (user) {
System.out.println("偏向锁101:" + ClassLayout.parseInstance(user).toPrintable());
}
System.out.println("释放偏向锁 :" + ClassLayout.parseInstance(user).toPrintable());
}
new Thread(()->{
synchronized (user) {
System.out.println("轻量级锁000:" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
}
}
运行结果部分截图:

此时,如果还有其他线程继续对此对象加锁(必要时可加延时),那么就会升级为重量级锁:
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
User user = new User();
System.out.println("启用偏向锁");
System.out.println(ClassLayout.parseInstance(user).toPrintable());
for (int i = 0; i < 2; i++) {
synchronized (user) {
System.out.println("偏向锁101:" + ClassLayout.parseInstance(user).toPrintable());
}
System.out.println("释放偏向锁 :" + ClassLayout.parseInstance(user).toPrintable());
}
new Thread(()->{
synchronized (user) {
System.out.println("轻量级锁000:" + ClassLayout.parseInstance(user).toPrintable());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("轻量级锁-> 重量级锁:" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized (user) {
System.out.println("重量级锁010:" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
}
运行结果部分截图:

因此,锁升级的原理是:默认情况下是无锁的,此时对象头中threadId的值为空,但在手动开始或延迟4秒后会进入偏向锁。在进入偏向锁时,会将threadId设置为此线程的id,那么当线程再次进入时 ,会判断此线程的id是否和threadId一致。如果一致则可以直接使用此对象,若不一致(有其他线程进入)则会升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁。如果执行一定次数后还没有正常获取到要使用的对象,则会升级轻量级锁为重量级锁。
Synchronized实现原理,你知道多少?的更多相关文章
- synchronized实现原理
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案, ...
- 深入理解Java并发之synchronized实现原理
深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...
- Java并发编程原理与实战九:synchronized的原理与使用
一.理论层面 内置锁与互斥锁 修饰普通方法.修饰静态方法.修饰代码块 package com.roocon.thread.t3; public class Sequence { private sta ...
- 深入理解并发编程之----synchronized实现原理
版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...
- Java精通并发-synchronized关键字原理详解
关于synchronized关键字原理其实在当时JVM的学习[https://www.cnblogs.com/webor2006/p/9595300.html]中已经剖析过了,这里从研究并发专题的角度 ...
- Java多线程和并发(八),synchronized底层原理
目录 1.对象头(Mark Word) 2.对象自带的锁(Monitor) 3.自旋锁和自适应自旋锁 4.偏向锁 5.轻量级锁 6.偏向锁,轻量级锁,重量级锁联系 八.synchronized底层原理 ...
- synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)
1.synchronized概述: synchronized修饰的方法或代码块相当于并发中的临界区,即在同一时刻jvm只允许一个线程进入执行.synchronized是通过锁机制实现同一时刻只允许一个 ...
- synchronized底层原理
synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...
- synchronized运行原理以及优化
线程安全问题 线程不安全: 当多线程并发访问临界资源时(可共享的对象),如果破坏原子操作,可能会造成数据不一致. 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可以保证其正确性. 原子操作 ...
- synchronized的原理
synchronized的使用 synchronized是一个java中的关键字,是基于JVM层面的,用于保证java的多线程安全,它具有四大特性,可用于完全替代volatile: 原子性:所谓原子性 ...
随机推荐
- ETL数据集成丨通过ETLCloud工具,将Oracle数据实时同步至Doris中
ETLCloud是一个全面的数据集成平台,专注于解决大数据量和高合规要求环境下的数据集成需求.采用先进的技术架构,如微服务和全Web可视化的集成设计,为用户提供了一站式的数据处理解决方案. 主要特点和 ...
- @DS注解的使用,动态数据源,事务 -九五小庞
有时,在一个项目中会用到多数据源,此时可以使用苞米豆的dynamic-datasource-spring-boot-starter:首先,引入jar包: <dependency> < ...
- Win10专业版去除桌面快捷图标箭头的问题
有深度技术的win10专业版用户,说他安装了几个软件程序,在桌面上的快捷访问图标,都会在图标左上角有个箭头,这看着很不习惯,说在win7旗舰版系统里面就不会出现这个箭头,这给人的感觉好不爽,想要删除那 ...
- Win11专业版电脑开机速度慢的问题
近期有一位电脑基地的用户反映自己的电脑开机速度总是比别人慢很多,老是需要等很久电脑才会进入系统,为此十分苦恼,那么对于这一情况有没有什么方法解决呢?下面我们一起来看看电脑基地小编是如何解决win11专 ...
- ARM-M与RISC-V(32bit)的区别--基于CM4与Nuclei_N300
1 systick与core timer ARM Cortex-M内核包含了一个SysTick定时器,SysTick 是一个24 位的倒计数定时器,当计到0 时,将从RELOAD 寄存器中自动重装载定 ...
- 递推&递归思想(递归=逆向递推)
递归 = 逆向递推(本质是一致的) 递推 初始条件 + 递推式 格点法 格点法 对于数的计算:对于合法操作来说,本质上即可看作递推 递归 终止条件 + 递归式 将规模大的问题转化为形式相同但规模更小的 ...
- 手把手教你多卡分布训练Accelerate使用配置教程
作者:SkyXZ CSDN:SkyXZ--CSDN博客 博客园:SkyXZ - 博客园 开发机环境:Ubuntu 22.04 | 112x CPU | 1TB RAM | 8×NVIDIA A100- ...
- CloudQuery 询盾社区版 v1.5.0 正式发布!
这是社区版回归与大家见面的第一个版本,在新版本 v1.5.0 中,CloudQuery 主要功能包括以下几个模块: 统一数据库管理 数据库纳管支持 完善 SQL 编辑器 数据源操作权限 时间权限设置 ...
- permutations函数和combinations函数使用
https://www.cnblogs.com/kaka00311/p/16114944.html python itertools模块中全排列函数包含combinations函数和permuta ...
- SPSS插件Process 2.16.3下载附安装教程
Process V2.16.3是一款用于 SPSS 软件中的调节效应插件,专门进行分析中介效应和调节效应,Process 主要应用于 SAS.SPSS 等传统数据统计分析软件,在 SPSS 中除了可以 ...