Java 线程同步与线程间通信
Java 线程同步与线程间通信
线程同步概述
核心问题:竞争状态与线程安全
当多个线程同时访问共享资源(临界资源)时,可能导致资源数据被破坏,这种现象称为竞争状态。若一个类的对象在多线程环境中不会引发竞争状态,则该类为线程安全的。
示例:未同步的账户存款问题
多个线程向同一账户存入1元,因未同步导致最终余额小于预期(理论100,实际可能远小)。
package com.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Jing61
*/
public class AccountWithoutSync {
//临界资源
private static Account account = new Account();
private static class Account {
private int balance = 0;
public int getBalance() {
return balance;
}
//为了故意放大数据破坏的可能性,采用下列语句,其实可用 balance += amount;代替
public void deposit(int amount) {
int newBalance = balance + amount;
try {
// 模拟处理时间
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance = newBalance;
}
}
/**
* 该任务向账户存入指定金额
*/
private static class AddPenyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 100; i++) executor.execute(new AddPenyTask());
executor.shutdown();//关闭执行器,不再接收新的任务
//等待线程池中所有的任务完毕
while(!executor.isTerminated());
System.out.println("账户余额:" + account.getBalance());
}
}
可能的运行结果:

问题原因
- 线程1读取余额(0)→ 计算新余额(1)→ 休眠。
- 线程2读取相同余额(0)→ 计算新余额(1)→ 休眠。
- 线程1唤醒后写入余额(1)→ 线程2唤醒后覆盖写入(1)。
- 最终两个线程的操作仅生效一次,导致数据错误。

