Java synchronized 全面解析:从入门使用到底层原理(面试必备)

目录

一、入门:synchronized 是什么?解决什么问题?

1.1 核心定义

1.2 线程不安全的场景(为什么需要 synchronized)

1.3 加上 synchronized 后的效果

二、基础使用:synchronized 的三种用法(必掌握)

2.1 用法一:修饰实例方法(锁定当前对象 this)

2.2 用法二:修饰静态方法(锁定类对象 .class)

2.3 用法三:修饰代码块(锁定指定对象,灵活度最高)

案例1:锁定 this(等价于修饰实例方法,但粒度更细)

案例2:锁定 Class 对象(等价于修饰静态方法)

案例3:锁定自定义对象(常用于多线程共享资源的同步)

三种用法的核心区别(面试高频)

三、进阶:synchronized 的核心特性(理解这些,才算入门)

3.1 原子性(核心特性)

3.2 可见性(核心特性)

3.3 有序性(核心特性)

3.4 可重入性(重要特性)

3.5 非公平性(重要特性)

四、底层原理:synchronized 是如何实现的?(面试难点)

4.1 字节码层面:synchronized 的指令体现

4.2 JVM 层面:Monitor 锁与对象头

4.2.1 Java 对象头结构

4.2.2 Monitor 锁的本质

4.3 锁升级机制(JDK 1.8 优化,重点面试考点)

4.3.1 1. 无锁状态

4.3.2 2. 偏向锁(减少无竞争下的锁开销)

4.3.3 3. 轻量级锁(减少多线程竞争下的阻塞开销)

4.3.4 4. 重量级锁(最终锁形态)

锁升级总结(表格梳理)

六、面试高频问题

Q1:synchronized 的作用是什么?

Q2:synchronized 有几种用法?分别锁定什么对象?

Q3:synchronized 是公平锁还是非公平锁?

Q4:synchronized 是可重入锁吗?什么是可重入性?

Q5:synchronized 如何保证原子性、可见性、有序性?

Q6:JDK 1.6 对 synchronized 做了哪些优化?

Q7:synchronized 和 volatile 的区别?(高频中的高频)

Q8:synchronized 和 ReentrantLock 的区别?

七、总结


一、入门:synchronized 是什么?解决什么问题?

1.1 核心定义

synchronized 是 Java 中的内置锁(也叫监视器锁 Monitor Lock),是一种非公平、可重入的独占锁。它可以修饰方法、代码块,通过“锁定特定对象”的方式,让多个线程排队访问共享资源,从而保证同一时刻只有一个线程能执行被锁定的代码,解决线程安全问题。

1.2 线程不安全的场景(为什么需要 synchronized)

我们先看一个没有使用 synchronized 的例子,直观感受线程不安全的问题:模拟多线程执行“自增操作”(共享变量 count 从 0 自增到 10000)。

「预期结果」:10000 + 10000 = 20000 「实际结果」:大概率是小于20000的随机数(比如18937、19562)

「原因」:count++ 不是原子操作,当两个线程同时读取到同一个 count 值(比如100),线程1自增为101、线程2自增也为101,最终写入内存的是101,相当于“少加了一次”。这种多线程竞争共享资源导致的数据错乱,就是“线程不安全”,而 synchronized 就能解决这个问题。

1.3 加上 synchronized 后的效果

我们给 increment 方法加上 synchronized 修饰,再执行代码:

