【Java并发】【synchronized】适合初学者体质入门的synchronized

发布于:2025-03-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中… 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏别名《在2B工作中寻求并发是否搞错了什么》

前言

这一篇同样是sychronzied的入门篇,不会涉及底层原理和实现,很适合初学者的学习。好了, 不废话了,让我们马上开始吧🤗

入门

什么是synchronized?

synchronized 是 Java 中的关键字,用于实现线程同步,确保多个线程在访问共享资源时不会发生冲突。它可以修饰方法或代码块,保证同一时间只有一个线程执行被修饰的代码,从而避免数据不一致问题。

为什么用synchronized?

  1. 解决竞态条件(Race Condition)

问题:多个线程同时修改同一个共享变量时,操作顺序可能被打乱,导致结果不可预测。
如果两个线程同时调用 increment(),可能发生以下情况:线程 A 读取 count=0 → 线程 B 也读取 count=0 → 两者都改为 1 → 最终 count=1(实际应为 2)。

// 有问题的count++
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // 这行代码实际包含三步:读取值 → 修改值 → 写回值
    }
} 

解决:使用synchronized解决,synchronized确保同一时刻只有一个线程能执行increment(),避免值被覆盖。

public class Counter {
    private int count = 0;
    
    // 添加 synchronized 关键字
    public synchronized void increment() {
        count++; // 现在是一个原子操作
    }
}
  1. 保证内存可见性

问题:线程有自己的工作内存(缓存),修改共享变量后可能不会立即同步到主内存,导致其他线程看到旧值。

// 存在问题的flag读和写方法
public class VisibilityDemo {
    private boolean flag = false;
    
    public void setFlag() {
        flag = true; // 线程 A 修改 flag
    }
    
    public void checkFlag() {
        while (!flag); // 线程 B 可能永远看不到 flag 变为 true
    }
}

解决:使用synchronized解决,通过 synchronized 的锁机制,强制线程从主内存读取最新值,避免可见性问题。

public class VisibilityDemo {
    private boolean flag = false;
    
    // 添加 synchronized 保证可见性
    public synchronized void setFlag() {
        flag = true; // 修改后立即同步到主内存
    }
    
    // 同样用 synchronized 读取
    public synchronized boolean checkFlag() {
        return flag; // 从主内存读取最新值
    }
}
  1. 避免原子性破坏

问题:某些操作看似是“一步完成”,但实际由多个底层指令组成(如 i++),多线程环境下可能被分割执行,比如下面的转账例子。

// 非原子操作
public void transfer(Account from, Account to, int amount) {
    if (from.balance >= amount) {
        from.balance -= amount; // 非原子操作
        to.balance += amount;    // 可能被其他线程打断
    }
}

解决:使用synchronized解决。这里更安全的做法是使用全局锁(如定义一个 final Object lock),避免嵌套锁导致的死锁风险。

public void transfer(Account from, Account to, int amount) {
    // 锁定两个账户对象,避免并发修改
    synchronized (from) {
        synchronized (to) {
            if (from.balance >= amount) {
                from.balance -= amount;
                to.balance += amount;
            }
        }
    }
}
  1. 协调多线程的有序访问

问题:多个线程需要按特定顺序操作共享资源(如生产者-消费者模型)。

public class Queue {
    private List<Integer> list = new ArrayList<>();
    
    public synchronized void add(int value) {
        list.add(value); // 生产者线程添加数据
    }
    
    public synchronized int remove() {
        return list.remove(0); // 消费者线程移除数据
    }
}

解决synchronized 确保同一时刻只有一个线程操作队列,避免并发异常。

public class Queue {
    private List<Integer> list = new ArrayList<>();
    
    // 添加和移除方法均用 synchronized 保护
    public synchronized void add(int value) {
        list.add(value);
    }
    
    public synchronized int remove() {
        if (!list.isEmpty()) {
            return list.remove(0);
        }
        return -1; // 或抛异常
    }
}

怎么用synchronized?

我们可以看到,JLS已经规定了,可以修饰在方法和代码块中。
在这里插入图片描述

