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

分类: 365bet指数 时间: 2026-01-02 17:24:08 作者: admin

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

前言

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

入门

什么是synchronized?

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

为什么用synchronized?

解决竞态条件(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++; // 现在是一个原子操作

}

}

保证内存可见性

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

// 存在问题的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; // 从主内存读取最新值

}

}

避免原子性破坏

问题:某些操作看似是“一步完成”,但实际由多个底层指令组成(如 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;

}

}

}

}

协调多线程的有序访问

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

public class Queue {

private List 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 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修饰所有公共方法实现线程安全。例如Vector的 add() 方法:

public synchronized boolean add(E e) {

modCount++;

ensureCapacityHelper(elementCount + 1);

elementData[elementCount++] = e;

return true;

}

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

StringBuffer

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

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

return this;

}

总结

synchronized的优点

简单易用:

只需在方法或代码块上添加关键字即可实现线程同步,无需手动管理锁的获取和释放。

自动释放锁:

当同步代码块执行完毕或发生异常时,锁会自动释放,避免死锁风险。

内置锁优化:

JVM 对 synchronized 进行了大量优化,如锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),在低竞争场景下性能较好。

内存可见性:

通过 synchronized 的锁机制,可以保证线程对共享变量的修改对其他线程可见(遵循 happens-before 原则)。

结构化锁:

锁的获取和释放必须成对出现,减少编码错误。

synchronized的缺点

性能开销:

在高竞争场景下,synchronized 会升级为重量级锁,导致线程阻塞和上下文切换,性能较差。

锁粒度较粗:

如果直接修饰方法,可能导致锁的范围过大,降低并发性能。

不可中断:

线程在等待锁时无法被中断(Lock 接口支持可中断的锁获取)。

功能有限:

不支持尝试获取锁(tryLock)、超时获取锁、公平锁等高级功能(ReentrantLock 支持)。

嵌套锁可能导致死锁:

如果多个线程以不同顺序获取嵌套锁,可能导致死锁。

synchronized的适用场景

低竞争场景:

当线程竞争不激烈时,synchronized 的性能足够好,且实现简单。

简单的线程同步需求:

如计数器、单例模式、简单的生产者-消费者模型等。

需要快速实现线程安全:

在开发初期或对性能要求不高的场景下,synchronized 是快速实现线程安全的有效工具。

需要保证内存可见性:

当多个线程需要共享变量时,synchronized 可以确保变量的修改对其他线程可见。

锁粒度较粗的场景:

如果锁的范围不需要特别精细,直接修饰方法即可满足需求。

后话

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

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

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

参考

Chapter 17. Threads and Locks