public class SynchronizedDemo { // 共享变量 private static int count = 0; // 自增方法(未加锁) public static void increment() { count++; // 看似一行代码,实际是 读取count → 自增 → 写入count 三步操作(非原子) } public static void main(String[] args) throws InterruptedException { // 线程1:执行10000次自增 Thread thread1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { increment(); } }); // 线程2:执行10000次自增 Thread thread2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { increment(); } }); thread1.start(); thread2.start(); thread1.join(); // 等待线程1执行完毕 thread2.join(); // 等待线程2执行完毕 System.out.println("最终count值:" + count); } }

「实际结果」:每次执行都是 20000,完全符合预期。

「原理」:synchronized 锁定了 increment 方法,让多个线程排队执行该方法,同一时刻只有一个线程能进入方法执行 count++,保证了自增操作的原子性,从而避免了数据错乱。

二、基础使用:synchronized 的三种用法(必掌握)

synchronized 有三种核心用法,分别是「修饰实例方法」、「修饰静态方法」、「修饰代码块」,每种用法锁定的“对象”不同,适用场景也不同,必须区分清楚(面试常考)。

2.1 用法一:修饰实例方法(锁定当前对象 this)

synchronized 修饰非静态方法(实例方法)时,锁定的是「调用该方法的对象实例」(this)。也就是说,同一个对象实例调用该方法时,会排队执行;不同对象实例调用该方法时,互不影响(因为锁定的是不同的对象)。

public class SynchronizedInstanceMethod { // 实例变量(属于对象实例的共享资源) private int num = 0; // synchronized 修饰实例方法,锁定 this(当前对象) public synchronized void add() { for (int i = 0; i < 1000; i++) { num++; } } public static void main(String[] args) throws InterruptedException { // 创建两个不同的对象实例 SynchronizedInstanceMethod obj1 = new SynchronizedInstanceMethod(); SynchronizedInstanceMethod obj2 = new SynchronizedInstanceMethod(); // 线程1:调用 obj1 的 add 方法 Thread t1 = new Thread(obj1::add); // 线程2:调用 obj2 的 add 方法 Thread t2 = new Thread(obj2::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("obj1.num:" + obj1.num); // 1000(预期) System.out.println("obj2.num:" + obj2.num); // 1000(预期) }

「说明」:t1 锁定的是 obj1,t2 锁定的是 obj2,两个线程锁定的是不同对象,因此可以同时执行 add 方法,互不干扰,最终两个对象的 num 都能达到 1000。

「注意」:如果两个线程调用的是同一个对象实例的 add 方法,就会排队执行,比如将 t2 改为 `new Thread(obj1::add)`,那么最终 obj1.num 会是 2000。

2.2 用法二:修饰静态方法(锁定类对象 .class)

synchronized 修饰静态方法时,锁定的不是 this,而是「当前类的 Class 对象」(每个类在 JVM 中只有一个 Class 对象,全局唯一)。因此,无论创建多少个对象实例,调用该静态方法时,所有线程都会排队执行(因为锁定的是同一个 Class 对象)。

public class SynchronizedStaticMethod { // 静态变量(属于类的共享资源,全局唯一) private static int num = 0; // synchronized 修饰静态方法,锁定 SynchronizedStaticMethod.class public static synchronized void add() { for (int i = 0; i < 1000; i++) { num++; } } public static void main(String[] args) throws InterruptedException { SynchronizedStaticMethod obj1 = new SynchronizedStaticMethod(); SynchronizedStaticMethod obj2 = new SynchronizedStaticMethod(); // 线程1:调用 obj1 的静态 add 方法 Thread t1 = new Thread(obj1::add); // 线程2:调用 obj2 的静态 add 方法 Thread t2 = new Thread(obj2::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + num); // 2000(预期) }

「说明」:虽然 t1 和 t2 调用的是不同对象实例的 add 方法,但 add 是静态方法,锁定的是 SynchronizedStaticMethod.class(全局唯一),因此两个线程会排队执行 add 方法,最终 num 累加为 2000。

2.3 用法三:修饰代码块(锁定指定对象,灵活度最高)

synchronized 修饰代码块时,需要手动指定「锁定的对象」(括号内填写锁定对象),语法:`synchronized(锁定对象) { 需同步的代码 }`。这种用法的灵活度最高,可以只锁定“需要同步的代码片段”(而非整个方法),减少锁的粒度,提升程序性能。

常见的锁定对象有两种:this(当前对象)、Class 对象,也可以是自定义的对象(只要保证多线程锁定的是同一个对象即可)。

案例1:锁定 this(等价于修饰实例方法,但粒度更细)
public class SynchronizedBlockThis { private int num = 0; public void add() { // 只锁定自增相关的代码块,而非整个 add 方法 synchronized (this) { for (int i = 0; i < 1000; i++) { num++; } } // 非同步代码(多个线程可同时执行) System.out.println("非同步代码执行"); } public static void main(String[] args) throws InterruptedException { SynchronizedBlockThis obj = new SynchronizedBlockThis(); Thread t1 = new Thread(obj::add); Thread t2 = new Thread(obj::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + obj.num); // 2000(预期) }

「说明」:只有 synchronized 代码块内的自增操作会被同步,代码块外的打印语句可以被多个线程同时执行,相比修饰整个实例方法,减少了锁的范围,提升了效率。

案例2:锁定 Class 对象(等价于修饰静态方法)
public class SynchronizedBlockClass { private static int num = 0; public void add() { // 锁定 Class 对象,全局唯一 synchronized (SynchronizedBlockClass.class) { for (int i = 0; i < 1000; i++) { num++; } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlockClass obj1 = new SynchronizedBlockClass(); SynchronizedBlockClass obj2 = new SynchronizedBlockClass(); Thread t1 = new Thread(obj1::add); Thread t2 = new Thread(obj2::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + num); // 2000(预期) }
案例3:锁定自定义对象(常用于多线程共享资源的同步)
public class SynchronizedBlockCustom { private int num = 0; // 自定义锁定对象(必须保证多线程使用的是同一个对象) private final Object lock = new Object(); public void add() { // 锁定自定义对象 lock synchronized (lock) { for (int i = 0; i < 1000; i++) { num++; } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlockCustom obj = new SynchronizedBlockCustom(); Thread t1 = new Thread(obj::add); Thread t2 = new Thread(obj::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("num:" + obj.num); // 2000(预期) }

「注意」:自定义锁定对象建议用 final 修饰,避免对象被修改(如果 lock 被重新赋值,会导致多个线程锁定不同对象,失去同步效果)。

三种用法的核心区别(面试高频)

用法

锁定对象

同步范围

适用场景

修饰实例方法

this(当前对象实例)

整个实例方法

实例变量的同步(多个线程操作同一个对象的实例变量)

修饰静态方法

当前类的 Class 对象

整个静态方法

静态变量的同步(多个线程操作同一个类的静态变量)

修饰代码块

手动指定(this、Class、自定义对象)

代码块内的代码

灵活控制同步粒度(只同步需要的代码,提升效率)

三、进阶:synchronized 的核心特性(理解这些,才算入门)

synchronized 能保证线程安全,核心依赖于它的三个特性:原子性、可见性、有序性,除此之外,它还是「可重入锁」、「非公平锁」,这些特性是面试的核心考点,必须逐一搞懂。

3.1 原子性(核心特性)

「原子性」:指一个操作或多个操作,要么全部执行完成,要么全部不执行,中间不会被其他线程打断。就像“银行转账”,从A账户扣钱、给B账户加钱,这两个操作必须同时完成,不能只执行一半。

synchronized 能保证「被锁定的代码块/方法」的原子性:同一时刻只有一个线程能进入被锁定的代码,避免了多线程同时执行导致的操作中断,从而保证了原子性。

「补充」:Java 中还有一些原子类(如 AtomicInteger),也能保证原子性,但底层是 CAS 机制,和 synchronized 的实现方式不同,后续会单独写博客拆解。

3.2 可见性(核心特性)

「可见性」:指当一个线程修改了共享变量的值后,其他线程能立即看到这个修改后的值。

为什么会有“不可见性”?因为每个线程都有自己的「工作内存」,线程读取共享变量时,会先从主内存中拷贝一份到工作内存,后续操作都基于工作内存中的副本;当线程修改了共享变量后,会先更新工作内存中的副本,再异步刷新到主内存中。如果其他线程此时读取共享变量,可能读取的还是主内存中未被更新的值,导致数据错乱。

synchronized 能保证可见性:当线程释放锁时,会将工作内存中修改后的共享变量值「强制刷新到主内存」;当线程获取锁时,会「清空工作内存中的共享变量副本」,重新从主内存中读取最新的值。这样就保证了多个线程看到的共享变量值是一致的。

3.3 有序性(核心特性)

「有序性」:指程序执行的顺序按照代码的先后顺序执行。但在 JVM 中,为了提升性能,会对代码进行「指令重排序」(在不影响单线程执行结果的前提下,调整指令的执行顺序)。

指令重排序在单线程环境下没问题,但在多线程环境下,可能会导致程序执行结果异常。例如:

// 共享变量 private static int a = 0; private static boolean flag = false; // 线程1执行 public static void thread1() { a = 1; // 指令1 flag = true; // 指令2 } // 线程2执行 public static void thread2() { if (flag) { // 指令3 System.out.println(a); // 可能输出 0,而非 1 } }

「原因」:JVM 可能会将线程1的指令1和指令2重排序(因为单线程下,先执行 flag=true 再执行 a=1,结果不变),如果线程1先执行 flag=true,线程2此时读取 flag 为 true,就会打印 a 的值(此时 a 还未被赋值为1,还是0),导致结果异常。

synchronized 能保证有序性:被 synchronized 锁定的代码块,会禁止指令重排序,保证代码按照编写顺序执行。同时,synchronized 还能保证“happens-before”关系(后续单独拆解),进一步保证多线程环境下的有序性。

3.4 可重入性(重要特性)

「可重入性」:指一个线程已经获取了某个锁,当它再次需要获取该锁时(比如递归调用被锁定的方法),可以直接获取,不会导致死锁。

synchronized 是可重入锁,我们用递归案例验证:

public class SynchronizedReentrant { // 锁定 this public synchronized void methodA() { System.out.println("进入 methodA"); methodB(); // 递归调用 methodB(同样锁定 this) } // 同样锁定 this public synchronized void methodB() { System.out.println("进入 methodB"); } public static void main(String[] args) { SynchronizedReentrant obj = new SynchronizedReentrant(); obj.methodA(); // 正常执行,不会死锁 } }

「执行结果」:

进入 methodA 进入 methodB

「说明」:线程调用 methodA 时,获取了 this 锁;在 methodA 中调用 methodB 时,因为 methodB 锁定的也是 this 锁,而线程已经持有该锁,所以可以直接进入 methodB,不会死锁。这就是可重入性的体现。

「补充」:synchronized 的可重入性,底层是通过「锁计数器」实现的:线程第一次获取锁时,计数器置为1;再次获取该锁时,计数器加1;释放锁时,计数器减1;当计数器为0时,锁才会被真正释放,其他线程才能获取。

3.5 非公平性(重要特性)

「公平锁」:多个线程等待同一个锁时,按照“先到先得”的顺序获取锁,不会出现线程饥饿(某个线程一直得不到锁)。

「非公平锁」:多个线程等待同一个锁时,线程获取锁的顺序是随机的,不一定是先到先得,可能存在某个线程多次获取锁,而其他线程长期等待的情况。

synchronized 是「非公平锁」:当锁被释放时,等待队列中的线程不会按照排队顺序获取锁,而是随机竞争,这样做的目的是「提升程序性能」(避免了线程排队的开销)。

「补充」:Java 中的 ReentrantLock 可以实现公平锁和非公平锁(默认非公平),后续会对比 synchronized 和 ReentrantLock 的区别。

四、底层原理:synchronized 是如何实现的?(面试难点)

前面我们讲了 synchronized 的用法和特性,接下来拆解最核心的底层原理 —— 它到底是靠什么实现“锁定对象、保证线程安全”的?

synchronized 的底层实现依赖于 JVM 的「监视器锁(Monitor)」,而 Monitor 的实现又和「对象头」、「字节码指令」密切相关。我们从“字节码层面”和“JVM 层面”两个维度拆解。

4.1 字节码层面:synchronized 的指令体现

我们先看一段简单的代码,反编译它的字节码,看看 synchronized 在字节码层面是如何表现的。

public class SynchronizedBytecode { private final Object lock = new Object(); public void test() { synchronized (lock) { System.out.println("synchronized 代码块"); } } }

使用 javac 编译后,再用 javap -v 反编译,查看 test 方法的字节码(核心部分):

public void test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: getfield #2 // Field lock:Ljava/lang/Object; 4: dup 5: astore_1 6: monitorenter // 进入同步代码块,获取锁 7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 10: ldc #4 // String synchronized 代码块 12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 15: aload_1 16: monitorexit // 退出同步代码块,释放锁 17: goto 25 20: astore_2 21: aload_1 22: monitorexit // 异常情况下释放锁 23: aload_2 24: athrow 25: return

「核心指令解析」:

