👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中… 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏别名《在2B工作中寻求并发是否搞错了什么》
文章目录
- 前言
- 入门
- 什么是synchronized?
- 为什么用synchronized?
- 怎么用synchronized?
- 修饰方法
- 修饰代码块
- syncrhonized在框架源码中的使用
- Vector 和 Hashtable
- StringBuffer
- 总结
- synchronized的优点
- synchronized的缺点
- synchronized的适用场景
- 后话
- 参考
前言
这一篇同样是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<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.classpublic static synchronized void increment() {count++;}
}
使用场景:多线程操作静态变量(如全局计数器)。
修饰代码块
可以指定任意对象作为锁,灵活性更高。
1.锁是当前对象实例(this)
public void doSomething() {// 同步代码块:锁是当前对象实例synchronized (this) {// 需要同步的代码}
}
2.锁是类对象(Class)
public void doSomething() {// 同步代码块:锁是 Counter.classsynchronized (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
进行了大量优化,如锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),在低竞争场景下性能较好。
- JVM 对
- 内存可见性:
- 通过
synchronized
的锁机制,可以保证线程对共享变量的修改对其他线程可见(遵循happens-before
原则)。
- 通过
- 结构化锁:
- 锁的获取和释放必须成对出现,减少编码错误。
synchronized的缺点
- 性能开销:
- 在高竞争场景下,
synchronized
会升级为重量级锁,导致线程阻塞和上下文切换,性能较差。
- 在高竞争场景下,
- 锁粒度较粗:
- 如果直接修饰方法,可能导致锁的范围过大,降低并发性能。
- 不可中断:
- 线程在等待锁时无法被中断(
Lock
接口支持可中断的锁获取)。
- 线程在等待锁时无法被中断(
- 功能有限:
- 不支持尝试获取锁(
tryLock
)、超时获取锁、公平锁等高级功能(ReentrantLock
支持)。
- 不支持尝试获取锁(
- 嵌套锁可能导致死锁:
- 如果多个线程以不同顺序获取嵌套锁,可能导致死锁。
synchronized的适用场景
- 低竞争场景:
- 当线程竞争不激烈时,
synchronized
的性能足够好,且实现简单。
- 当线程竞争不激烈时,
- 简单的线程同步需求:
- 如计数器、单例模式、简单的生产者-消费者模型等。
- 需要快速实现线程安全:
- 在开发初期或对性能要求不高的场景下,
synchronized
是快速实现线程安全的有效工具。
- 在开发初期或对性能要求不高的场景下,
- 需要保证内存可见性:
- 当多个线程需要共享变量时,
synchronized
可以确保变量的修改对其他线程可见。
- 当多个线程需要共享变量时,
- 锁粒度较粗的场景:
- 如果锁的范围不需要特别精细,直接修饰方法即可满足需求。
后话
什么就结束了?别急,这个synchronized
的原理,也是很有说法的。
点上关注,主播马上带你们深入学习synchronized
。
最近主播的下班时间都是准点,这下沉淀爽了🤗
参考
Chapter 17. Threads and Locks