在步骤1中,任务1从账户中获取余额数目。在步骤2中,任务2从账户中获取同样数目的余额。在步骤3中,任务1向账户写入一个新余额。在步骤4中,任务2也向该账户写入一个新余额。这个场景的效果就是任务1什么都没做,因为在步骤4中,任务4覆盖了任务1的结果。很明显,问题是任务1和任务2以一种会引起冲突的方式访问一个公共资源。这是多线程中的一个普遍问题,称为竞争状态(race condition)。如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的(thread-safe)。
线程同步实现方式
方式一:synchronized 关键字
synchronized 用于标记临界区(需同步的代码/方法),通过加锁机制保证同一时间只有一个线程访问临界区,分为同步方法和同步代码块。
同步方法
直接在方法上添加 synchronized,锁对象为当前实例(实例方法)或类对象(静态方法)。
package com.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Jing61
*/
public class AccountWithSync {
//临界资源
private static Account account = new Account();
private static class Account{
private int balance = 0;
public int getBalance() {
return balance;
}
/**
* 临界区:整个deposit
* synchronized防止多个线程同时今日临界区——一次只有一个线程可以访问该方法
* @param amount 金额
*
*/
public synchronized void deposit(int amount) {
//可以使用balance += amount;
int newBalance = balance + amount;
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance = newBalance;
}
}
/**
* 该任务向账户存入指定金额
*/
private static class AddPenyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 100; i++)
executor.execute(new AddPenyTask());
executor.shutdown();//关闭执行器,不再接收新的任务
//等待线程池中所有的任务完毕
while(!executor.isTerminated()) {}
System.out.println("账户余额:" + account.getBalance());
}
}
同步代码块
仅同步方法中的部分代码,锁对象可自定义(需保证多个线程使用同一锁),灵活性更高,并发能力更强。
package com.thread;
/**
* @author Jing
*/
public class CustomThreadTest {
public static void main(String[] args) {
//创建10个线程,代表10个售票窗口
for(var i = 0; i < 10; i++) {
var thread = new CustomThread("窗口-" + (i + 1));
thread.start();//启动线程
}
}
}
/**
* 由于Thread类实现了Runnable接口,
* 所以可以定义一个类扩展自Thread类,并且覆盖run方法。
*
*/
class CustomThread extends Thread{
private static int ticket = 100;//共享资源,存在线程安全问题
public CustomThread() {}
public CustomThread(String name) {
super(name);//Thread(String):指定线程名称
}
@Override
public void run() {
try {
while(true) {
synchronized (CustomThread.class) { // 必须是锁住类,不能锁住对象,锁住对象只是锁住当前对象
if(ticket <= 0)
break;
//Thread.currentThread():获取当前运行的线程
System.out.println(Thread.currentThread().getName() + "正在售出编号为 " + ticket-- + " 的火车票");
Thread.sleep(10);//模拟销售一张火车票的时间为1ms
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized 锁机制说明
- 实例方法:锁为调用方法的对象,不同对象的锁相互独立。
- 静态方法:锁为当前类的 Class 对象,所有实例共享同一锁。
- 同步代码块:锁为括号内的对象(需确保多线程共用该对象)。
- 线程获取锁后执行临界区,其他线程需阻塞等待锁释放。
方式二:显式锁(Lock 接口)
JDK 5 提供的 java.util.concurrent.locks.Lock 接口,是 synchronized 的增强版,解决了 synchronized 的局限性(单一锁类型、锁只能在代码块/方法首尾获取释放、线程只能阻塞等待)。
核心优势
- 支持多种锁类型(如读写锁)。
- 可在一个方法加锁、另一个方法解锁。
- 提供
tryLock()方法,获取不到锁时可选择等待、回退或超时放弃。
主要实现类
ReentrantLock:重入锁,功能与synchronized类似,灵活性更高。ReentrantReadWriteLock:读写锁,读操作共享、写操作排他,适合读多写少场景。
代码示例:ReentrantLock 实现账户同步
package com.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Jing61
*/
public class AccountWithLock {
//临界资源
private static Account account = new Account();
private static class Account{
private static Lock lock = new ReentrantLock();
private int balance = 0;
public int getBalance() {
return balance;
}
/**
* 锁方式
*/
public void deposit(int amount) {
lock.lock();//获取锁
try {
//可以使用balance += amount;
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
}
/**
* 该任务向账户存入指定金额
*
*/
private static class AddPenyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 100; i++)
executor.execute(new AddPenyTask());
executor.shutdown();//关闭执行器,不再接收新的任务
//等待线程池中所有的任务完毕
while(!executor.isTerminated()) {}
System.out.println("账户余额:" + account.getBalance());
}
}
注意事项
- 获得锁后一定要释放锁。
- 锁的释放最好放在
finally块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。
线程间通信
线程间通信用于协调相互依赖的线程执行(如“生产者-消费者”模式),主要通过“锁+条件”或传统的内置监视器实现。
方式一:Lock + Condition(推荐)
Condition 是 Lock 接口的配套工具,通过 Lock.newCondition() 创建,提供线程等待/唤醒的精细化控制。
核心方法
await():当前线程释放锁并进入等待状态,直到被唤醒。signal():唤醒一个等待该条件的线程。signalAll():唤醒所有等待该条件的线程。
示例:存款与取款线程协作
取款线程需等待存款线程存入足够金额后再执行,存款线程存入后通知取款线程。
package com.lock;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程间相互协作——通过条件实现线程间的通信
* 条件是通过调用Lock对象的newCondition()方法创建的对象
* await():让当前线程进入等待,直到条件发生
* signal():唤醒一个等待的线程
* signalAll():唤醒所有等待的线程
*
* 启动两个任务:一个任务向账户中存钱,另一个任务向账户中提款。当取钱金额大于账户余额,提款线程必须等待。
* 不管什么时候,只要向账户中存入一笔钱,存钱线程必须通知提款线程重新尝试
*
* 使用一个有条件的锁 newDeposit 。一个条件对应一个锁。在等待和通知状态之前,线程必须先获取该条件的锁。
* @author Jing61
*/
public class ThreadCooperation {
private static Account account = new Account();
private static class Account{
//创建锁
private static Lock lock = new ReentrantLock();
//创建条件
private static Condition newDeposit = lock.newCondition();
//余额
private int balance = 0;
public void withdraw(int amount) {
lock.lock();
try {
while(balance < amount) {// 当余额小于取款金额,必须等待存款任务条件的发生
System.out.println("等待存款任务的完成.....");
newDeposit.await();
}
balance -= amount;
System.out.println("取款完成,取款金额:" + amount);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
System.out.println("存入一笔金额:" + amount);
newDeposit.signalAll();
} finally {
lock.unlock();
}
}
/*
* public int getBalance() { return balance; }
*/
}
/**
* 存款任务
*
*/
private static class DepositTask implements Runnable{
@Override
public void run() {
try {
while(true) {
account.deposit((int)(Math.random() * 100) + 1);
Thread.sleep(5);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class WithdrawTask implements Runnable{
@Override
public void run() {
while(true) {
account.withdraw((int)(Math.random() * 100) + 1);
}
}
}
public static void main(String[] args) {
var executor = Executors.newFixedThreadPool(2);
executor.execute(new WithdrawTask());
executor.execute(new DepositTask());
executor.shutdown();
}
}
方式二:传统内置监视器(wait/notify)
JDK 5 之前的线程通信方式,基于 synchronized 锁的内置监视器实现,依赖 Object 类的 wait()、notify()、notifyAll() 方法。
核心规则
- 需在
synchronized方法/代码块中调用,否则抛出IllegalMonitorStateException。 wait():释放当前对象的锁,线程进入等待队列。notify():唤醒等待队列中的一个线程,使其重新竞争锁。notifyAll():唤醒等待队列中的所有线程。
示例:传统方式实现存款取款协作
package com.lock;
import java.util.concurrent.Executors;
/**
* @author Jing61
*/
public class TraditionalCooperation {
private static Account account = new Account();
public static class Account{
private int balance;
public synchronized void withdraw(int amount) {
try {
while(amount > balance) {
System.out.println("余额不足,终止当前线程并且释放对象的锁");
wait(); //必须先获取锁(在同步代码块或者方法中进行调用,否则IllegalMonitorStateException)
}
balance -= amount;
System.out.println("取款金额:" + amount + "当前余额:" + balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(int amount) {
balance += amount;
System.out.println("存款金额:" + amount + "当前余额:" + balance);
notify();//通知一个等待的线程重新获取锁并恢复执行
}
/**
* 取款任务
*
*/
private static class WithdrawTask implements Runnable{
@Override
public void run() {
while(true) {
int amount = (int)(Math.random() * 10000 + 1);
account.withdraw(amount);
}
}
}
private static class DepositTask implements Runnable{
@Override
public void run() {
try {
while(true) {
int amount = (int)(Math.random() * 10000 + 1);
account.deposit(amount);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
var executor = Executors.newFixedThreadPool(2);
executor.execute(new DepositTask());
executor.execute(new WithdrawTask());
executor.shutdown();
}
}
}
两种通信方式对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| Lock + Condition | 支持多条件、唤醒精准、灵活性高 | 代码稍复杂,需手动释放锁 |
| synchronized + wait/notify | 代码简洁、无需手动释放锁 | 仅支持单条件、唤醒不精准 |

浙公网安备 33010602011771号