  1. monitorenter:当线程执行到 monitorenter 指令时,会尝试获取当前锁定对象的 Monitor 锁。
    1. 如果 Monitor 的计数器为 0(表示锁未被持有),则将计数器置为 1,当前线程成为 Monitor 的所有者;
    2. 如果当前线程已经是 Monitor 的所有者(可重入),则将计数器加 1;
    3. 如果 Monitor 被其他线程持有,则当前线程会阻塞,直到 Monitor 的计数器为 0,再尝试获取锁。
  2. monitorexit:当线程执行到 monitorexit 指令时,会释放 Monitor 锁,将 Monitor 的计数器减 1。当计数器减为 0 时,Monitor 会被释放,阻塞的线程可以尝试获取锁。

「注意」:字节码中会有两个 monitorexit 指令,第一个用于正常退出同步代码块时释放锁,第二个用于异常情况下释放锁(避免锁泄露)。

「补充」:如果是 synchronized 修饰方法(实例方法/静态方法),字节码层面不会出现 monitorenter 和 monitorexit 指令,而是会在方法的访问标志中增加 ACC_SYNCHRONIZED 标志,JVM 会根据该标志自动为方法添加 Monitor 的获取和释放逻辑(本质和代码块一致)。

4.2 JVM 层面:Monitor 锁与对象头

前面提到,synchronized 锁定的是“对象”,而 Monitor 锁是和“对象”绑定的 —— 每个 Java 对象在 JVM 中都有一个「对象头」,对象头中包含了「Monitor 锁的相关信息」,这也是 synchronized 能锁定对象的核心原因。

4.2.1 Java 对象头结构

Java 对象头由两部分组成(32位 JVM 为例):

