深入了解JAVA中Synchronized
什么是synchroniaed?
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized
属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
不过,在 Java 6 之后, synchronized
引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized
锁的效率提升了很多。因此, synchronized
还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized
synchronized 的三种使用方式
1、同步实例方法
public synchronized void method() {// 需要同步的代码
}
特点:
- 锁对象是当前实例对象(this)
- 同一实例的多个同步方法不能同时被多个线程执行
- 不同实例的同步方法互不影响
案例:银行账户操作
我们创建一个 BankAccount
类,其中包含使用 synchronized
修饰的实例方法,模拟多线程环境下的存款和取款操作。
public class BankAccount {private String accountNumber; // 账号private double balance; // 余额// 构造函数public BankAccount(String accountNumber, double initialBalance) {this.accountNumber = accountNumber;this.balance = initialBalance;}/*** 存款操作 - 同步实例方法* @param amount 存款金额*/public synchronized void deposit(double amount) {System.out.println(Thread.currentThread().getName() + " 存款前余额: " + balance);balance += amount;System.out.println(Thread.currentThread().getName() + " 存入: " + amount);System.out.println(Thread.currentThread().getName() + " 存款后余额: " + balance);try {// 模拟处理时间Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}/*** 取款操作 - 同步实例方法* @param amount 取款金额* @return 取款是否成功*/public synchronized boolean withdraw(double amount) {System.out.println(Thread.currentThread().getName() + " 取款前余额: " + balance);if (balance >= amount) {balance -= amount;System.out.println(Thread.currentThread().getName() + " 取出: " + amount);System.out.println(Thread.currentThread().getName() + " 取款后余额: " + balance);try {// 模拟处理时间Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return true;} else {System.out.println(Thread.currentThread().getName() + " 取款失败,余额不足");return false;}}/*** 获取当前余额 - 同步实例方法* @return 账户余额*/public synchronized double getBalance() {return balance;}// 测试主方法public static void main(String[] args) throws InterruptedException {BankAccount account = new BankAccount("123456789", 1000.0);// 创建多个线程进行存款和取款操作Thread depositThread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {account.deposit(100);}}, "存款线程1");Thread depositThread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {account.deposit(150);}}, "存款线程2");Thread withdrawThread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {account.withdraw(200);}}, "取款线程1");Thread withdrawThread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {account.withdraw(50);}}, "取款线程2");// 启动所有线程depositThread1.start();depositThread2.start();withdrawThread1.start();withdrawThread2.start();// 等待所有线程完成depositThread1.join();depositThread2.join();withdrawThread1.join();withdrawThread2.join();// 打印最终余额System.out.println("\n最终余额: " + account.getBalance());}
}
代码解析
- synchronized 实例方法:
deposit()
、withdraw()
和getBalance()
方法都使用synchronized
修饰- 锁对象是当前 BankAccount 实例(this)
- 同一时间只有一个线程可以执行这些同步方法
- 线程安全保证:
- 多个线程同时操作同一个账户时,不会出现竞态条件
- 所有对余额的修改都是原子的、可见的
- 测试场景:
- 创建了 2 个存款线程和 2 个取款线程
- 每个线程执行多次操作
- 最终余额应该正确反映所有操作的结果
2、同步静态方法
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
特点:
- 锁对象是当前类的 Class 对象(如 Counter.class)
- 所有实例的同步静态方法不能同时被多个线程执行
- 静态同步方法和实例同步方法使用不同的锁,互不干扰
public static synchronized void staticMethod() {// 需要同步的代码
}
案例:全局计数器
我们创建一个 GlobalCounter
类,其中包含使用 synchronized
修饰的静态方法,模拟多线程环境下对全局计数器的操作。
public class GlobalCounter {private static int count = 0; // 静态计数器/*** 增加计数器 - 同步静态方法*/public static synchronized void increment() {System.out.println(Thread.currentThread().getName() + " 增加前: " + count);count++;System.out.println(Thread.currentThread().getName() + " 增加后: " + count);try {// 模拟处理时间Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}/*** 减少计数器 - 同步静态方法*/public static synchronized void decrement() {System.out.println(Thread.currentThread().getName() + " 减少前: " + count);count--;System.out.println(Thread.currentThread().getName() + " 减少后: " + count);try {// 模拟处理时间Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}/*** 获取当前计数值 - 同步静态方法*/public static synchronized int getCount() {return count;}// 测试主方法public static void main(String[] args) throws InterruptedException {// 创建多个线程进行操作Thread incrementThread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {GlobalCounter.increment();}}, "增加线程1");Thread incrementThread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {GlobalCounter.increment();}}, "增加线程2");Thread decrementThread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {GlobalCounter.decrement();}}, "减少线程1");Thread decrementThread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {GlobalCounter.decrement();}}, "减少线程2");// 启动所有线程incrementThread1.start();incrementThread2.start();decrementThread1.start();decrementThread2.start();// 等待所有线程完成incrementThread1.join();incrementThread2.join();decrementThread1.join();decrementThread2.join();// 打印最终计数值System.out.println("\n最终计数值: " + GlobalCounter.getCount());}
}
关键点解析
- 锁对象不同:
- 静态同步方法的锁是类的
Class
对象(GlobalCounter.class
) - 实例同步方法的锁是当前实例对象(this)
- 静态同步方法的锁是类的
- 全局互斥:
- 所有调用这些静态同步方法的线程都会竞争同一把锁
- 不论创建多少个
GlobalCounter
实例,静态同步方法都是互斥的
在Java中,静态synchronized方法和非静态synchronized方法之间不会产生互斥现象。这是因为它们使用的是完全不同的锁对象。当一个线程调用某个实例的非静态synchronized方法时,它获取的是该实例对象本身的锁(即this对象锁);而另一个线程调用该类的静态synchronized方法时,它获取的是类的Class对象锁。由于实例对象锁和Class对象锁是两个独立的锁,因此这两种方法调用可以同时进行而不会互相阻塞。这种设计使得类级别的同步和实例级别的同步能够相互独立,既保证了静态数据在多线程环境下的安全性,又不影响实例方法的并发执行效率。理解这个区别对于编写正确的多线程程序非常重要,特别是在处理既有静态数据又有实例数据的类时。
3、同步代码块
同步代码块是比同步方法更细粒度的同步控制方式,它允许我们精确指定需要加锁的对象和同步范围。
synchronized(lockObject) {// 需要同步的代码
}
特点:
- 灵活选择锁对象:可以使用任意对象作为锁,而不仅限于当前实例(this)或Class对象
- 缩小同步范围:只同步必要的代码部分,提高并发性能
- 明确锁的边界:代码块开始处获取锁,结束处释放锁,行为更明确
同步代码块案例
案例1:使用特定对象作为锁
public class SharedResource {private final Object lock = new Object(); // 专门创建的锁对象private int count = 0;public void increment() {// 非同步代码可以放在同步块外System.out.println(Thread.currentThread().getName() + " 准备增加计数");synchronized(lock) { // 使用特定对象作为锁System.out.println(Thread.currentThread().getName() + " 进入同步块");count++;System.out.println(Thread.currentThread().getName() + " 增加后计数: " + count);try {Thread.sleep(100); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " 离开同步块");}public int getCount() {synchronized(lock) { // 读操作也需要同步return count;}}
}
代码解析
这个SharedResource
类展示了如何使用同步代码块保护共享资源:
- 锁设计:
private final Object lock
- 专门创建的锁对象- 使用final确保锁引用不变,避免意外修改
- 共享资源:
private int count
- 需要保护的共享计数器
- 同步控制:
increment()
方法中只有关键操作放在同步块内getCount()
读操作也使用同步保证可见性- 使用相同的
lock
对象作为同步锁
public class SharedResourceTest {public static void main(String[] args) throws InterruptedException {SharedResource resource = new SharedResource();// 创建3个线程并发操作Runnable task = () -> {for (int i = 0; i < 3; i++) {resource.increment();try {Thread.sleep(50); // 增加操作间隔} catch (InterruptedException e) {e.printStackTrace();}}};Thread t1 = new Thread(task, "线程A");Thread t2 = new Thread(task, "线程B");Thread t3 = new Thread(task, "线程C");t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();System.out.println("最终计数: " + resource.getCount()); // 应该总是9}
}
案例2:使用Class对象作为锁
public class Logger {private static List<String> logs = new ArrayList<>();public static void addLog(String message) {synchronized(Logger.class) { // 使用Class对象作为锁logs.add(Thread.currentThread().getName() + ": " + message);System.out.println("已添加日志: " + message);}}public static void printAllLogs() {synchronized(Logger.class) {System.out.println("\n=== 日志记录 ===");for (String log : logs) {System.out.println(log);}}}
}
代码解析
这个Logger
类是一个线程安全的静态日志记录器实现:
- 静态共享资源:
private static List<String> logs
- 静态变量存储所有日志- 被所有Logger类的实例共享
- 同步控制:
- 使用
synchronized(Logger.class)
对静态方法加锁 - 锁对象是Logger的Class对象,确保全局唯一锁
- 保证多线程环境下对静态变量的安全访问
- 使用
- 核心方法:
addLog()
- 添加日志到集合,线程安全printAllLogs()
- 打印所有日志,线程安全
public class LoggerTest {public static void main(String[] args) throws InterruptedException {// 创建多个线程并发添加日志Thread t1 = new Thread(() -> {for (int i = 1; i <= 3; i++) {Logger.addLog("线程1的日志" + i);try {Thread.sleep(100); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(() -> {for (int i = 1; i <= 3; i++) {Logger.addLog("线程2的日志" + i);try {Thread.sleep(150);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();// 等待日志添加完成t1.join();t2.join();// 打印所有日志Logger.printAllLogs();}
}
案例3:使用this作为锁(等效于同步实例方法)
public class Counter {private int value = 0;public void add(int num) {synchronized(this) { // 等同于同步实例方法value += num;System.out.println(Thread.currentThread().getName() + " 添加 " + num + ", 当前值: " + value);}}public void subtract(int num) {synchronized(this) {value -= num;System.out.println(Thread.currentThread().getName() + " 减去 " + num + ", 当前值: " + value);}}
}
代码解析
这个Counter
类使用同步代码块实现了一个线程安全的计数器:
- 实例变量:
private int value
- 存储计数器当前值- 是实例变量,每个Counter对象独立
- 同步控制:
- 使用
synchronized(this)
对实例方法加锁 - 锁对象是当前实例(this),等同于同步实例方法
- 保证多线程环境下对实例变量的安全访问
- 使用
- 核心方法:
add()
- 增加计数器值,线程安全subtract()
- 减少计数器值,线程安全
public class CounterTest {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();// 创建增加线程Thread addThread = new Thread(() -> {for (int i = 0; i < 5; i++) {counter.add(10);try {Thread.sleep(100); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}}}, "增加线程");// 创建减少线程Thread subtractThread = new Thread(() -> {for (int i = 0; i < 5; i++) {counter.subtract(5);try {Thread.sleep(150);} catch (InterruptedException e) {e.printStackTrace();}}}, "减少线程");addThread.start();subtractThread.start();// 等待线程完成addThread.join();subtractThread.join();System.out.println("最终计数器值: " + counter.value);}
}
构造方法可以用 synchronized 修饰么?
构造方法不能也不需要使用 synchronized
修饰
1. 语法层面不允许
Java 语言规范明确规定构造方法不能使用 synchronized
修饰符。
2. 逻辑上不需要
构造方法本身具有以下特性,使得同步变得不必要:
- 对象创建期间不会被多线程访问:在构造函数执行完成前,对象尚未完全创建,其他线程无法获取该对象引用
- 每个线程都会创建自己的实例:构造方法是针对新创建的对象操作,不存在多线程共享问题
相关面试题
1、Synchronized可以作用在哪里? 分别通过对象锁和类锁进行举例。
Synchronized可以作用于实例方法、静态方法和代码块。对象锁是针对实例对象的,比如在实例方法上加synchronized或使用synchronized(this)修饰代码块,锁的是当前对象实例;类锁是针对Class对象的,比如在静态方法上加synchronized或使用synchronized(类.class)修饰代码块,锁的是类的Class对象。例如public synchronized void instanceMethod()是对象锁,而public static synchronized void staticMethod()是类锁,它们使用不同的锁对象互不干扰。
2、Synchronized本质上是通过什么保证线程安全的?
Synchronized通过三种机制保证线程安全:首先是通过Monitor机制实现加锁和释放锁,每个对象关联一个Monitor,线程通过CAS操作竞争Owner标记;其次是可重入性,通过_recursions计数器记录重入次数,同一线程可重复获取锁;最后是内存可见性,通过MonitorEnter和MonitorExit内存屏障保证变量的修改对所有线程可见,遵循happens-before原则确保同步块内的修改对后续获取锁的线程可见。
3、Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的。
Synchronized的主要缺陷是功能单一且不够灵活,比如无法中断等待锁的线程、不支持超时获取锁、不支持公平锁、不能绑定多个条件变量等。Java的Lock接口通过提供lockInterruptibly()、tryLock()、newCondition()等方法弥补这些缺陷,实现了可中断锁获取、尝试非阻塞获取锁、公平锁、多条件变量等高级功能,给开发者更灵活的并发控制能力。
4、Synchronized和Lock的对比,和选择?
Synchronized是JVM内置的关键字,使用简单自动释放锁,适合简单的同步场景;Lock是JDK提供的接口,功能丰富但需要手动释放锁,适合复杂的同步需求。选择依据是:简单同步用Synchronized,需要高级功能如超时、中断、公平锁等用Lock;Synchronized在JDK6后性能已大幅优化,除非有特殊需求,否则优先考虑Synchronized的简洁性。
5、Synchronized在使用时有何注意事项?
使用Synchronized时要注意:尽量减小同步范围,只同步必要的代码;避免在同步块中执行耗时IO操作;注意锁的粒度,过大会降低并发性,过小可能导致逻辑错误;不同线程获取多个锁时要保持一致的顺序避免死锁;对于读多写少的场景考虑使用读写锁替代;静态同步和非静态同步使用不同的锁要注意区分。
6、Synchronized修饰的方法在抛出异常时,会释放锁吗?
是的,Synchronized修饰的方法在抛出异常时会自动释放锁。这是因为Synchronized的锁释放是通过MonitorExit指令实现的,而无论方法是正常返回还是异常退出,编译器都会在方法返回前插入MonitorExit指令,确保锁一定会被释放,避免死锁情况的发生。这也是Synchronized相比需要手动释放的Lock接口的一个安全优势。
7、多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
当多个线程竞争同一个Synchronized锁时,JVM默认采用非公平策略随机选择下一个获取锁的线程,不保证先等待的线程先获取锁。这种设计基于性能考虑,避免了线程切换开销。从JDK6开始,可以通过JVM参数-XX:+UseBiasedLocking启用偏向锁优化,但Synchronized本身不提供公平锁选项,如需公平性需要使用ReentrantLock的公平模式。
8、Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?
提升Synchronized性能的方法包括:减小同步范围,只同步必要的代码段;对于读多写少的场景改用读写锁;考虑使用并发容器替代同步块;使用锁分段技术减小锁粒度;JDK6后的锁升级优化已大幅改善Synchronized性能,在无竞争时几乎无开销;对于特定场景可使用原子变量或volatile替代;最终极的方案是避免共享数据,采用线程封闭或消息传递机制。
9、我想更加灵活的控制锁的释放和获取(现在释放锁和获取锁的时机都被规定死了),怎么办?
当需要更灵活的锁控制时,应该使用java.util.concurrent.locks.Lock接口及其实现类如ReentrantLock。Lock接口提供了lock()/unlock()方法手动控制锁获取释放,支持tryLock()尝试获取锁、lockInterruptibly()可中断获取锁、tryLock(timeout)超时获取等功能。还可以通过newCondition()创建多个条件变量,实现精细的线程等待/唤醒控制,这些都是Synchronized无法提供的灵活性。
10、什么是锁的升级和降级? 什么是JVM里的偏斜锁、轻量级锁、重量级锁?
锁升级是JVM根据竞争情况从偏向锁→轻量级锁→重量级锁的优化过程:偏向锁在无竞争时记录线程ID消除同步开销;出现竞争时升级为轻量级锁,通过CAS自旋尝试获取锁;自旋失败后最终升级为重量级锁使线程阻塞。锁降级是从重量级锁降为轻量级锁,JDK16开始支持。这三种锁状态对应不同的Mark Word内容,JVM会根据运行情况自动转换以实现最佳性能。
11、不同的JDK中对Synchronized有何优化?
JDK对Synchronized的主要优化包括:1.6引入偏向锁和轻量级锁,大幅减少无竞争时的开销;1.6加入锁消除优化,通过逃逸分析去掉不必要的同步;1.6引入自适应自旋锁,动态调整自旋次数;1.6加入锁粗化合并相邻同步块;16开始支持锁降级;各版本持续优化Monitor实现减少系统调用开销。这些优化使Synchronized在无竞争或低竞争场景下性能接近无锁,高竞争时性能也有显著提升。