修饰方法

1.修饰实例方法
锁是当前对象实例(this),同一对象的多个线程调用该方法时会互斥。

public class Counter {
    private int count = 0;

    // 修饰实例方法:锁是当前对象实例
    public synchronized void increment() {
        count++;
    }
}

使用场景:多个线程操作同一个对象的实例方法时(如单例对象的资源修改)。

2.修饰静态方法
锁是类的 Class 对象(如 Counter.class),所有线程调用该类的静态方法时会互斥。

public class Counter {
    private static int count = 0;

    // 修饰静态方法:锁是 Counter.class
    public static synchronized void increment() {
        count++;
    }
}

使用场景:多线程操作静态变量(如全局计数器)。

修饰代码块

可以指定任意对象作为锁,灵活性更高。

1.锁是当前对象实例(this)

public void doSomething() {
    // 同步代码块:锁是当前对象实例
    synchronized (this) {
        // 需要同步的代码
    }
}

2.锁是类对象(Class)

public void doSomething() {
    // 同步代码块:锁是 Counter.class
    synchronized (Counter.class) {
        // 需要同步的代码
    }
}

3.锁是任意对象

private final Object lock = new Object();

public void doSomething() {
    // 同步代码块:锁是自定义对象
    synchronized (lock) {
        // 需要同步的代码
    }
}

syncrhonized在框架源码中的使用

Vector 和 Hashtable

这些类在 JDK 早期版本中通过synchronized修饰所有公共方法实现线程安全。例如Vectoradd() 方法:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

缺点:锁粒度粗(整个方法加锁),性能较低,现代开发中多被ConcurrentHashMapCollections.synchronizedList()替代。

StringBuffer

StringBuffer的方法均用synchronized修饰以实现线程安全,例如append()

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

总结

synchronized的优点

  1. 简单易用
    • 只需在方法或代码块上添加关键字即可实现线程同步,无需手动管理锁的获取和释放。
  2. 自动释放锁
    • 当同步代码块执行完毕或发生异常时,锁会自动释放,避免死锁风险。
  3. 内置锁优化
    • JVM 对 synchronized 进行了大量优化,如锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),在低竞争场景下性能较好。
  4. 内存可见性
    • 通过 synchronized 的锁机制,可以保证线程对共享变量的修改对其他线程可见(遵循 happens-before 原则)。
  5. 结构化锁
    • 锁的获取和释放必须成对出现,减少编码错误。

synchronized的缺点

  1. 性能开销
    • 在高竞争场景下,synchronized 会升级为重量级锁,导致线程阻塞和上下文切换,性能较差。
  2. 锁粒度较粗
    • 如果直接修饰方法,可能导致锁的范围过大,降低并发性能。
  3. 不可中断
    • 线程在等待锁时无法被中断(Lock 接口支持可中断的锁获取)。
  4. 功能有限
    • 不支持尝试获取锁(tryLock)、超时获取锁、公平锁等高级功能(ReentrantLock 支持)。
  5. 嵌套锁可能导致死锁
    • 如果多个线程以不同顺序获取嵌套锁,可能导致死锁。

synchronized的适用场景

  1. 低竞争场景
    • 当线程竞争不激烈时,synchronized 的性能足够好,且实现简单。
  2. 简单的线程同步需求
    • 如计数器、单例模式、简单的生产者-消费者模型等。
  3. 需要快速实现线程安全
    • 在开发初期或对性能要求不高的场景下,synchronized 是快速实现线程安全的有效工具。
  4. 需要保证内存可见性
    • 当多个线程需要共享变量时,synchronized 可以确保变量的修改对其他线程可见。
  5. 锁粒度较粗的场景
    • 如果锁的范围不需要特别精细,直接修饰方法即可满足需求。

后话

什么就结束了?别急,这个synchronized的原理,也是很有说法的。

点上关注,主播马上带你们深入学习synchronized

最近主播的下班时间都是准点,这下沉淀爽了🤗

参考

Chapter 17. Threads and Locks