  1. Mark Word(标记字,32位):存储对象的哈希码、分代年龄、锁状态标志、持有锁的线程 ID 等信息(核心部分);
  2. Klass Pointer(类型指针,32位):指向对象所属类的 Class 对象,JVM 通过该指针确定对象的类型。

其中,Mark Word 的结构会随着「锁状态」的变化而变化,synchronized 的锁升级机制也和 Mark Word 密切相关。Mark Word 的默认结构(无锁状态)如下:

位偏移

内容

说明

0-1位

锁状态标志

01:无锁状态;00:轻量级锁;10:重量级锁;11:偏向锁

2位

是否偏向锁

0:无偏向;1:偏向锁

3-23位

对象的哈希码

无锁/偏向锁状态下存储

24-30位

对象的分代年龄

GC 回收时使用

31位

无意义(固定为0)

-

4.2.2 Monitor 锁的本质

Monitor 是 JVM 内部的一个数据结构,也叫「监视器」,每个对象都有一个对应的 Monitor(可以理解为“锁的载体”)。Monitor 的结构如下(简化版):

  • Owner:存储当前持有 Monitor 的线程 ID,只有一个线程能成为 Owner;
  • EntryList:存储等待获取 Monitor 锁的线程队列(阻塞状态);
  • WaitSet:存储调用 wait() 方法后释放锁、进入等待状态的线程队列。

「Monitor 工作流程」:

  1. 线程执行 monitorenter 指令时,尝试获取 Monitor 的 Owner 权限:
    1. 如果 Owner 为空,当前线程成为 Owner,Monitor 计数器置为 1;
    2. 如果当前线程已是 Owner,计数器加 1(可重入);
    3. 如果 Owner 是其他线程,当前线程进入 EntryList 队列,阻塞等待。
  2. 线程执行 monitorexit 指令时,Monitor 计数器减 1:
    1. 如果计数器减为 0,释放 Owner 权限(置为 null),唤醒 EntryList 中一个阻塞的线程,让其尝试获取锁;
    2. 如果计数器不为 0(可重入场景),则当前线程仍持有 Owner 权限。

4.3 锁升级机制(JDK 1.8 优化,重点面试考点)

在 JDK 1.6 之前,synchronized 是「重量级锁」—— 每次获取和释放锁都需要调用操作系统的互斥量(Mutex),切换线程状态(用户态 ↔ 内核态),开销很大,效率很低。

为了优化 synchronized 的性能,JDK 1.6 及以后引入了「锁升级机制」:synchronized 的锁会从「无锁」→「偏向锁」→「轻量级锁」→「重量级锁」逐步升级,根据线程竞争的激烈程度动态调整,减少锁的开销。

锁升级的核心依据:「线程竞争的激烈程度」—— 竞争越激烈,锁的级别越高,开销也越大,但能保证线程安全。

4.3.1 1. 无锁状态

「场景」:没有线程竞争共享资源,对象处于无锁状态。

「Mark Word 特征」:锁状态标志为 01,是否偏向锁为 0,存储对象的哈希码和分代年龄。

4.3.2 2. 偏向锁(减少无竞争下的锁开销)

「场景」:只有一个线程多次获取同一个锁,没有其他线程竞争。

「核心思想」:既然只有一个线程获取锁,就不需要每次都执行 monitorenter/monitorexit 指令(避免内核态切换),而是将锁“偏向”于这个线程,后续该线程再次获取锁时,直接通过 Mark Word 验证即可,无需竞争。

「升级过程」:

