深入剖析synchronized
一、引言
在 Java 的多线程编程领域中,synchronized 关键字无疑占据着举足轻重的地位,堪称保障线程安全的基石之一。随着软件系统日益复杂,多线程环境下的并发问题愈发凸显,而 synchronized 正是解决这些问题的关键利器。
在多线程同时访问和修改共享资源时,倘若缺乏有效的同步机制,数据一致性将难以保障,线程安全问题也会接踵而至,诸如竞态条件、数据不一致、内存可见性问题等,都可能让程序出现难以调试和定位的错误。例如,在一个银行账户转账的场景中,多个线程同时对账户余额进行操作,如果没有同步控制,就可能导致账户余额出现错误的结果,给用户和银行都带来损失。
synchronized 的核心价值在于,它能够确保同一时刻仅有一个线程能够进入被其修饰的代码块或方法,从而实现对共享资源的互斥访问,有效避免了多线程竞争共享资源所引发的一系列问题。这就好比为共享资源上了一把锁,只有获得这把锁的线程才能对资源进行操作,操作完成后释放锁,其他线程才有机会获取锁并进行操作 ,以此保障数据的一致性和线程的安全性。
接下来,本文将从 synchronized 的用法、原理、特性、与其他并发工具的比较以及优化建议等多个维度,对其展开深度剖析,带领读者全面深入地理解这一重要的 Java 并发编程工具,掌握其在实际项目中的高效运用。
二、synchronized 基础入门
(一)定义与作用
synchronized 是 Java 语言中的一个关键字,它就像是一把神奇的 “锁”,主要用于解决多线程编程中的线程安全问题 。在多线程环境下,当多个线程同时访问和修改共享资源时,数据一致性很容易遭到破坏,进而引发各种难以调试和定位的错误。而 synchronized 的关键作用在于,它能够保证在同一时刻,仅有一个线程可以执行被其修饰的代码块或方法 ,实现对共享资源的互斥访问。
例如,在一个银行账户的多线程操作场景中,若多个线程同时对账户余额进行取款或存款操作,如果没有合适的同步机制,账户余额很可能会出现错误的结果。使用 synchronized 关键字,就可以确保在同一时刻只有一个线程能够对账户余额进行操作,从而避免数据不一致的问题,保障了账户操作的准确性和安全性 。
(二)使用场景
多线程对共享变量的读写操作:当多个线程需要对同一个变量进行读写操作时,为了保证数据的一致性,就可以使用 synchronized 关键字。比如经典的计数器场景:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
在上述代码中,increment
方法和getCount
方法都被synchronized
修饰。这意味着,当多个线程同时访问Counter
对象的这两个方法时,同一时刻只有一个线程能够进入方法执行操作。假设线程 A 正在执行increment
方法对count
进行自增操作,此时线程 B 如果也想执行increment
方法或者getCount
方法,就必须等待线程 A 执行完相应方法并释放锁之后,才能获得执行权限。这样就能有效避免由于多线程并发访问导致的count
变量数据不一致问题。
多线程访问共享资源的方法:若多个线程需要访问同一个对象的共享资源方法,为防止资源竞争和数据不一致,可使用 synchronized 进行同步。以一个简单的文件写入操作为例:
public class FileWriterUtil {
private final File file;
public FileWriterUtil(File file) {
this.file = file;
}
public synchronized void writeToFile(String content) {
try (FileWriter writer = new FileWriter(file, true)) {
writer.write(content);
writer.write("\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,FileWriterUtil
类的writeToFile
方法被synchronized
修饰。当有多个线程尝试调用这个方法向同一个文件写入内容时,同一时刻只会有一个线程能够成功进入方法执行写入操作,其他线程需要等待。这样就避免了多个线程同时写入文件时可能出现的内容覆盖、数据错乱等问题,保证了文件写入操作的正确性和完整性。
三、synchronized 的使用方式
(一)修饰实例方法
当 synchronized 关键字用于修饰实例方法时,它所锁定的是当前实例对象,即该方法的调用者。这意味着,同一时刻,对于同一个实例对象,只有一个线程能够进入并执行这个被修饰的实例方法,其他试图进入该方法的线程将会被阻塞,直到当前持有锁的线程执行完毕并释放锁。
下面通过一个具体的代码示例来深入理解:
public class SynchronizedInstanceMethodExample {
private int count = 0;
// 被synchronized修饰的实例方法
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
}
public static void main(String[] args) {
SynchronizedInstanceMethodExample example = new SynchronizedInstanceMethodExample();
// 创建并启动三个线程
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment()
}
}, "Thread-2");
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 5; i++)
example.increment();
}
}, "Thread-3");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
// 等待所有线程执行完毕
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,increment
方法被synchronized
修饰。当thread1
开始执行increment
方法时,它会获取当前example
实例对象的锁。在thread1
持有锁的期间,thread2
和thread3
如果尝试调用example
的increment
方法,由于无法获取到锁,就会被阻塞。只有当thread1
执行完increment
方法并释放锁后,thread2
或thread3
中的一个线程才有机会获取锁并执行increment
方法 。如此循环,保证了在多线程环境下,count
变量的自增操作是线程安全的,不会出现数据不一致的情况。
(二)修饰静态方法
当 synchronized 关键字用于修饰静态方法时,它所锁定的是当前类的 Class 对象。由于一个类在整个 JVM 中只有一个 Class 对象,所以这就意味着,无论通过哪个实例对象去调用该静态方法,同一时刻都只有一个线程能够执行该方法。这对于多个实例对象需要共享访问的静态资源或执行静态操作时,提供了有效的同步控制机制。
下面通过一个示例代码来详细说明:
public class SynchronizedStaticMethodExample {
private static int count = 0;
// 被synchronized修饰的静态方法
public static synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
}
public static void main(String[] args) {
// 创建两个不同的实例对象
SynchronizedStaticMethodExample example1 = new SynchronizedStaticMethodExample();
SynchronizedStaticMethodExample example2 = new SynchronizedStaticMethodExample();
// 创建并启动三个线程
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
SynchronizedStaticMethodExample.increment();
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example1.increment();
}
}, "Thread-2");
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example2.increment();
}
}, "Thread-3");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
// 等待所有线程执行完毕
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,increment
是一个被synchronized
修饰的静态方法。尽管创建了example1
和example2
两个不同的实例对象,并且thread1
通过类名调用increment
方法,thread2
通过example1
调用,thread3
通过example2
调用,但由于锁定的是类的 Class 对象,所以同一时刻只有一个线程能够进入并执行increment
方法。比如当thread1
获取到类的 Class 对象的锁并执行increment
方法时,thread2
和thread3
会被阻塞,直到thread1
释放锁,以此确保了多线程环境下对静态变量count
操作的线程安全性 。
(三)修饰代码块
synchronized 关键字还可以用于修饰代码块,这种方式允许我们更加精细地控制同步的范围。在修饰代码块时,需要在synchronized
关键字后面的括号中指定一个对象作为锁,这个对象被称为监视器对象(Monitor Object)。同一时刻,只有一个线程能够获取到这个监视器对象的锁,从而进入并执行被该锁保护的代码块,其他线程则需要等待锁的释放。
通过以下示例代码来深入理解:
public class SynchronizedBlockExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 同步代码块,使用lock对象作为锁
synchronized (lock) {
count++;
System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
}
}
public static void main(Strin[] args) {
SynchronizedBlockExample example = new SynchronizedBlockExample();
// 创建并启动三个线程
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
}, "Thread-2");
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}, "Thread-3");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
// 等待所有线程执行完毕
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,increment
方法中的代码块被synchronized
修饰,并指定了lock
对象作为锁。当thread1
进入同步代码块时,它会获取lock
对象的锁。在thread1
持有锁期间,thread2
和thread3
如果尝试进入相同的同步代码块,由于无法获取到lock
对象的锁,就会被阻塞。只有当thread1
执行完同步代码块并释放锁后,thread2
或thread3
中的一个线程才有机会获取锁并执行同步代码块中的代码,从而保证了count
变量自增操作在多线程环境下的线程安全性 。
值得注意的是,在选择锁对象时,要确保该对象对于需要同步的线程是唯一且可共享的。例如,如果在不同的地方创建了多个不同的锁对象,那么这些锁对象之间是相互独立的,无法实现有效的同步。同时,使用this
作为锁对象时,要特别小心,因为this
代表的是当前实例对象,如果在多线程环境下存在多个实例对象,那么每个实例对象的this
是不同的,也就无法实现跨实例的同步。此外,还可以使用类的 Class 对象作为锁对象,这种方式类似于修饰静态方法时的锁机制,适用于多个实例对象需要共享访问的静态资源或执行静态操作时的同步控制 。
四、synchronized 的原理剖析
(一)JVM 层面实现
在 JVM 层面,synchronized 主要通过monitorenter
和monitorexit
指令来实现同步功能 。当线程执行到synchronized
修饰的代码块时,首先会执行monitorenter
指令,该指令会尝试获取对象的监视器(Monitor)锁。如果对象的监视器锁尚未被其他线程持有,当前线程就能成功获取锁,并继续执行代码块中的内容 ;若对象的监视器锁已被其他线程持有,当前线程则会被阻塞,进入等待队列,直到持有锁的线程执行monitorexit
指令释放锁后,当前线程才有机会重新竞争并获取锁 。
以如下代码为例:
public class SynchronizedJVMExample {
private final Object lock = new Object();
public void synchronizedMethod()
synchronized (lock) {
// 同步代码块
System.out.println(Thread.currentThread().getName() + " is executing synchronized code.");
}
}
使用javap -c
命令对上述代码进行反编译,可以看到在同步代码块的入口和出口处分别对应monitorenter
和monitorexit
指令:
public void synchronizedMethod();
Code:
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Thread is executing synchronized code.
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_2
13: monitorexit
14: goto 22
17: astore_3
18: aload_2
19: monitorexit
20: aload_3
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
从反编译结果中可以清晰地看到,在同步代码块开始处(第 3 行)执行monitorenter
指令,在正常结束处(第 13 行)和异常结束处(第 19 行)都会执行monitorexit
指令,以确保无论代码块是正常执行完毕还是发生异常,锁都能被正确释放 。
(二)对象头与 Monitor
在 Java 中,每个对象都有一个对象头(Object Header),它包含了一些关于对象的元数据信息,其中 Mark Word 部分用于记录对象的锁状态等重要信息 。以 32 位 JVM 为例,Mark Word 通常占用 4 个字节,其结构如下:
当对象处于不同的锁状态时,Mark Word 中的内容会相应变化,以此来标识对象的当前锁状态 。
Monitor(监视器)是 synchronized 实现同步的关键底层数据结构,每个 Java 对象都可以关联一个 Monitor 对象 。当线程通过monitorenter
指令尝试获取锁时,实际上就是在竞争对象关联的 Monitor 锁 。Monitor 主要由以下几个部分组成:
Owner:指向当前持有锁的线程,如果当前没有线程持有锁,则 Owner 为 null 。
EntryList:一个双向链表,用于存放等待获取锁的线程 。当多个线程同时尝试获取同一对象的锁时,未能获取到锁的线程会被加入到 EntryList 中,进入阻塞状态,等待锁的释放 。
WaitSet:同样是一个双向链表,用于存放调用了对象的wait()
方法后进入等待状态的线程 。当线程调用wait()
方法时,它会释放当前持有的锁,并进入 WaitSet 等待,直到被其他线程调用notify()
或notifyAll()
方法唤醒 。
其工作原理如下:当一个线程尝试获取对象的锁时,如果对象的 Monitor 的 Owner 为 null,说明当前没有线程持有锁,该线程可以成功获取锁,并将 Owner 设置为自己;如果 Owner 不为 null,说明锁已被其他线程持有,当前线程会被加入到 EntryList 中等待 。当持有锁的线程执行完同步代码块,通过monitorexit
指令释放锁时,会将 Owner 设置为 null,并从 EntryList 中唤醒一个线程来竞争锁,竞争成功的线程将成为新的锁持有者 。如果线程在持有锁期间调用了wait()
方法,它会释放锁并进入 WaitSet 等待,当被唤醒后,会重新进入 EntryList 竞争锁 。
(三)锁升级机制
从 Java 6 开始,为了提高 synchronized 在不同并发场景下的性能,JVM 引入了锁升级机制,它会根据实际的竞争情况,自动将锁从低级别逐步升级到高级别,以适应不同的并发环境,但是锁只能升级不能降级 。
偏向锁
概念:偏向锁是一种针对单线程访问场景的优化锁机制 。它的核心思想是,如果一个对象在运行过程中总是被同一个线程访问,那么可以将锁偏向于这个线程,避免每次获取锁时的竞争开销 。
适用场景:适用于只有一个线程频繁访问同步代码块的场景,例如某些单线程环境下的缓存操作,或者局部变量在单线程中的同步访问等 。
锁获取流程:当一个线程首次访问被synchronized
修饰的代码块时,JVM 会检查对象的 Mark Word 中是否已经偏向了其他线程 。如果没有(即是否偏向锁标志为 0 且锁标志位为 01,处于无锁状态),JVM 会通过 CAS(Compare-And-Swap)操作将线程 ID 记录到 Mark Word 中,将锁偏向当前线程,并将是否偏向锁标志设置为 1,此时锁进入偏向锁状态 。后续该线程再次访问同步代码块时,只需检查 Mark Word 中的线程 ID 是否与自己一致,若一致则直接进入同步代码块,无需进行额外的锁获取操作 。
锁释放流程:偏向锁不会主动释放,只有当有其他线程尝试获取锁时,才会触发偏向锁的撤销操作 。JVM 会暂停拥有偏向锁的线程,将 Mark Word 恢复到无锁状态或升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码 。
轻量级锁
适用场景:适用于多个线程偶尔竞争同一对象锁的场景,例如多个线程交替访问共享资源,但竞争并不激烈,且同步代码块执行时间较短 。
加锁过程:当线程执行同步块时,如果对象处于无锁状态,JVM 会在当前线程的栈帧中创建一个名为 Lock Record 的空间,用于存储当前对象 Mark Word 的拷贝,称为 “Displaced Mark Word” 。然后,线程尝试使用 CAS 操作将对象头中的 Mark Word 替换为指向 Lock Record 的指针 。如果替换成功,当前线程获得轻量级锁,锁标志位设置为 00,进入轻量级锁状态;如果替换失败,说明可能存在竞争,JVM 会检查对象头中的 Mark Word 是否指向当前线程的栈帧,如果是,则表示这是一次锁重入,直接进入同步代码块;否则,说明有其他线程竞争锁,轻量级锁会膨胀为重量级锁 。
解锁过程:轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头 。如果成功,则表示没有竞争发生,锁释放;如果失败,表示当前锁存在竞争,锁会膨胀成重量级锁 。
自旋操作意义:在轻量级锁竞争过程中,如果发现锁已被其他线程持有,当前线程不会立即阻塞,而是会进行自旋操作 。自旋是指线程在一个短时间内的循环中不断尝试获取锁,希望在持有锁的线程释放锁后能立即获取到锁 。自旋操作可以避免线程频繁的上下文切换,因为线程阻塞和唤醒涉及到操作系统的线程调度,开销较大 。如果自旋成功获取到锁,就可以避免线程进入阻塞状态,提高了程序的响应速度 。但如果自旋时间过长,会浪费 CPU 资源,因此自旋次数通常会有一个限制,当超过限制仍未获取到锁时,锁就会升级为重量级锁,线程进入阻塞状态 。
重量级锁
原理:重量级锁是传统的 synchronized 锁实现,基于操作系统级别的线程调度和阻塞 。当多个线程竞争同一对象的锁时,未能获取锁的线程会被阻塞,进入操作系统的等待队列,直到持有锁的线程释放锁 。
线程进入阻塞状态及唤醒机制:当一个线程尝试获取对象的重量级锁时,如果锁已被其他线程持有,当前线程会被阻塞,放入 Monitor 的 EntryList 中,进入等待状态 。此时线程会从用户态切换到内核态,等待操作系统的调度 。当持有锁的线程执行完同步代码块,释放锁时,会从 EntryList 中唤醒一个线程 。被唤醒的线程会重新尝试获取锁,从内核态切换回用户态,如果获取成功,则可以继续执行同步代码块;如果获取失败,可能会再次进入阻塞状态,等待下一次被唤醒和竞争锁的机会 。由于线程的阻塞和唤醒涉及到操作系统的线程调度,会产生较大的开销,因此重量级锁的性能相对较低,适用于竞争激烈的高并发场景 。
五、synchronized 的特性与优缺点
(一)特性
可重入性
可重入性是指同一个线程在外层方法获取锁后,再进入内层方法会自动获取锁(前提是锁的是同一个对象),不会因为之前获取过没有释放导致阻塞。synchronized 是可重入锁,其实现原理是每个锁关联一个线程持有者和一个计数器(这里介绍的是重量级锁,偏向锁只有偏向线程Id不存在加锁解锁,轻量级锁重入会在对应的线程栈中创建多个LockRecord)。当计数器为 0 时,表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法 。当一个线程请求成功后,JVM 会记下持有锁的线程,并将计数器计为 1 。此时其他线程请求该锁,则必须等待 。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增 。当线程退出一个 synchronized 方法 / 块时,计数器会递减,如果计数器为 0 则释放该锁 。
通过以下代码示例来展示 synchronized 的可重入性:
public class ReentrantExample {
public synchronized void outerMethod() {
System.out.println(Thread.currentThread().getName() + " entered outerMethod.");
innerMethod();
}
public synchronized void innerMethod() {
System.out.println(Thread.currentThread().getName() + " entered innerMethod.")
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
Thread thread = new Thread(() -> example.outerMethod(), "Thread-1");
thread.start();
}
}
在上述代码中,outerMethod
和innerMethod
都被synchronized
修饰,且都锁定的是当前实例对象this
。当Thread-1
进入outerMethod
时,它获取了当前实例对象的锁 。在outerMethod
中调用innerMethod
时,由于synchronized
的可重入性,Thread-1
可以直接进入innerMethod
,而无需再次获取锁,这就避免了线程自己锁死自己的情况 。运行上述代码,输出结果如下:
Thread-1 entered outerMethod.
Thread-1 entered innerMethod.
从输出结果可以清晰地看到,同一个线程成功进入了两个被synchronized
修饰的方法,证明了 synchronized 的可重入性 。
不可中断性
不可中断性是指当一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断(即对线程的中断标志没有做处理)。例如:
public class UninterruptibleExample {
private static final Object lock = new Object();
public static void main(Strin[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
try {
Thread.sleep(5000); // 模拟线程持有锁的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " trying to acquire the lock.");
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired the lock after waiting.");
}
}, "Thread-2");
thread1.start();
try {
Thread.sleep(1000); // 确保thread1先获取锁
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
// 尝试中断thread2
thread2.interrupt();
}
}
在上述代码中,thread1
先启动并获取了lock
对象的锁,然后睡眠 5 秒 。在这期间,thread2
启动并尝试获取lock
对象的锁,由于lock
已被thread1
持有,thread2
会进入阻塞状态 。即使在thread2
阻塞期间调用了thread2.interrupt()
方法尝试中断它,thread2
也不会响应中断,直到thread1
释放锁后,thread2
才有机会获取锁并继续执行 。运行上述代码,输出结果如下:
Thread-1 acquired the lock.
Thread-2 trying to acquire the lock.
Thread-1 released the lock.
Thread-2 acquired the lock after waiting.
从输出结果可以看出,thread2
在thread1
释放锁之前一直处于阻塞状态,且没有响应中断,体现了 synchronized 的不可中断性 。
(二)优点
语法简单易用:synchronized 是 Java 语言的关键字,使用起来非常方便,只需在需要同步的方法或代码块前加上synchronized
关键字即可,无需手动进行复杂的锁管理操作,降低了开发难度,提高了开发效率 。例如:
public class SynchronizedSimpleExample {
private int count = 0;
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
}
}
在上述代码中,仅仅通过在increment
方法前添加synchronized
关键字,就轻松实现了对count
变量操作的线程安全,代码简洁明了 。
JVM 自动管理锁释放:使用 synchronized 时,当线程执行完被 synchronized 修饰的方法或代码块,或者在执行过程中发生异常时,JVM 会自动释放线程持有的锁,无需开发者手动编写释放锁的代码,避免了因忘记释放锁而导致的死锁等问题,增强了程序的健壮性 。例如:
public class SynchronizedAutoReleaseExample {
private final Object lock = new Object();
public void autoReleaseMethod() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " entered synchronized block.");
// 模拟业务逻辑
int result = 10 / 0; // 故意抛出异常
System.out.println(Thread.currentThread().getName() + " exiting synchronized block.");
}
}
public static void main(String[] args) {
SynchronizedAutoReleaseExample example = new SynchronizedAutoReleaseExample();
Thread thread = new Thread(example::autoReleaseMethod, "Thread-1");
thread.start();
}
}
在上述代码中,autoReleaseMethod
方法中的同步代码块在执行过程中会抛出ArithmeticException
异常,但由于 synchronized 的自动锁释放机制,即使发生异常,JVM 也会自动释放lock
对象的锁,不会导致死锁 。运行上述代码,输出结果如下:
Thread-1 entered synchronized block.
Exception in thread "Thread-1" java.lang.ArithmeticException: / by zero
at SynchronizedAutoReleaseExample.autoReleaseMethod(SynchronizedAutoReleaseExample.java:10)
at SynchronizedAutoReleaseExample.lambda\$main\$0(SynchronizedAutoReleaseExample.java:19)
at java.lang.Thread.run(Thread.java:748)
从输出结果可以看出,虽然代码抛出了异常,但锁依然被正确释放 。
有效解决多线程并发问题:synchronized 能够保证在同一时刻,只有一个线程可以执行被其修饰的代码块或方法,实现对共享资源的互斥访问,从而有效避免多线程并发访问共享资源时可能出现的数据不一致、竞态条件等问题,确保了程序在多线程环境下的正确性和稳定性 。例如,在一个多线程操作银行账户的场景中:
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public synchronized void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println(Thread.currentThread().getName() + " deposited $" + amount + ". New balance: $" + balance);
}
}
public synchronized boolean withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() + " withdrew $" + amount + ". New balance: $" + balance);
return true;
}
System.out.println(Thread.currentThread().getName() + " failed to withdraw $" + amount + ". Insufficient funds.");
return false;
}
}
在上述代码中,deposit
方法和withdraw
方法都被synchronized
修饰,保证了在多线程环境下对balance
的操作是线程安全的 。多个线程同时进行存款或取款操作时,不会出现数据不一致的情况 。
(三)缺点
无法判断锁获取状态:使用 synchronized 时,线程在尝试获取锁时无法立即知道是否成功 。如果锁已被另一个线程持有,则尝试访问的线程将被阻塞,直至该锁被释放 。这种不确定性可能导致线程在长时间内处于等待状态,从而增加了系统的不确定性和延迟 。例如:
public class CannotJudgeLockStatusExample {
private static final Object lock = new Object();
public static void main(Strin[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
try {
Thread.sleep(5000); // 模拟线程持有锁的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " trying to acquire the lock.");
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired the lock after waiting.");
}
}, "Thread-2");
thread1.start();
try {
Thread.sleep(1000); // 确保thread1先获取锁
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
在上述代码中,thread2
在尝试获取lock
对象的锁时,无法得知自己是否能立即获取到锁,只能进入阻塞状态等待thread1
释放锁,这在一些对响应时间要求较高的场景中可能会带来问题 。
锁竞争激烈时性能较低:当多个线程竞争同一个 synchronized 锁时,会导致线程频繁的上下文切换和阻塞,从而增加系统的开销,降低程序的性能 。特别是在高并发场景下,这种性能下降会更加明显 。例如,在一个高并发的计数器场景中:
public class HighConcurrencyCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
当有大量线程同时调用increment
方法时,由于 synchronized 锁的竞争,线程会频繁地被阻塞和唤醒,导致 CPU 资源的浪费和程序执行效率的降低 。
可能产生死锁:在复杂的多线程环境中,如果使用 synchronized 不当,仍然可能导致死锁问题 。死锁是指两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行 。例如:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " acquired lock1.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " acquired lock2.");
}
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " acquired lock2.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " acquired lock1.");
}
}
}, "Thread-2");
thread1.start();
thread2.start();
}
}
在上述代码中,thread1
先获取lock1
,然后尝试获取lock2
;thread2
先获取lock2
,然后尝试获取lock1
。由于两个线程互相持有对方需要的锁,就会导致死锁的发生,程序将无法继续执行 。运行上述代码,会发现Thread-1
和Thread-2
都停留在获取对方锁的状态,无法继续执行后续代码 。
六、synchronized 与其他锁机制对比
(一)与 ReentrantLock 对比
锁获取方式:synchronized 是隐式锁,当线程进入被其修饰的方法或代码块时,会自动获取锁,退出时自动释放锁,无需手动干预 。例如:
public class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 代码逻辑
}
}
而 ReentrantLock 是显式锁,需要手动调用lock()
方法获取锁,使用完后调用unlock()
方法释放锁 。为了确保锁一定能被释放,通常将unlock()
方法放在finally
块中 。示例如下:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void reentrantLockMethod() {
lock.lock();
try {
// 代码逻辑
} finally {
lock.unlock();
}
}
}
可中断性:synchronized 不支持线程在等待锁的过程中被中断,如果一个线程在等待 synchronized 锁,它只能一直等待,直到获取到锁或者持有锁的线程释放锁 。而 ReentrantLock 提供了lockInterruptibly()
方法,允许线程在等待锁的过程中响应中断 。例如:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockInterruptExample {
private final ReentrantLock lock = new ReentrantLock();
public void interruptibleMethod() throws InterruptedException {
lock.lockInterruptibly();
try {
// 代码逻辑
} finally {
lock.unlock();
}
}
}
在上述代码中,当线程调用lockInterruptibly()
方法获取锁时,如果在等待过程中被其他线程中断,会抛出InterruptedException
异常,从而使线程能够及时响应中断,避免无限期等待 。
公平性:synchronized 是非公平锁,它并不保证等待时间最长的线程会最先获得锁,新来的线程有一定几率在锁可用时直接获取到锁,而不是按照等待顺序获取 。ReentrantLock 默认也是非公平锁,但可以通过构造函数将其设置为公平锁 。例如:
import java.util.concurrent.locks.ReentrantLock;
public class FairReentrantLockExample {
private final ReentrantLock fairLock = new ReentrantLock(true);
public void fairMethod() {
fairLock.lock();
try {
// 代码逻辑
} finally {
fairLock.unlock();
}
}
}
在公平锁模式下,ReentrantLock 会按照线程请求锁的顺序来分配锁,等待时间最长的线程将最先获得锁 ,这在一些对公平性要求较高的场景(如任务队列处理)中非常重要,可以避免线程饥饿问题 。
锁释放:synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生 。而 ReentrantLock 在发生异常时,如果没有在finally
块中主动通过unlock()
去释放锁,则很可能造成死锁现象 。所以使用 ReentrantLock 时必须在finally
块中释放锁,以确保锁一定会被释放 。
功能特性(条件变量):ReentrantLock 提供了更强大的线程间协作功能,它可以通过newCondition()
方法创建多个Condition
对象,每个Condition
对象可以实现不同条件下的线程等待和唤醒 。例如:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void awaitMethod() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void signalMethod() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
而 synchronized 只能通过对象的wait()
、notify()
和notifyAll()
方法来实现线程间的简单协作,且这些方法依赖于对象的监视器锁,功能相对单一 。
适用场景建议:
synchronized 适用场景:当代码逻辑简单,对锁的功能需求较为基础,且希望代码简洁易读时,优先使用 synchronized 。例如,在一些简单的多线程对共享资源的读写操作场景中,synchronized 可以轻松实现线程安全 。
ReentrantLock 适用场景:当需要更灵活的锁控制,如可中断锁、公平锁、多个条件变量等功能时,应选择 ReentrantLock 。在高并发且竞争激烈的场景下,如果对性能有较高要求,也可以考虑使用 ReentrantLock,因为它在某些情况下性能表现优于 synchronized 。例如,在实现一个线程安全的阻塞队列时,ReentrantLock 的多个条件变量功能可以更方便地实现生产者 - 消费者模式 。
(二)与 volatile 对比
原子性:volatile 关键字不保证变量操作的原子性 。例如,对于volatile int count = 0;
,执行count++;
操作看似简单,但实际上它包含了读取count
的值、将值加 1、再将结果写回count
这三个步骤,不是原子操作 。在多线程环境下,可能会出现线程安全问题,比如一个线程读取count
的值为 1,另一个线程也读取到 1,然后两个线程分别加 1 后写回,最终count
的值可能是 2 而不是预期的 3 。而 synchronized 可以保证被其修饰的代码块或方法中的操作是原子性的,同一时刻只有一个线程能执行这些操作 ,从而避免了上述问题 。
可见性:volatile 保证了变量的可见性,当一个线程修改了被 volatile 修饰的变量,会立即将修改后的值刷新到主内存,其他线程在读取该变量时,会直接从主内存中获取最新值,而不是从自己的工作内存中读取旧值 。例如:
public class VolatileVisibilityExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public void checkFlag() {
while (!flag) {
// 等待flag被修改
}
System.out.println("Flag has been set.");
}
}
在上述代码中,当一个线程调用setFlag()
方法修改flag
的值后,其他线程在执行checkFlag()
方法时,能够立即看到flag
的变化,从而避免了因工作内存和主内存数据不一致导致的错误 。synchronized 同样保证了可见性,当线程进入 synchronized 代码块时,会先清空工作内存中该锁对象相关变量的值,从主内存中读取最新值;退出时,会将修改后的变量值刷新回主内存 。但 synchronized 的可见性是基于锁的机制实现的,开销相对较大 。
适用场景:
volatile 适用场景:当只需要保证变量的可见性,且变量的操作是简单的赋值操作(不依赖于当前值),不涉及复合操作时,适合使用 volatile 。例如,在多线程环境中用于控制开关状态的布尔变量,如volatile boolean stop = false;
,一个线程修改stop
的值,其他线程能立即感知到,从而决定是否停止任务 。
synchronized 适用场景:当需要保证操作的原子性和可见性,且操作较为复杂,涉及对共享资源的读写和修改等复合操作时,应使用 synchronized 。例如,在多线程操作银行账户余额时,涉及取款、存款等操作,需要保证这些操作的原子性和数据一致性,synchronized 就能很好地满足需求 。
七、synchronized 使用的注意事项与最佳实践
(一)避免死锁
死锁是多线程编程中一个非常棘手的问题,当两个或多个线程相互等待对方释放锁,导致所有线程都无法继续执行时,就会发生死锁 。死锁一旦发生,程序将陷入无限期的等待,无法正常运行,严重影响系统的稳定性和可用性 。
死锁产生的根本原因在于线程对资源的竞争和不合理的锁获取顺序 。具体来说,死锁的产生需要同时满足以下四个必要条件:
互斥条件:每个资源一次只能被一个线程使用,即资源具有排他性 。例如,一个文件在同一时刻只能被一个线程打开进行写入操作,其他线程必须等待 。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放 。比如线程 A 已经获取了锁 1,在请求锁 2 时被阻塞,但它仍然持有锁 1 不释放 。
不可剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能由该线程自己释放 。例如,线程 B 持有锁 3,其他线程无法直接从线程 B 手中抢走锁 3 。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系 。例如,线程 A 等待线程 B 释放锁 2,线程 B 等待线程 C 释放锁 3,而线程 C 又等待线程 A 释放锁 1,形成了一个循环等待的环路 。
为了避免死锁的发生,可以采取以下方法:
按相同顺序获取锁:在多线程环境中,如果多个线程需要获取多个锁,应确保它们按照相同的顺序获取锁 。例如,假设有两个线程Thread1
和Thread2
,它们都需要获取LockA
和LockB
两把锁,那么可以统一让它们先获取LockA
,再获取LockB
。这样可以避免循环等待条件的出现,从而有效预防死锁 。以下是示例代码:
public class DeadlockAvoidanceByOrder {
private static final Object LockA = new Object();
private static final Object LockB = new Object();
public static void main(String[] args) {
Thread Thread1 = new Thread(() -> {
synchronized (LockA) {
System.out.println(Thread.currentThread().getName() + " acquired LockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LockB) {
System.out.println(Thread.currentThread().getName() + " acquired LockB");
}
}
}, "Thread1");
Thread Thread2 = new Thread(() -> {
synchronized (LockA) {
System.out.println(Thread.currentThread().getName() + " acquired LockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LockB) {
System.out.println(Thread.currentThread().getName() + " acquired LockB");
}
}
}, "Thread2");
Thread1.start();
Thread2.start();
}
}
在上述代码中,Thread1
和Thread2
都先获取LockA
,再获取LockB
,按照相同的顺序获取锁,从而避免了死锁的发生 。
设置锁获取超时时间:使用tryLock(long timeout, TimeUnit unit)
方法(在ReentrantLock
中)或在自定义的同步机制中实现类似的超时逻辑 。如果在规定时间内未能获取到锁,线程可以选择放弃获取锁并执行其他操作,从而避免无限期等待,打破死锁的条件 。例如:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockAvoidanceByTimeout {
private static final ReentrantLock LockA = new ReentrantLock();
private static final ReentrantLock LockB = new ReentrantLock();
public static void main(String[] args) {
Thread Thread1 = new Thread(() -> {
boolean gotLockA = false;
boolean gotLockB = false;
try {
if (gotLockA = LockA.tryLock(5, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " acquired LockA");
if (gotLockB = LockB.tryLock(5, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " acquired LockB");
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire LockB within timeout");
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire LockA within timeout");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (gotLockA) {
LockA.unlock();
}
if (gotLockB) {
LockB.unlock();
}
}
}, "Thread1");
Thread Thread2 = new Thread(() -> {
boolean gotLockA = false;
boolean gotLockB = false;
try {
if (gotLockB = LockB.tryLock(5, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " acquired LockB");
if (gotLockA = LockA.tryLock(5, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " acquired LockA");
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire LockA within timeout");
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire LockB within timeout");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (gotLockA) {
LockA.unlock();
}
if (gotLockB) {
LockB.unlock();
}
}
}, "Thread2");
Thread1.start();
Thread2.start();
}
}
在这个示例中,Thread1
和Thread2
尝试获取锁时设置了 5 秒的超时时间 。如果在规定时间内未能获取到锁,线程会打印相应的提示信息并放弃获取锁,避免了死锁的发生 。
(二)合理控制锁粒度
锁粒度是指在多线程编程中,锁所保护的代码范围的大小 。合理控制锁粒度是优化多线程程序性能和确保线程安全的关键因素之一 。
锁粒度过大会导致并发性能下降 。当锁的范围覆盖了大量的代码和操作时,同一时刻只有一个线程能够进入该锁所保护的区域,其他线程只能等待 。这会限制程序的并发执行能力,尤其是在高并发场景下,可能会导致线程长时间等待,降低系统的吞吐量 。例如,在一个包含大量计算和少量共享资源访问的方法中,如果对整个方法加锁:
public class LargeLockGranularityExample {
private int sharedResource;
public synchronized void largeGranularityMethod() {
// 大量计算操作
for (int i = 0; i < 1000000; i++) {
// 复杂计算逻辑
}
// 对共享资源的访问
sharedResource++;
}
}
在上述代码中,largeGranularityMethod
方法被synchronized
修饰,对整个方法加锁 。虽然保证了对sharedResource
的线程安全访问,但大量的计算操作也被包含在锁的范围内,导致其他线程在等待锁时,无法并发执行计算操作,大大降低了并发性能 。
锁粒度过小则容易引发线程安全问题 。如果锁的范围设置得过小,可能无法完全保护共享资源,导致多个线程同时访问和修改共享资源,从而出现数据不一致等线程安全问题 。例如,在一个简单的计数器场景中,如果锁的粒度设置过小:
public class SmallLockGranularityExample {
private int count;
public void smallGranularityMethod() {
// 错误示范:锁粒度太小,无法保护count的操作
synchronized (this) {
// 仅对部分操作加锁,无法保证count的原子性
int temp = count;
}
temp++;
synchronized (this) {
count = temp;
}
}
}
在这个例子中,虽然对count
的读取和写入操作分别加了锁,但中间的temp++
操作在锁外,这就导致在多线程环境下,count
的值可能会出现错误,因为多个线程可能同时读取count
的值,然后分别进行自增操作,最后写入,从而覆盖了其他线程的修改 。
为了根据业务合理设置锁粒度,可以采取以下策略:
分析业务操作对共享资源的依赖关系:仔细分析业务逻辑,确定哪些操作真正需要同步,哪些操作可以并发执行 。对于那些不涉及共享资源或者对共享资源的访问是只读的操作,可以将其放在锁的范围之外 。例如,在一个电商订单处理系统中,订单查询操作通常不涉及对订单数据的修改,因此可以将订单查询方法不加锁,让多个线程可以并发查询订单信息,而对于订单创建、修改和删除等涉及数据一致性的操作,则需要加锁保护 。
将锁的范围尽量缩小到必要的操作:只对那些真正需要保护共享资源的操作加锁 。例如,在一个多线程操作数组的场景中,如果只是对数组中的某个元素进行修改,那么可以只对修改该元素的代码块加锁,而不是对整个数组操作的方法加锁 。如下所示:
public class ReasonableLockGranularityExample {
private int[] array = new int[10];
public void updateArrayElement(int index, int value) {
synchronized (this) {
if (index >= 0 && index < array.length) {
array[index] = value;
}
}
}
}
在上述代码中,updateArrayElement
方法只对修改数组元素的操作加锁,而不是对整个方法加锁,这样可以提高并发性能,同时保证了对数组元素修改的线程安全性 。
(三)性能优化建议
减少锁持有时间:尽量缩短线程持有锁的时间,以减少其他线程等待锁的时间,从而提高并发性能 。例如,将一些不需要同步的操作移到同步代码块之外 。比如在一个银行账户操作的场景中:
public class BankAccount {
private double balance;
public void transfer(BankAccount target, double amount) {
// 错误示范:锁持有时间过长
synchronized (this) {
// 进行一些与同步无关的计算
double fee = calculateTransferFee(amount);
// 真正需要同步的操作
if (balance >= amount + fee) {
balance -= amount + fee;
target.balance += amount;
}
}
}
private double calculateTransferFee(double amount) {
// 计算转账手续费的逻辑
return amount * 0.01;
}
}
在上述代码中,calculateTransferFee
方法的计算操作与同步无关,却被包含在同步代码块中,导致锁持有时间过长 。可以将其移到同步代码块之外,优化后的代码如下:
public class BankAccount {
private double balance;
public void transfer(BankAccount target, double amount) {
// 优化后:减少锁持有时间
double fee = calculateTransferFee(amount);
synchronized (this) {
// 真正需要同步的操作
if (balance >= amount + fee) {
balance -= amount + fee;
target.balance += amount;
}
}
}
private double calculateTransferFee(double amount) {
// 计算转账手续费的逻辑
return amount * 0.01;
}
}
这样,在计算手续费时,其他线程可以同时访问BankAccount
对象的其他方法,提高了并发性能 。
使用分段锁:在处理大规模数据结构(如哈希表、数组等)时,使用分段锁可以将数据结构分成多个段,每个段有自己的锁 。这样,不同的线程可以同时访问不同段的数据,从而减少锁竞争,提高并发性能 。例如,在ConcurrentHashMap
中就使用了分段锁的机制 。在 Java 7 及之前的版本中,ConcurrentHashMap
将数据分成多个段(Segment),每个段都有自己的锁 。当一个线程要访问某个键值对时,首先根据键的哈希值计算出它所在的段,然后获取该段的锁 。这样,多个线程可以同时访问不同段的数据,大大提高了并发性能 。在 Java 8 中,ConcurrentHashMap
虽然在实现上有所改进,但仍然保留了类似分段锁的思想,通过更细粒度的锁控制和 CAS 操作来进一步提高并发性能 。以下是一个简单的自定义分段锁示例:
import java.util.HashMap;
import java.util.Map;
public class CustomSegmentedLock {
private static final int SEGMENT_COUNT = 16;
private final Segment[] segments;
public CustomSegmentedLock() {
segments = new Segment[SEGMENT_COUNT];
for (int i = 0; i < SEGMENT_COUNT; i++) {
segments[i] = new Segment();
}
}
public void put(String key, Object value) {
int segmentIndex = Math.abs(key.hashCode()) % SEGMENT_COUNT;
Segment segment = segments[segmentIndex];
synchronized (segment) {
segment.map.put(key, value);
}
}
public Object get(String key) {
int segmentIndex = Math.abs(key.hashCode()) % SEGMENT_COUNT;
Segment segment = segments[segmentIndex];
synchronized (segment) {
return segment.map.get(key);
}
}
private static class Segment {
Map<String, Object> map = new HashMap<>();
}
}
在上述代码中,CustomSegmentedLock
将数据分成 16 个段,每个段都有自己的锁 。当进行put
或get
操作时,首先根据键的哈希值确定所在的段,然后获取该段的锁,从而实现了分段锁的功能,减少了锁竞争,提高了并发性能 。
八、总结
(一)总结 synchronized 关键知识点
synchronized 作为 Java 并发编程中的重要关键字,在多线程环境下确保线程安全方面发挥着核心作用。它的主要作用是实现对共享资源的互斥访问,保证同一时刻只有一个线程能够执行被其修饰的代码块或方法,有效避免多线程竞争共享资源时引发的数据不一致、竞态条件等问题。
在用法上,synchronized 有三种方式:修饰实例方法时,锁定的是当前实例对象,同一时刻只有一个线程能进入该实例对象的同步实例方法;修饰静态方法时,锁定的是当前类的 Class 对象,无论通过哪个实例对象调用静态同步方法,同一时刻都只有一个线程可执行;修饰代码块时,需指定一个对象作为锁,只有获取到该锁的线程才能进入同步代码块执行。
从原理层面来看,在 JVM 层面,synchronized 通过monitorenter
和monitorexit
指令实现同步,线程执行到同步代码块时先执行monitorenter
尝试获取对象监视器锁,成功则继续,失败则阻塞,执行完同步代码块通过monitorexit
释放锁。每个对象都有对象头,其中 Mark Word 记录锁状态,Monitor 是实现同步的关键底层数据结构,包含 Owner、EntryList 和 WaitSet 等部分,用于管理线程对锁的竞争和等待 。并且从 Java 6 开始引入锁升级机制,根据竞争情况自动将锁从偏向锁逐步升级到轻量级锁再到重量级锁 ,以提高在不同并发场景下的性能 。
synchronized 具有可重入性,同一个线程可以多次获取同一把锁,不会造成死锁;但它具有不可中断性,线程在等待锁时无法被中断 。其优点包括语法简单易用,JVM 自动管理锁释放,能有效解决多线程并发问题;缺点则是无法判断锁获取状态,在锁竞争激烈时性能较低,且使用不当可能产生死锁 。
与 ReentrantLock 相比,synchronized 是隐式锁,自动获取和释放锁,不支持可中断、公平锁和多个条件变量等功能;而 ReentrantLock 是显式锁,需要手动获取和释放锁,支持可中断、公平锁和多个条件变量等更灵活的功能 。与 volatile 相比,synchronized 保证原子性和可见性,而 volatile 仅保证可见性,不保证原子性 。
在使用 synchronized 时,要注意避免死锁,可通过按相同顺序获取锁、设置锁获取超时时间等方法来预防;合理控制锁粒度,避免锁粒度过大导致并发性能下降或锁粒度过小引发线程安全问题;还可通过减少锁持有时间、使用分段锁等方式进行性能优化 。