回答重点

synchronized 实现原理依赖于JVM 的 Monitor(监视器锁)和对象头(Object Header)

  • synchronized 修饰代码块:会在代码块的前后插入 monitorentermonitorexit 指令。可以把 monitorenter理解为加锁,monitorexit 理解为解锁。(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
  • synchroized修饰方法:synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

其它问题

synchronized的用法有哪些?

  1. 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  2. 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

synchronized的作用有哪些?

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改能够及时可见;
  • 有序性:有效解决重排序问题。

Synchronized 修饰静态方法和修饰普通方法有什么区别?

  • Synchronized 修饰静态方法:锁的是这个类的 class 对象。也就是说,无论创建了多少个该类的实例,所有的实例共享同一个锁,因为这个锁属于类本身而不是某个对象实例。
  • Synchronized 修饰实例方法:锁的是当前实例(调用该方法的对象),也就是这个对象的内在锁。这也就是说每个对象实例都有自己独立的锁。

构造方法可以用 synchronized 修饰么?

构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。

另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。

volatile和synchronized的区别是什么?

  1. volatile只能使用在变量上;而synchronized可以在类,变量,方法和代码块上。
  2. volatile至保证可见性;synchronized保证原子性与可见性。
  3. volatile禁用指令重排序;synchronized不会。
  4. volatile不会造成阻塞;synchronized会。

Synchronized 能不能禁止指令重排序?

synchronized 无法完全禁止指令重排序,但能通过内存屏障保证多线程环境下的有序性。对于需要严格禁止重排序的场景,应优先选择 volatile。

这是因为同步块内部的代码仍可能被重排序,只要这种重排序不违反单线程语义

ReentrantLock和synchronized区别

  1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
  2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。
  3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
  6. synchronized 和 ReentrantLock 都是可重入锁

什么是可重入锁

可重入锁是一种特殊的互斥锁,它允许同一个线程在持有锁的情况下再次获取该锁。也就是说,同一个线程可以多次获取同一个可重入锁,而不会发生死锁。

在 Java 中,synchronized关键字就是一种可重入锁。当一个线程使用synchronized修饰的方法或代码块时,它会获得该对象的锁。如果该线程在持有锁的情况下再次调用同一个对象的synchronized方法或代码块,那么它会再次获得该对象的锁,而不会等待其他线程释放锁。

可重入锁的好处是可以避免死锁的发生。因为同一个线程可以多次获取同一个锁,所以当一个线程在持有锁的情况下需要再次获取锁时,它不需要等待其他线程释放锁,从而避免了死锁的发生。

需要注意的是,可重入锁并不是绝对安全的。如果一个线程在持有锁的情况下进行了一些不当的操作,仍然可能导致死锁的发生。因此,在使用可重入锁时,需要注意避免出现这种情况。

锁升级原理了解吗?

在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多(JDK18 中,偏向锁已经被彻底废弃)。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

synchronized 升级到重量级锁后,所有线程都释放锁了,此时它还是重量级锁吗?

当重量级锁释放了之后,锁对象是无锁的。

有新的线程来竞争的话又会从无锁再到轻量级锁开始后续的升级流程。

扩展——底层机制详细剖析

加锁释放锁原理

synchronized是 Java内建的同步机制,所以也被称为 Intrinsic Locking,提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取锁的线程时只能等待或者阻塞在那里。

synchronized是基于一对 monitorenter/monitorexit 指令实现的,Monitor对象是同步的基本实现单元,无论是显示同步,还是隐式同步都是如此。区别是同步代码块是通过明确的 monitorenter 和 monitorexit 指令实现,而同步方法通过ACC_SYNCHRONIZED 标志来隐式实现。

同步代码块

public class Test1 {
public void fun1(){
synchronized (this){
System.out.println("fun111111111111");
}
}
}

将.java文件使用javac命令编译为.class文件,然后将class文件反编译出来。反编译的字节码文件截取:

通过反编译后的内容查看可以发现,synchronized编译后,同步块的前后有monitorenter/monitorexit两个 字节码指令。在Java虚拟机规范中有描述两条指令的作用:翻译一下如下:

每个对象有一个监视器锁(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 的所有权。

Q:synchronized 代码块内出现异常会释放锁吗?

A:会自动释放锁,查看字节码指令可以知道,monitorexit插入在方法结束处(13行)和异常处(19行)。从Exception table异常表中也可以看出。

同步方法代码

public class Test1 {
//锁当前对象(this)
public synchronized void fun2(){
System.out.println("fun2222222222222222222222");
}
//静态synchronized修饰:使用的锁对象是当前类的class对象
public synchronized static void fun3(){
System.out.println("fun33333333333333");
}
}

编译之后反编译截图:

从反编译的结果来看,同步方法表面上不是通过monitorenter/monitorexit指令来完成,但是与普通方法相比,常量池中多出来了ACC_SYNCHRONIZED标识符。java虚拟机就是根据ACC_SYNCHRONIZED标识符来实现方法的同步,当调用方法时,调用指令先检查方法是否有 ACC_SYNCHRONIZED访问标志,如果存在,执行线程将先获取monitor,获取成功之后才执行方法体,执行完后再释放monitor。在方法执行期间,其他线程都无法再获取到同一个monitor对象。 虽然编译后的结果看起来不一样,但实际上没有本质的区别,只是方法的同步是通过隐式的方式来实现,无需通过字节码来完成。

ACC_SYNCHRONIZED的访问标志,其实就是代表:当线程执行到方法后,如果检测到有该访问标志就会隐式的去调用monitorenter/monitorexit两个命令来将方法锁住。

小结

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

可重入锁原理

ReentrantLock和synchronized都是可重入锁

定义

指的是 同一个线程的 可以多次获得 同一把锁(一个线程可以多次执行synchronized,重复获取同一把锁)。

/*  可重入特性    指的是 同一个线程获得锁之后,可以再次获取该锁。*/
public class Demo01 {
public static void main(String[] args) {
Runnable sellTicket = new Runnable() {
@Override
public void run() {
synchronized (Demo01.class) {
System.out.println("我是run");
test01();
}
} public void test01() {
synchronized (Demo01.class) {
System.out.println("我是test01");
}
}
};
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}

为什么要有可重入性?

可重入性主要有以下核心原因:

  • 避免死锁:在嵌套调用场景下(如递归方法、多层服务调用),同一个线程需要多次获取同一把锁。若不可重入,外层获取锁后内层再次尝试获取会被阻塞,导致线程永久等待。
  • 简化编程模型:业务代码中可能隐式调用已加锁的方法,可重入锁允许我们不必手动维护"当前线程是否已持有锁"的状态,降低心智负担
  • 提升性能:可重入机制通过维护重入计数器,避免了同一线程重复获取锁时的网络通信开销(如Redis的多次SETNX操作)。
  • 业务场景驱动:常见于需要嵌套事务、递归处理、链式调用等场景。例如:
// 伪代码示例:嵌套调用
public void methodA() {
lock.lock();
try {
methodB(); // 需要能再次获取同一个锁
} finally {
lock.unlock();
}
} public void methodB() {
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
}

原理

synchronized 的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就 + 1,在执行完一个同步代码块时,计数器数量就会减1,直到计数器的数量为0才释放这个锁。

  • 执行monitorenter获取锁 :

    • (monitor计数器=0,可获取锁)
    • 执行method1()方法,monitor计数器+1 -> 1 (获取到锁)
    • 执行method2()方法,monitor计数器+1 -> 2
    • 执行method3()方法,monitor计数器+1 -> 3
  • 执行monitorexit命令 :
    • method3()方法执行完,monitor计数器-1 -> 2
    • method2()方法执行完,monitor计数器-1 -> 1
    • method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
    • (monitor计数器=0,锁被释放了)

优点

可以一定程度上避免死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入);

保证可见性原理

这个主要在于内存模型和happens-before规则

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。

public class MonitorDemo {
private int a = 0; public synchronized void writer() { // 1
a++; // 2
} // 3 public synchronized void reader() { // 4
int i = a; // 5
} // 6
}

该代码的happens-before关系如图所示:

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。

这里是2 happens-before 5,通过这个关系可以得出:根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1

Synchronized是怎么实现的?的更多相关文章

  1. java 多线程 Synchronized方法和方法块 synchronized(this)和synchronized(object)的理解

    synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块. 1. synchronized 方法:通过在方法声明中加入 synchronized ...

  2. 单例模式中用volatile和synchronized来满足双重检查锁机制

    背景:我们在实现单例模式的时候往往会忽略掉多线程的情况,就是写的代码在单线程的情况下是没问题的,但是一碰到多个线程的时候,由于代码没写好,就会引发很多问题,而且这些问题都是很隐蔽和很难排查的. 例子1 ...

  3. Thread 学习记录 <1> -- volatile和synchronized

    恐怕比较一下volatile和synchronized的不同是最容易解释清楚的.volatile是变量修饰符,而synchronized则作用于一段代码或方法:看如下三句get代码: int i1;  ...

  4. synchronized使用说明

    好久没有更新博客了,今天试着用简单的语言把synchronized的使用说清楚. synchronized是什么? synchronized是用来保证在多线程环境下代码同步执行的可重入的互斥锁.所谓互 ...

  5. 【Java并发系列04】线程锁synchronized和Lock和volatile和Condition

    img { border: solid 1px } 一.前言 多线程怎么防止竞争资源,即防止对同一资源进行并发操作,那就是使用加锁机制.这是Java并发编程中必须要理解的一个知识点.其实使用起来还是比 ...

  6. (转)Lock和synchronized比较详解

    今天看了并发实践这本书的ReentantLock这章,感觉对ReentantLock还是不够熟悉,有许多疑问,所有在网上找了很多文章看了一下,总体说的不够详细,重点和焦点问题没有谈到,但这篇文章相当不 ...

  7. Synchronized同步性与可见性

    Synchronized是具有同步性与可见性的,那么什么是同步性与可见性呢? (1)同步性:同步性就是一个事物要么一起成功,要么一起失败,可谓是有福同享有难同当,就像A有10000去银行转5000给身 ...

  8. 基于synchronized 或 ReadWriteLock实现 简单缓存机制

    package cn.xxx.xxx; import java.util.HashMap; import java.util.Map; import java.util.concurrent.lock ...

  9. 【Java并发编程实战】-----synchronized

    在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者.写相同的数据,访问相同的文件等等.对于这种情况如果我们不加以控制,是非常容易导致错误的.在java中,为了解决这个问题,引入临界区概念. ...

  10. Lock、ReentrantLock、synchronized、ReentrantReadWriteLock使用

    先来看一段代码,实现如下打印效果: 1 2 A 3 4 B 5 6 C 7 8 D 9 10 E 11 12 F 13 14 G 15 16 H 17 18 I 19 20 J 21 22 K 23 ...

随机推荐

  1. 红日复现为什么失败之struct-046流量分析加msf特征总结

    struts2漏洞 一.指纹识别 s2的url路径组成(详见struts.xml配置文件):name工程名+namespace命名空间+atcion名称+extends拓展名 部署在根目录下,工程名可 ...

  2. k8s:The connection to the server localhost:8080 was refused - did you specify the right host or port?

    前言 k8s 集群 node节点报错:The connection to the server localhost:8080 was refused - did you specify the rig ...

  3. Linux系统查看CPU使用率、内存使用率、磁盘使用率

    一.查看CPU使用率1. top 命令[root@sss ~]# toptop - 16:54:38 up 7 days,  5:13,  3 users,  load average: 0.00, ...

  4. [每日算法 - 阿里机试] leetcode739. 每日温度

    入口 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer.https://le ...

  5. 【Docker】DockerFile解析

    DockerFile解析 中文官网 英文官网 是什么 Dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本. 构建三步骤 注意:Dockerfile可以构建出镜像, ...

  6. HTB打靶记录-Administrator

    # 信息收集 nmap -sV -sC -O 10.10.11.42 Nmap scan report for 10.10.11.42 Host is up (0.70s latency). Not ...

  7. zk源码—3.单机和集群通信原理

    大纲 1.单机版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 2.集群版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 (3)Leader选举阶段 (4)Leader和Fol ...

  8. 微软正式发布 .NET 10 Preview 3

    2025年4月11日,.NET团队在博客上宣布了.NET 10 Preview 3的正式发布,文章参见:https://devblogs.microsoft.com/dotnet/dotnet-10- ...

  9. mybatis的输入参数类型

    一.传递简单数据类型 二.传入一个bean对象 三.传入一个包装对象(对象中存放对象)

  10. 🎀git统计某段时间内代码的修改量/总代码量

    1.前往git本地项目路径下 2.右键打开Git Bash工具 3.输入命令: 3.1.某段时间代码修改量 git log --since=2021-01-01 --until=2021-05-18 ...