  1. 线程第一次获取锁时,将 Mark Word 中的「是否偏向锁」设为 1,「锁状态标志」设为 11,同时存储当前线程的 ID;
  2. 该线程后续再次获取锁时,检查 Mark Word 中的线程 ID 是否为当前线程 ID:
    1. 如果是,直接获取锁,无需其他操作;
    2. 如果不是,说明有其他线程竞争,偏向锁升级为轻量级锁。

「补充」:偏向锁是 JDK 1.6 默认开启的,可通过 JVM 参数 -XX:-UseBiasedLocking 关闭。

4.3.3 3. 轻量级锁(减少多线程竞争下的阻塞开销)

「场景」:有多个线程竞争同一个锁,但竞争不激烈(线程交替获取锁,没有长时间阻塞)。

「核心思想」:当偏向锁被打破(有其他线程竞争),锁升级为轻量级锁,此时线程获取锁时,不再使用 Monitor(避免内核态切换),而是通过「CAS 机制」竞争锁。

「升级过程」:

  1. 线程获取锁时,JVM 会在当前线程的栈帧中创建一个「锁记录(Lock Record)」,存储锁定对象的 Mark Word 副本;
  2. 线程通过 CAS 操作,将锁定对象的 Mark Word 替换为「指向当前线程锁记录的指针」:
    1. 如果 CAS 成功,说明当前线程获取到锁,Mark Word 锁状态标志设为 00;
    2. 如果 CAS 失败,说明有其他线程正在竞争锁,此时会自旋(循环尝试 CAS)几次,如果自旋成功,仍能获取锁;如果自旋失败,说明竞争激烈,轻量级锁升级为重量级锁。

「补充」:自旋锁的目的是避免线程阻塞(内核态切换开销大),适合竞争不激烈的场景;如果竞争激烈,自旋会浪费 CPU 资源,此时升级为重量级锁更合适。

4.3.4 4. 重量级锁(最终锁形态)

「场景」:多个线程激烈竞争同一个锁,自旋多次仍无法获取锁。

「核心思想」:重量级锁依赖于 JVM 中的 Monitor 和操作系统的互斥量(Mutex),线程获取不到锁时,会进入阻塞状态(放弃 CPU 资源),直到锁被释放后被唤醒。

「Mark Word 特征」:锁状态标志为 10,存储指向 Monitor 的指针,此时线程获取锁会触发内核态切换,开销最大。

锁升级总结(表格梳理)

锁状态

适用场景

实现方式

开销

无锁

无线程竞争

无锁机制

偏向锁

单线程重复获取锁

Mark Word 存储线程 ID

极低

轻量级锁

多线程交替竞争,竞争不激烈

CAS 机制 + 自旋

较低

重量级锁

多线程激烈竞争

Monitor + 操作系统互斥量

较高

六、面试高频问题

结合前面的内容,整理了 synchronized 最常考的面试题,每个问题都给出简洁、精准的答案(适合面试时直接回答)。

Q1:synchronized 的作用是什么?

答:保证多线程环境下共享资源的原子性、可见性和有序性,避免数据错乱,实现线程安全。

Q2:synchronized 有几种用法?分别锁定什么对象?

答:三种用法:

