杰瑞科技汇

Java synchronized 块如何保证线程安全?

什么是 synchronized 块?

synchronized 块是 Java 中用于实现线程同步的关键字,它的主要作用是确保在同一时间,只有一个线程可以执行被 synchronized 保护的代码块,这可以有效防止多个线程同时修改共享数据,从而避免数据不一致、数据损坏等并发问题。

Java synchronized 块如何保证线程安全?-图1
(图片来源网络,侵删)

synchronized 就是在代码周围加了一把“锁”,拿到锁的线程才能进入,执行完代码后释放锁,其他线程才能竞争这把锁。


synchronized 块的基本语法

synchronized 块有以下两种主要形式:

锁定任意对象(最常用)

这种形式用于锁定一个对象实例类对象,确保只有一个线程能访问被该对象锁定的代码块。

synchronized (锁对象) {
    // 需要被同步的代码块
    // 只有获取到 "锁对象" 的锁的线程才能执行这里的代码
}

关键点:

Java synchronized 块如何保证线程安全?-图2
(图片来源网络,侵删)
  • 锁对象:可以是任何 Java 对象,通常推荐使用共享资源本身、或者专门用于同步的 Object 对象。
  • 同一个锁对象:只有当多个线程试图锁定的是同一个锁对象时,同步才有效,如果每个线程都用自己的锁对象,那么它们之间就不会互相阻塞。

锁定 Class 对象(用于静态方法)

这种形式用于锁定类的 Class 对象,常用于同步静态方法或静态代码块,确保在 JVM 中,一个类的所有静态同步代码块在同一时间只能被一个线程执行。

synchronized (ClassName.class) {
    // 需要被同步的静态代码块
    // 只有获取到 "ClassName.class" 这把锁的线程才能执行
}

synchronized 块 vs. synchronized 方法

在学习 synchronized 块之前,很多人已经接触过 synchronized 方法,它们的核心原理是一样的,都是通过“锁”来实现的,但锁的粒度和应用场景有所不同。

特性 synchronized 方法 synchronized
锁对象 非静态方法:this (当前实例对象)
静态方法:ClassName.class (类对象)
由程序员显式指定,可以是任意对象
锁粒度 粗粒度,锁定了整个方法,即使方法中只有一小部分代码需要同步。 细粒度,只锁定真正需要同步的关键代码,提高了并发性能。
灵活性 低,只能对整个方法进行同步。 高,可以精确控制哪些代码需要同步,哪些不需要。
语法 public synchronized void method() { ... } synchronized (lock) { ... }

在大多数情况下,推荐使用 synchronized,因为它提供了更好的性能和灵活性。


synchronized 块的工作原理(可重入锁)

synchronized 使用的是可重入锁(Reentrant Lock)。

Java synchronized 块如何保证线程安全?-图3
(图片来源网络,侵删)
  • 可重入:指的是同一个线程可以多次获取同一个锁,而不会导致自己阻塞自己。
  • 场景:如果一个线程已经持有一个锁,然后在同步代码块中调用另一个也需要同一个锁的同步方法,它是可以成功获取锁的。

示例:

class ReentrantDemo {
    public synchronized void method1() {
        System.out.println("进入 method1");
        method2(); // 调用另一个同步方法
        System.out.println("退出 method1");
    }
    public synchronized void method2() {
        System.out.println("进入 method2");
        // do something...
        System.out.println("退出 method2");
    }
}
// 在某个线程中调用
ReentrantDemo demo = new ReentrantDemo();
demo.method1();

输出:

进入 method1
进入 method2
退出 method2
退出 method1

synchronized 不是可重入的,当 method1 执行到 method2 时,会因为试图再次获取已经持有的锁而陷入死锁,但由于它是可重入的,所以可以正常执行。


代码示例

示例1:使用 synchronized 块保护共享资源(银行取钱)

假设有一个银行账户,两个线程同时尝试取钱。

class BankAccount {
    private double balance = 1000.0;
    // 锁对象,可以是任意对象,但必须是共享的
    private final Object lock = new Object();
    public void withdraw(double amount) {
        // 使用 synchronized 块来保护关键代码
        synchronized (lock) {
            if (balance >= amount) {
                System.out.println(Thread.currentThread().getName() + " 正在取款: " + amount);
                try {
                    // 模拟处理时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + " 余额不足,取款失败");
            }
        }
        // synchronized 块外的代码可以被其他线程同时执行
    }
}
public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        // 创建两个线程,模拟两个人同时取钱
        Thread t1 = new Thread(() -> account.withdraw(600), "张三");
        Thread t2 = new Thread(() -> account.withdraw(600), "李四");
        t1.start();
        t2.start();
    }
}

可能输出:

张三 正在取款: 600.0
张三 取款成功,剩余余额: 400.0
李四 余额不足,取款失败

在这个例子中,synchronized (lock) 确保 if 判断、sleepbalance -= amount 这几个操作是原子性的,不会被另一个线程打断。t1 已经进入同步块并开始取款,t2 必须等待 t1 完成并释放 lock 对象后才能进入。

示例2:synchronized 块 vs. synchronized 方法

class Counter {
    private int count = 0;
    // 使用 synchronized 方法
    public synchronized void incrementSyncMethod() {
        count++;
    }
    // 使用 synchronized 块,锁对象是 this
    public void incrementSyncBlock() {
        synchronized (this) {
            count++;
        }
    }
}

在这个简单的例子中,incrementSyncMethod()incrementSyncBlock() 的效果是完全相同的,因为 synchronized 方法的锁对象就是 this


最佳实践和注意事项

  1. 锁的范围要尽可能小:只同步真正需要同步的代码块,而不是整个方法,这样可以减少线程阻塞的时间,提高程序的并发性能。
  2. 避免锁定 StringBoxed 基本类型
    • 不要用 String:因为 Java 的字符串常量池可能会导致两个看似不同的 String 对象指向同一个内存地址,或者两个不同的字符串因为 intern() 而成为同一个锁,引发意想不到的死锁问题。
    • 不要用 Integer, Long 等包装类:因为它们的值在 -128127 之间会被缓存,也可能导致不同线程使用同一个锁对象。
    • 最佳实践:专门为同步创建一个 private final Object lock = new Object(); 对象,或者直接同步共享资源本身(如果它是对象类型)。
  3. 不要在 synchronized 块内调用外部方法:如果在同步块内调用的方法可能会长时间运行、阻塞或获取其他锁,这会严重影响并发性能,甚至可能导致死锁
  4. 注意死锁:如果多个线程互相等待对方持有的锁,就会形成死锁,线程 A 锁住了对象 1 并等待对象 2,而线程 B 锁住了对象 2 并等待对象 1。

特性 描述
目的 保证多线程环境下共享数据的一致性和完整性。
原理 通过内置锁(监视器 Monitor)实现,同一时间只有一个线程能持有锁。
语法 synchronized (锁对象) { ... }
优点 - 简单易用,由 JVM 保证原子性、可见性和有序性。
- 是可重入锁,避免死锁。
缺点 - 性能开销比 java.util.concurrent.locks.Lock 稍大。
分享:
扫描分享到社交APP
上一篇
下一篇