本文纲要
- 线程安全问题 – 卖票案例实现
- 线程安全问题 – 原因分析
- 同步代码块
- 锁对象的唯一性
- 同步方法
Lock锁- 死锁
卖票案例实现
1 ) 需求
模拟电影院售票:全场共 100 张票,有 3 个窗口 同时卖票。要求使用多线程来设计程序。
2 ) 基础设计
- 三个线程代表三个窗口。
- 共享数据为 100 张票。
- 使用实现
Runnable接口的方式创建线程,保证多个线程操作同一个任务对象。
3 ) 初始代码(存在安全问题)
下面的代码通过 Runnable 共享一个 Ticket 对象,每个线程在 run() 中循环卖票。为了更真实地模拟出票耗时,我们加入 Thread.sleep(100)。但此时尚未加锁,会暴露线程安全问题。
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket <= 0) {
break;
} else {
try {
Thread.sleep(100); // 模拟出票耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()
" 在卖票, 还剩下 " + ticket + " 张票");
}
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket(); // 只创建一个任务对象
Thread t1 = new Thread(ticket, "窗口一");
Thread t2 = new Thread(ticket, "窗口二");
Thread t3 = new Thread(ticket, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
运行结果会出现 重复票 和 负数票 的现象。这是典型的线程安全问题。
原因分析
1 ) 为什么出现重复票?
我们通过下图分析三个线程的执行交错情况。
原因:多个线程在“判断 → sleep → 减减 → 打印”过程中交替执行,导致多个线程读到了同一个 ticket 值,减减后打印出相同的数。
2 ) 为什么出现负数票?
当 ticket = 1 时,三条线程同样可能交错执行。假设流程如下:
根本原因:多条语句在操作共享数据时,未能保证 原子性,被其他线程穿插执行。
同步代码块
1 ) 解决思想
将操作共享数据的代码 锁起来,任一时刻只允许一个线程进入执行,执行完释放锁后下一个线程才能进入。
2 ) 语法格式
synchronized(锁对象) {
// 操作共享数据的代码
}
- 锁对象可以是任意对象,但多个线程必须 使用同一把锁。
- 当线程进入同步块,锁自动关闭;执行完代码块,锁自动打开。
3 ) 改进代码
在 Ticket 类中引入一把锁 obj = new Object(),用 synchronized 包裹卖票逻辑:
public class Ticket implements Runnable {
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 多个线程使用同一把锁
if (ticket <= 0) {
break; // 卖完结束
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()
" 在卖票, 还剩下 " + ticket + " 张票");
}
}
}
}
}
测试类不变,此时运行结果不再出现重复票或负数票,线程安全问题得以解决。
项目结构
threadmodule/src/com/wb/threaddemo9/
├── Ticket.java
└── Demo.java
锁对象的唯一性
如果使用 继承 Thread 的方式创建线程,由于每个线程是一个独立对象,使用 this 作为锁就不再唯一。下面演示这个错误及修复方法。
1 ) 错误示例(使用 this 作为锁)
public class MyThread extends Thread {
private int ticketCount = 100; // 非静态,各对象独立
@Override
public void run() {
while (true) {
synchronized (this) { // this 是各自不同的线程对象
if (ticketCount <= 0) {
break;
} else {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
ticketCount--;
System.out.println(getName() + " 在卖票, 还剩下 " + ticketCount + " 张票");
}
}
}
}
}
测试类:
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
由于 t1 和 t2 是两个对象,synchronized(this) 实际是两把不同的锁,同步失效,仍会出现重复票和负数票。
2 ) 正确做法:使用静态变量 + 静态锁
public class MyThread extends Thread {
private static int ticketCount = 100; // 共享票数
private static Object obj = new Object(); // 唯一的锁
@Override
public void run() {
while (true) {
synchronized (obj) { // 所有线程共用 obj 锁
if (ticketCount <= 0) {
break;
} else {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
ticketCount--;
System.out.println(getName() + " 在卖票, 还剩下 " + ticketCount + " 张票");
}
}
}
}
}
测试类:
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
此时共享数据 ticketCount 和锁 obj 都是 static,所有线程共用同一把锁,线程安全得以保证。
项目结构
threadmodule/src/com/wb/threaddemo10/
├── MyThread.java
└── Demo.java
同步方法
synchronized 还可以直接修饰方法,将整个方法体变为同步代码。
1 ) 同步成员方法
- 锁对象是
this。 - 适用于
Runnable方式,因为多个线程共享同一个Runnable对象。
public class MyRunnable implements Runnable {
private int ticketCount = 100;
// 同步方法,锁对象为 this
private synchronized boolean sellTicket() {
if (ticketCount == 0) {
return true; // 卖完
} else {
try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
ticketCount--;
System.out.println(Thread.currentThread().getName()
" 在卖票, 还剩下 " + ticketCount + " 张票");
return false;
}
}
@Override
public void run() {
while (true) {
if ("窗口一".equals(Thread.currentThread().getName())) {
if (sellTicket()) break; // 调用同步方法
}
// ... 窗口二可以使用同步代码块,保证同一把锁(this)
}
}
}
2 ) 同步静态方法
- 锁对象是 类的字节码文件,即
类名.class。 - 静态方法中没有
this,只能使用类锁。
示例:
private static synchronized boolean synchronizedMethod() {
// 锁对象为 MyRunnable.class
// 卖票逻辑
}
在与同步代码块混用时,若需共用一把锁,则同步块应使用 MyRunnable.class。
synchronized (MyRunnable.class) {
// 与静态同步方法使用同一把锁
}
测试类:
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr, "窗口一");
Thread t2 = new Thread(mr, "窗口二");
t1.start();
t2.start();
此时同步方法和同步代码块使用的都是同一把锁(对象锁 this 或类锁 .class),线程安全。
3 ) 项目结构
threadmodule/src/com/wb/threaddemo11/
├── MyRunnable.java
└── Demo.java
Lock 锁
JDK 5 起提供了更灵活的锁接口 java.util.concurrent.locks.Lock。
可手动控制 加锁 (lock()) 和 释放锁 (unlock())。
为避免异常导致锁无法释放,unlock() 通常放在 finally 块中。
1 ) 使用 ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock(); // 手动加锁
if (ticket <= 0) {
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName()
" 在卖票, 还剩下 " + ticket + " 张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 手动释放,务必执行
}
}
}
}
测试类与同步代码块方式相同,运行效果一致,线程安全。
2 ) 与 synchronized 对比
| 特性 | synchronized | Lock |
|---|---|---|
| 加锁/释放 | 自动 | 手动 (lock() / unlock()) |
| 灵活性 | 较低 | 可中断、可尝试获取锁 |
| 异常处理 | 自动释放 | 必须在 finally 中释放 |
| 出现版本 | Java 早期 | JDK 5 |
3 ) 项目结构
threadmodule/src/com/wb/threaddemo12/
├── Ticket.java
└── Demo.java
死锁
当多个线程相互持有对方所需的锁时,会产生 死锁,所有线程永久阻塞。
1 ) 死锁产生的条件
- 有多个锁(如 A 锁、B 锁)。
- 线程1 先获取 A 锁,再试图获取 B 锁;线程2 先获取 B 锁,再试图获取 A 锁。
2 ) 死锁代码演示
public class DeadLockDemo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(() -> {
while (true) {
synchronized (objA) {
synchronized (objB) {
System.out.println("线程1 正在运行");
}
}
}
}).start();
new Thread(() -> {
while (true) {
synchronized (objB) {
synchronized (objA) {
System.out.println("线程2 正在运行");
}
}
}
}).start();
}
}
运行后程序将卡死,两个线程都无法继续执行。
3 ) 避免死锁
- 尽量减少锁的嵌套。
- 如果需要多个锁,确保所有线程 按相同的顺序 获取锁。
4 ) 项目结构
threadmodule/src/com/wb/threaddemo13/
└── Demo.java
总结
- 线程安全问题源于多个线程并发操作共享数据。
- 解决方式:将操作共享数据的代码变为 原子操作,即加锁。
- Java 提供了:
synchronized同步代码块(灵活指定锁对象)synchronized同步方法(成员方法锁this,静态方法锁类.class)Lock接口(ReentrantLock)实现手动控制锁
- 锁对象的 唯一性 是线程安全的前提。
- 死锁由 锁嵌套 导致,开发中应避免。

476

被折叠的 条评论
为什么被折叠?