  1. 修饰实例方法:锁定当前对象 this;
  2. 修饰静态方法:锁定当前类的 Class 对象;
  3. 修饰代码块:锁定括号内手动指定的对象(this、Class 对象、自定义对象)。

Q3:synchronized 是公平锁还是非公平锁?

答:非公平锁。当锁被释放时,等待队列中的线程不会按照排队顺序获取锁,而是随机竞争,目的是提升程序性能。

Q4:synchronized 是可重入锁吗?什么是可重入性?

答:是可重入锁。可重入性指一个线程已经获取了某个锁,当它再次需要获取该锁时(如递归调用),可以直接获取,不会导致死锁。底层通过锁计数器实现。

Q5:synchronized 如何保证原子性、可见性、有序性?

答:

  • 原子性:同一时刻只有一个线程能进入被锁定的代码,避免操作中断;
  • 可见性:释放锁时强制刷新工作内存到主内存,获取锁时清空工作内存,重新读取主内存;
  • 有序性:禁止指令重排序,保证代码按编写顺序执行。

Q6:JDK 1.6 对 synchronized 做了哪些优化?

答:引入了「锁升级机制」,从无锁 → 偏向锁 → 轻量级锁 → 重量级锁逐步升级,根据线程竞争激烈程度动态调整锁的级别,减少锁的开销;同时引入了自旋锁、偏向锁等优化,提升了 synchronized 的性能。

Q7:synchronized 和 volatile 的区别?(高频中的高频)

答:核心区别:volatile 只能保证可见性和有序性,不能保证原子性;synchronized 能保证原子性、可见性和有序性。

特性

synchronized

volatile

原子性

支持

不支持

可见性

支持

支持

有序性

支持

支持(禁止指令重排序)

锁机制

独占锁(可重入、非公平)

无锁机制

Q8:synchronized 和 ReentrantLock 的区别?

答:

  • 锁的实现:synchronized 是 JVM 内置锁,ReentrantLock 是 Java 代码实现的锁;
  • 公平性:synchronized 只能是非公平锁,ReentrantLock 可指定公平/非公平锁;
  • 灵活性:ReentrantLock 支持更多功能(如中断锁、超时锁、条件变量),synchronized 更简洁;
  • 性能:JDK 1.8 后两者性能差距不大,synchronized 更易维护,ReentrantLock 灵活性更高。

七、总结

synchronized 是 Java 并发编程的基础,也是面试必考的知识点,从入门到底层,核心要点可以总结为:

