深入理解Java中的synchronized关键字:原理、用法与最佳实践
synchronized是Java并发编程中实现互斥访问的核心关键字,用于确保在同一时刻只有一个线程能执行特定代码块或方法。本文详细解析其两种主要用法(同步方法与同步块)、底层锁机制(实例锁与类锁)的区别,并通过代码示例展示如何在实际开发中利用它解决数据竞争问题,同时规避常见陷阱。
核心概念:什么是synchronized
synchronized 关键字代表了方法加锁。当多个线程试图访问同一个资源时,synchronized 确保在某一时刻,只有一个线程可以执行被锁定的代码段。其他尝试进入该区域的线程会被阻塞,直到持有锁的线程释放锁为止。这种机制有效避免了多线程环境下的数据冲突。
synchronized 主要有两种使用形式:
- 同步方法:通过在方法声明中加入
synchronized关键字来修饰整个方法。 - 同步块:在代码块中使用
synchronized并指定一个锁对象,仅对该代码块进行锁定。
第一种用法:synchronized 方法
第1步:定义同步方法
目的:确保类实例对成员变量的访问是线程安全的,一次只允许一个线程执行该方法。
操作:在方法返回值前添加 synchronized 修饰符。
public synchronized void accessVal(int newVal) {
// 方法体
}
完成状态:每个类实例对应一把锁。当线程A调用此方法时,它会获取该实例的锁。如果线程B也尝试调用同一实例的任意 synchronized 方法,它必须等待线程A返回并释放锁后才能执行。
注意:虽然静态方法也可以加 synchronized,但其锁定的是类本身(Class对象),而非实例。对于大型方法直接使用 synchronized 会影响性能,因为整个方法的执行期间都独占锁。更推荐的做法是使用同步块来缩小锁定范围。
第二种用法:synchronized 块
第2步:创建同步块
目的:提供比同步方法更灵活的锁定粒度,仅对关键代码段进行保护,提高并发效率。
入口与参数:指定一个作为锁的对象(可以是类实例、类本身或专门创建的辅助对象)。
synchronized (lockObject) {
// 允许访问控制的代码
}
优势:你可以任意指定上锁的对象。例如,可以使用一个专门的 byte[] 数组或 Object 实例作为锁,而不是依赖当前实例 this 或类锁。
第3步:理解 synchronized(this)
当代码中使用 synchronized(this) 时,锁定的就是当前类的实例对象。
- 互斥性:如果两个线程访问同一个对象中的
synchronized(this)代码块,只有一个能先执行。 - 阻塞范围:当一个线程持有该对象的锁时,其他线程无法访问该对象的其他
synchronized(this)代码块,但可以访问非同步方法。 - 对象锁唯一性:每个对象只有一个关联的锁。获得该锁即意味着暂时阻塞了其他线程对该对象所有同步部分的访问。
实例演示:线程安全的计数器
以下代码展示了如何使用 synchronized 块来保证多线程环境下变量 num 的递减操作是安全的。通过锁定字符串对象 str,确保同一时间只有一个线程能修改 num。
public class TextThread {
public static void main(String[] args) {
TxtThread tt = new TxtThread();
// 启动多个线程共享同一个 Runnable 实例
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
}
}
class TxtThread implements Runnable {
int num = 100;
String str = new String();
public void run() {
while (true) {
synchronized(str) { // 锁定 str 对象
if (num > 0) {
try {
Thread.sleep(10); // 模拟处理延迟,增加竞态条件概率
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " this is " + num--);
}
}
}
}
}
在此例中,Thread.sleep(10) 增加了出现竞态条件的可能性,但 synchronized 确保了即便发生了睡眠,num-- 的操作也是原子性的,不会出现输出重复或负数的情况。
高级技巧:锁对象的选定与作用域
理解 synchronized 锁定的是哪个对象是设计安全并发程序的关键。
1. 实例锁 vs 类锁
- 实例锁:
synchronized作用于普通方法或非静态同步块时,锁的是调用该方法的对象实例(即this)。不同实例间的同步互不干扰。 - 类锁:
synchronized作用于static方法或Foo.class字面量时,锁的是该类的 Class 对象。无论创建了多少个实例,所有线程对静态同步资源的访问都受同一把锁控制。
示例:
Class Foo {
// 锁定 Class 对象
public synchronized static void methodAAA() { ... }
// 锁定 Class 对象(等价于 above)
public void methodBBB() {
synchronized(Foo.class) { ... }
}
// 锁定 Foo 类的具体实例
public synchronized void methodCCC() { ... }
}
如果方法A是静态同步,方法B是实例同步,即使是对同一个对象Obj,访问A和B也不会互相阻塞,因为它们使用的锁完全不同。
2. 选择合适的锁对象
为了减少锁定范围和提高性能,建议使用专用的私有助手对象作为锁,而不是暴露的 this。
class Foo implements Runnable {
private byte[] lock = new byte[0]; // 高效的轻量级锁对象
public void methodA() {
synchronized(lock) { ... }
}
}
使用零长度的 byte[] 作为锁比 new Object() 更高效,且在语义上明确表明这是一个内部辅助锁,不易被外部代码意外持有。
最佳实践与注意事项
尽管 synchronized 简单易用,但需注意以下事项以构建更健壮的并发系统:
第4步:封装与不可变性
- 私有变量:将共享状态定义为
private,并提供同步的访问方法(Getter/Setter)。禁止直接暴露公共成员变量,防止绕过同步机制。 - 防御性拷贝:如果Get返回的是可变对象(如数组或集合),应考虑返回其副本(clone)或使用
Collections.synchronizedList/Collections.synchronizedMap,防止调用者修改内部状态而不持有任何锁。
第5步:性能与死锁风险
- 最小化同步范围:仅锁定必要代码,避免在整个长耗时方法上加锁,以减少线程阻塞等待时间。
- 避免嵌套锁:复杂的锁嵌套容易导致死锁。应尽量保持锁的顺序一致,或尽量使用细粒度的锁代替全局锁。
总结
synchronized 是 Java 实现线程安全的基础工具。通过正确区分实例锁和类锁,合理选择同步块和锁对象,开发者可以有效避免数据竞争。记住,锁的是对象,不是代码;只有理解了这一点,才能在复杂的多线程场景中设计出既安全又高效的程序。
以上就是深入理解java中的synchronized关键字的详细内容,更多关于java多线程同步的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













