Java synchronized关键字实战:同步方法与同步块的区别及线程安全陷阱
Java开发中常误以为在方法上添加synchronized即可自动解决线程安全问题,但实际运行中不同对象实例间的竞争会导致数据混乱。本文通过具体代码演示,深入解析synchronized锁定的是"对象"而非"代码块"的核心机制,对比同步方法与同步类的行为差异,帮助开发者正确理解对象锁与类锁,从而写出真正安全的并发代码。
synchronized的基本锁机制
synchronized是Java中用于实现线程互斥访问的关键字。要正确使用它,首先需要明确其作用范围和锁定对象:
- synchronized代码块:锁定括号内指定的对象。推荐尽量缩小同步范围(减小锁粒度),以提高并发性能。
- synchronized方法:锁定当前调用该方法的实例对象(this)。只有持有该对象锁的线程才能执行该方法。
- synchronized静态方法:锁定该类对应的Class对象。这意味着该类的所有实例对象调用此静态方法时都会发生互斥。
- synchronized(ClassName.class):显式使用类字面量作为锁,效果等同于静态同步方法,实现全局级别的互斥。
实例解析:为何同步方法并非万能钥匙
很多开发者认为只要在共享资源操作方法上加synchronized就能保证线程安全,但在涉及多个对象实例时,这往往是一个陷阱。下面通过一个具体的测试案例来验证这一现象。
第1步:构建测试环境与非安全实现
创建两个不同的Thread对象实例,分别执行包含打印操作的run方法。首先尝试使用标准的synchronized实例方法来实现同步。
package com;
public class ThreadTest {
public static void main(String[] args) {
MyThread m1 = new MyThread(1);
MyThread m2 = new MyThread(2);
// 启动两个不同的线程实例
m1.start();
m2.start();
}
}
final class MyThread extends Thread {
private int val;
public MyThread(int v) {
val = v;
}
// 错误示范:同步实例方法
// 当m1和m2是不同对象时,它们持有的锁(this)不同,因此不会互相阻塞
public synchronized void print1(int v) {
for (int i = 0; i < 100; i++) {
System.out.print(v);
}
}
public void run() {
print1(val);
}
}
结果分析:
虽然print1方法被synchronized修饰,但由于main方法中创建了m1和m2两个不同的对象,m1调用print1时锁定的是m1对象,m2调用时锁定的是m2对象。两者持有的锁不同,因此不存在互斥关系。
控制台输出会出现1和2交织的现象(如:121211112...),证明这种写法未能保证线程安全。
第2步:修正为正确的类级别同步
为了确保不同线程实例之间的数据隔离和完整性,必须使用类级别的锁,即锁定Class对象本身。这样无论哪个对象调用该方法,都需要先获取同一把锁。
// ... 上文省略 ...
public void print2(int v) {
// 正确做法:使用类名.class作为锁
synchronized (MyThread.class) {
for (int i = 0; i < 100; i++) {
System.out.print(v);
}
}
}
public void run() {
// print1(val); // 注释掉之前的非安全写法
print2(val); // 启用正确的类同步写法
}
结果分析:
运行后,控制台将优先输出一连串的1,随后是一连串的2(或反之)。因为所有MyThread实例共享同一个类锁(Class Object),一次只有一个线程能进入同步块执行完毕,从而实现了真正的线程安全。
synchronized锁定对象的深度辨析
要设计安全的多线程程序,必须厘清synchronized在不同上下文中的锁定目标:
- 同步方法实质是对this加锁
等同于:public synchronized void methodAAA() { // ... }
结论:如果P1和P2是同一个类的不同对象,P1持有锁时,P2依然可以调用该方法而不受阻塞。public void methodAAA() { synchronized (this) { // 锁定调用者实例 // ... } } - 同步块锁定指定对象
可以使用任意对象作为锁。为了减少开销,有时推荐使用零长度的byte数组:
class Foo implements Runnable { private byte[] lock = new byte[0]; // 轻量级锁对象 public void methodA() { synchronized(lock) { /* ... */ } } } - 静态同步方法锁定Class对象
注意:静态同步方法(锁Class)与实例同步方法(锁this)是两种完全不同的锁。同一对象Obj同时调用这两个方法时,不会构成同步,因为它们锁定的对象不同。public synchronized static void methodAAA() { /* ... */ } // 等价于 public void methodBBB() { synchronized(Foo.class) { /* ... */ } }
线程安全的高级防护技巧
除了正确使用synchronized,还需要配合良好的封装策略以防止外部绕过锁机制:
- 私有化变量:永远不要暴露public/protected的共享变量。应定义为private并提供get/set方法。
- 防御性拷贝:如果共享变量引用的是可变对象(如Array、List等),仅仅保护字段引用是不够的。外界拿到引用后可以修改其内部状态。
应对方案:在getter方法中返回对象的clone()副本,或者确保getter/setter也加上同步控制,并且只操作不可变类型或深拷贝。 - 避免无谓的同步:同步会带来系统开销甚至死锁风险。应尽量缩小synchronized的作用范围,只在必要的临界区使用。
总结
synchronized的成功关键在于“锁是谁”。
- 锁定实例对象(this):适用于保护单个对象内部状态的一致性。
- 锁定类对象(Class):适用于需要跨所有实例进行互斥的场景。
理解这一点,能有效规避常见的并发Bug。以上就是Java中synchronized关键字与线程安全问题的详细内容,更多关于Java多线程并发编程的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









