Java多线程:使用Lock解决售票并发安全问题
在Java多线程开发中,同步锁用于解决资源竞争导致的安全问题。本文以经典的窗口售票为例,对比未加锁导致的重复票现象,并详细展示如何利用 ReentrantLock 进行加锁与解锁操作,确保数据一致性。
结果或目标:解决多线程数据竞争
当多个线程同时访问共享资源时,若没有适当的同步机制,会导致数据错乱。例如在多线程售票场景中,可能出现一张票被卖出多次的情况。我们的目标是引入一种机制,确保同一时刻只有一个线程能执行售票逻辑。
准备条件
要实现基于 Lock 接口的同步控制,需要以下基础:
- JDK版本:建议使用 JDK 1.5 及以上版本,因为从该版本开始引入了
java.util.concurrent.locks包。 - 核心类:
java.util.concurrent.locks.ReentrantLock是Lock接口的常用实现类。 - 业务场景:创建一个模拟售票的 Runnable 任务,包含余票数计数器。
真实操作顺序
第1步:重现无锁环境下的并发问题
首先,我们构建一个多线程售票模型,观察不加锁时的运行结果。此步骤旨在验证并发问题的存在性。
- 创建内部类
Ticket实现Runnable接口,初始化总票数(如100张)。 - 在
run()方法中编写循环逻辑,模拟每0.2秒卖出一张票的过程。 - 在主线程中创建三个子线程,分别命名为“1号窗口”、“2号窗口”和“3号窗口”,并传入同一个
Ticket实例启动。
public class LockDemo1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "1号窗口").start();
new Thread(ticket, "2号窗口").start();
new Thread(ticket, "3号窗口").start();
}
static class Ticket implements Runnable {
private int tick = 100;
@Override
public void run() {
while (true) {
try {
// 模拟售票间隔
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "完成售票,余票是:" + --tick);
}
}
}
}
}
运行上述代码,控制台会出现明显的并发错误:部分票号为负数,或者多行输出显示相同的余票数(即超卖现象)。这是因为三个线程同时读取了相同的 tick 值并同时进行递减操作,缺乏原子性保护。
第2步:引入 Lock 实现同步控制
为了解决上述问题,我们需要引入显式锁机制。与 synchronized 不同,Lock 提供了更灵活的控制能力,但要求开发者手动管理锁的生命周期。
- 实例化锁对象:在
Ticket类中声明并初始化private Lock lock = new ReentrantLock();。 - 获取锁:在执行核心业务逻辑前,调用
lock.lock();获取锁。这会阻塞其他试图进入该代码段的线程。 - 释放锁:使用完毕后必须释放锁。**关键规范**:释放锁的操作必须放在
finally块中,以确保即使发生异常,锁也能被正确释放,避免死锁。
第3步:重构代码以应用 ReentrantLock
基于第一步的代码,增加锁逻辑后得到修正后的版本:
public class LockDemo2 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "1号窗口").start();
new Thread(ticket, "2号窗口").start();
new Thread(ticket, "3号窗口").start();
}
static class Ticket implements Runnable {
private int tick = 100;
// 创建可重入锁实例
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 上锁
lock.lock();
try {
// 业务逻辑:模拟耗时操作及判断
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "完成售票,余票是:" + --tick);
}
} finally {
// 必须释放锁,防止死锁
lock.unlock();
}
}
}
}
}
在修改后的代码中,每一次售票操作都被包裹在 lock.lock() 和 finally { lock.unlock(); } 之间。这意味着只要有一个线程正在执行售票,其他线程就必须等待,从而保证了 --tick 操作的原子性。
再次运行代码,可以观察到输出结果中票号递减有序,不再出现负数票或重复票,成功解决了并发安全问题。
完成判断与优势分析
完成判断标准:程序运行时控制台打印的余票数严格递减且无负数,无卡死现象(证明锁能正常释放)。
Lock vs synchronized:相比传统的 synchronized 关键字,Lock 具有以下显著优势:
- 灵活性:开发者可以自行控制加锁和解锁的时机,支持非阻塞尝试获取锁(
tryLock)和定时等待,更适合复杂的并发场景。 - 状态感知:可以通过
isLocked()等方法查询锁的状态。 - 公平性:在构造函数中指定
true参数即可创建公平锁,按请求顺序分配锁,避免线程饥饿。
简要总结
在Java并发编程中,ReentrantLock 是一种强大且灵活的同步工具。它通过显式的 lock() 和 unlock() 方法配合 finally 块,有效解决了多线程环境下的数据竞争问题。使用时务必注意资源的释放,以避免不可预知的系统故障。
以上就是Java多线程中使用Lock同步锁解决并发安全的详细内容,更多关于Java并发编程与线程同步的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









