Java synchronized关键字实战:同步方法与同步块的区别及线程安全陷阱

白榆
白榆
发布时间:2026-10-13| 1人浏览过
原创

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在不同上下文中的锁定目标:

  1. 同步方法实质是对this加锁
    public synchronized void methodAAA() {
        // ...
    }
    等同于:
    public void methodAAA() {
        synchronized (this) { // 锁定调用者实例
            // ...
        }
    }
    结论:如果P1和P2是同一个类的不同对象,P1持有锁时,P2依然可以调用该方法而不受阻塞。
  2. 同步块锁定指定对象
    可以使用任意对象作为锁。为了减少开销,有时推荐使用零长度的byte数组:
    class Foo implements Runnable {
        private byte[] lock = new byte[0]; // 轻量级锁对象
        public void methodA() {
            synchronized(lock) { /* ... */ }
        }
    }
  3. 静态同步方法锁定Class对象
    public synchronized static void methodAAA() { /* ... */ }
    // 等价于
    public void methodBBB() {
        synchronized(Foo.class) { /* ... */ }
    }
    注意:静态同步方法(锁Class)与实例同步方法(锁this)是两种完全不同的锁。同一对象Obj同时调用这两个方法时,不会构成同步,因为它们锁定的对象不同。

线程安全的高级防护技巧

除了正确使用synchronized,还需要配合良好的封装策略以防止外部绕过锁机制:

  • 私有化变量:永远不要暴露public/protected的共享变量。应定义为private并提供get/set方法。
  • 防御性拷贝:如果共享变量引用的是可变对象(如Array、List等),仅仅保护字段引用是不够的。外界拿到引用后可以修改其内部状态。
    应对方案:在getter方法中返回对象的clone()副本,或者确保getter/setter也加上同步控制,并且只操作不可变类型或深拷贝。
  • 避免无谓的同步:同步会带来系统开销甚至死锁风险。应尽量缩小synchronized的作用范围,只在必要的临界区使用。

总结

synchronized的成功关键在于“锁是谁”。
- 锁定实例对象(this):适用于保护单个对象内部状态的一致性。
- 锁定类对象(Class):适用于需要跨所有实例进行互斥的场景。

理解这一点,能有效规避常见的并发Bug。以上就是Java中synchronized关键字与线程安全问题的详细内容,更多关于Java多线程并发编程的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。