回答重点

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


程序员Seven
53 声望6 粉丝

程序员Seven,记录Seven的菜鸟成长之路。在线阅读网站:www.seven97.top