  1. 「用法」:三种用法(实例方法、静态方法、代码块),锁定对象不同,同步范围不同;
  2. 「特性」:原子性、可见性、有序性、可重入性、非公平性;
  3. 「底层」:依赖 JVM 的 Monitor 锁,通过 monitorenter/monitorexit 指令和对象头实现;
  4. 「优化」:JDK 1.8 锁升级机制(无锁→偏向锁→轻量级锁→重量级锁),减少开销;
  5. 「避坑」:锁定同一对象、避免可变对象、缩小锁粒度

Read more

Flutter for OpenHarmony: Flutter 三方库 ntp 精准同步鸿蒙设备系统时间(分布式协同授时利器)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 分布式开发、金融交易或具有严格时效性的业务(如:秒杀倒计时、双因素认证 OTP)时,开发者不能完全信任设备本地的系统时间。用户可能为了某种目的手动篡改时间,或者由于网络同步问题导致时间存在偏差。 ntp 软件包提供了一种直接与互联网授时中心(NTP 服务器)通信的能力。它能绕过本地系统时钟,获取绝对精准的 UTC 时间,并计算出本地时间与真实时间的“偏移量(Offset)”。 一、核心授时原理 ntp 通过测量往返网络延迟来消除误差。 发送 NTP 请求 (UDP) 返回高精度时间戳 鸿蒙 App 全球授时中枢 (pool.ntp.org) 计算网络往返耗时 (RTT) 得出绝对时间偏移量 生成鸿蒙业务专用准时 二、

By Ne0inhk
SkyWalking - Spring Cloud Alibaba 全链路追踪实战

SkyWalking - Spring Cloud Alibaba 全链路追踪实战

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕SkyWalking这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * SkyWalking - Spring Cloud Alibaba 全链路追踪实战 🚀 * 1. 环境准备与核心概念 🧰 * 1.1 核心概念解析 * 1.2 环境准备 * 2. 构建 Spring Cloud Alibaba 微服务项目 🏗️ * 2.1 创建父工程 * 2.2 构建 `inventory-service`(库存服务) * 2.3 构建 `order-service`(订单服务) * 2.4 验证基础功能 * 3. 集成

By Ne0inhk

6个开源视觉模型推荐:M2FP支持WebUI交互,调试更高效

6个开源视觉模型推荐:M2FP支持WebUI交互,调试更高效 在计算机视觉领域,人体解析(Human Parsing)作为语义分割的精细化分支,正广泛应用于虚拟试衣、动作识别、智能安防和人机交互等场景。传统方案多聚焦单人解析,面对多人重叠、遮挡或复杂背景时表现不稳定。本文将重点介绍基于 ModelScope 的 M2FP 多人人体解析服务,并延伸推荐5个功能互补的开源视觉模型,构建从开发到部署的完整技术生态。 🧩 M2FP 多人人体解析服务 (WebUI + API) 📖 项目简介 本镜像基于 ModelScope 平台的 M2FP (Mask2Former-Parsing) 模型构建,专为多人人体解析任务优化。M2FP 融合了 Mask2Former 的 Transformer 解码架构与人体解析领域的先验知识,在 LIP 和 CIHP 等权威数据集上达到 SOTA(State-of-the-Art)性能。 该模型能够对图像中多个个体进行像素级语义分割,精确区分多达 18

By Ne0inhk

前端大文件分片上传实现与断点续传方案(含完整代码讲解)

在上传大文件(如视频、安装包、模型文件)时,直接上传容易出现以下问题: * 文件过大 → 浏览器/服务器容易超时 * 上传过程中断 → 重新上传浪费时间 * 网络波动 → 上传失败率高 因此,大文件分片上传 + 断点续传 + 秒传校验 是目前最通用、最稳定的解决方案。 本文将通过一段完整可运行的示例代码,详细讲解如何在前端实现分片上传、断点续传、服务端校验等关键功能。 ✨ 实现效果 * ✔ 自动切片(默认 5MB/片,可配置) * ✔ 查询已上传分片(断点续传) * ✔ 自动跳过已上传的片段 * ✔ 每片上传成功后重新校验 * ✔ 所有片段上传完成后自动触发合并 * ✔ 错误处理完善 📌 核心代码(uploadLargeFile) 以下代码就是本文的核心逻辑,也是你提供的代码版本,经过梳理解释后会更易理解: export async function uploadLargeFile({ file, fileId, id, chunkSize = 5 * 1024

By Ne0inhk