什么是线程安全问题?
两个示例
ThreadSafetyTest
/**
* 线程安全性测试类 - 展示并发问题及解决方案
* ────────────────────────────────────────────────────────────────────
* 场景:两个线程对同一个数进行递减操作
* 问题:由于非原子性操作导致的数据不一致
*
* @author uluckyXH
* @date 2024-12-23
*/
@Slf4j
public class ThreadSafetyTest {
static class Counter {
private int count = 100;
/**
* 不安全的递减操作
* 直接对count操作,并添加延迟使问题更容易复现
*/
public void decreaseUnsafe() {
try {
// 在减少之前记录当前值,这样可以看到操作前后的变化
int before = count;
Thread.sleep(10); // 模拟耗时操作
count--;
// 打印详细信息:线程名称、操作前值、操作后值
System.out.printf("[%s] %d -> %d%n",
Thread.currentThread().getName(), before, count);
} catch (InterruptedException e) {
log.error("线程异常", e);
}
}
/**
* 安全的递减操作
* synchronized 修饰方法,使用的是this对象锁
*/
public synchronized void decreaseSafe() {
try {
int before = count;
Thread.sleep(10); // 相同的延迟
count--;
System.out.printf("[%s] %d -> %d%n",
Thread.currentThread().getName(), before, count);
} catch (InterruptedException e) {
log.error("线程异常", e);
}
}
public int getCount() {
return count;
}
}
/**
* 测试线程不安全的情况
* ──────────────────────────────────────────
* 增加线程数和循环次数,使问题更容易复现
*/
@Test
public void testUnsafeCounter() throws InterruptedException {
Counter unsafeCounter = new Counter();
// 创建5个线程,每个线程减20次,总共还是减100
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 20; j++) {
unsafeCounter.decreaseUnsafe();
}
}, "Thread-" + (i + 1));
}
// 启动所有线程
for (Thread t : threads) {
t.start();
}
// 等待所有线程完成
for (Thread t : threads) {
t.join();
}
System.out.println("\n不安全的计数器最终值: " + unsafeCounter.getCount());
}
/**
* 测试线程安全的情况
* ──────────────────────────────────────────
*/
@Test
public void testSafeCounter() throws InterruptedException {
Counter safeCounter = new Counter();
// 同样创建5个线程
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 20; j++) {
safeCounter.decreaseSafe();
}
}, "Thread-" + (i + 1));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("\n安全的计数器最终值: " + safeCounter.getCount());
}
}
CinemaTest
/**
* 同步锁示例 - 模拟电影院售票窗口
* ────────────────────────────────────────────────────────────────────
* 场景:
* 1. 多个窗口同时售票
* 2. 使用同一把锁确保不会超卖
*
* @author uluckyXH
* @date 2024-12-23 06:02:07
*/
@Slf4j
public class CinemaTest {
static class Cinema {
// 总票数
private int tickets = 100;
// 售票窗口共用一把锁
private final Object TICKET_LOCK = new Object();
/**
* 售票方法
* synchronized(TICKET_LOCK) 确保同一时刻只有一个窗口能卖票
*/
public void sellTicket(String windowName) {
synchronized(TICKET_LOCK) { // 进入同步块,需要先获得锁
try {
if (tickets > 0) {
// 模拟出票过程的耗时
Thread.sleep(10);
tickets--;
System.out.printf("[%s] 卖出一张票,剩余: %d%n",
windowName, tickets);
} else {
System.out.printf("[%s] 票已售罄!%n", windowName);
}
} catch (InterruptedException e) {
log.error("售票异常", e);
}
} // 退出同步块,释放锁
}
public int getTickets() {
return tickets;
}
}
@Test
public void testTicketSelling() throws InterruptedException {
/*
* 关键点:
*
* 所有窗口共用同一把锁(TICKET_LOCK)
* 一个窗口在卖票时,其他窗口必须等待
* 确保了票不会超卖,最终票数准确
* 这就像现实生活中:
*
* 五个窗口共用一本票簿(共享资源)
* 给了一支笔(锁)
* 同一时刻只有拿到笔的窗口才能在票簿上登记(同步)
* 用完笔后要还给其他窗口用(释放锁)
* 这样就能确保不会出现多个窗口同时卖同一张票的情况!
*/
Cinema cinema = new Cinema();
// 模拟5个售票窗口
Thread[] windows = new Thread[5];
for (int i = 0; i < 5; i++) {
windows[i] = new Thread(() -> {
// 每个窗口尝试卖30张票
for (int j = 0; j < 30; j++) {
cinema.sellTicket(Thread.currentThread().getName());
}
}, "窗口" + (i + 1));
}
// 启动所有窗口
for (Thread window : windows) {
window.start();
}
// 等待所有窗口结束营业
for (Thread window : windows) {
window.join();
}
System.out.println("\n营业结束,剩余票数: " + cinema.getTickets());
}
}
synchronized 的不同使用方式和对应的锁
📍 1. synchronized 加在普通方法上
对象锁 - 即 this 锁
等同于 synchronized(this) { ... }
- 锁住的是当前对象实例
- 同一个对象的其他同步方法也会被阻塞
- 不同对象之间互不影响
📍 2. synchronized 加在静态方法上
类锁 - 即 Class 锁
等同于 synchronized(Class.class) { ... }
- 锁住的是这个类的 Class 对象
- 这个类的所有对象共享同一把锁
- 类的所有静态同步方法都会被阻塞
📍 3. synchronized(this) 代码块
对象锁
和同步普通方法是同一把锁
锁住的是当前对象实例
- 同步范围更精确,性能更好
- 其他同步方法会被阻塞
📍 4. synchronized(Object) 代码块
对象锁
可以是任意 Object 实例
- 锁住的是传入的对象
- 更灵活,可以自定义锁对象
- 常用于同步特定资源
📍 5. synchronized(Class.class) 代码块
类锁
和同步静态方法是同一把锁
- 锁住的是类对象
- 影响所有实例
- 静态方法中常用
🔑 关键区别:
1. 对象锁:每个对象都有自己的锁
2. 类锁:整个类只有一把锁
⚡ 记忆技巧:
- 普通方法/this → 对象锁(一个对象一把锁)
- 静态方法/Class → 类锁(所有对象共享一把锁)
🚫 注意事项:
1. 对象锁和类锁是不同的锁
2. 类锁和对象锁互不干扰
3. 类的所有对象共享类锁
4. 每个对象都有自己的对象锁
synchronized死锁问题
DeadLockDemoTest
/**
* 死锁示例 - 哲学家就餐问题
* ────────────────────────────────────────────────────────────────────
* 问题描述:
* 假设有两个哲学家和两支筷子。每个哲学家都需要两支筷子才能吃饭。
* 当他们同时拿起一支筷子,并等待另一支筷子时,就会发生死锁。
* 死锁形成的过程:
* 1. 哲学家1拿起左边的筷子1
* 2. 同时哲学家2拿起左边的筷子2
* 3. 哲学家1等待筷子2(被哲学家2拿着)
* 4. 哲学家2等待筷子1(被哲学家1拿着)
* 5. 死锁形成!
*
* @author uluckyXH
* @date 2024-12-23 06:31:33
*/
public class DeadLockDemoTest {
/**
* 筷子类
* ────────────────────────────────────
* 筷子是关键资源,使用synchronized锁住筷子对象
*/
static class Chopstick {
private final String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
/**
* 会造成死锁的哲学家类
* ────────────────────────────────────
* 死锁原因:两个哲学家都先拿左边的筷子,再等右边的筷子
*/
static class Philosopher implements Runnable {
private final String name;
private final Chopstick left; // 左手边的筷子
private final Chopstick right; // 右手边的筷子
public Philosopher(String name, Chopstick left, Chopstick right) {
this.name = name;
this.left = left;
this.right = right;
}
// 模拟思考
private void think() throws InterruptedException {
System.out.println(name + " 正在思考人生...");
Thread.sleep(1000); // 思考1秒钟
}
// 模拟吃饭
private void eat() throws InterruptedException {
System.out.println(name + " 正在优雅地进餐~");
Thread.sleep(1000); // 吃饭1秒钟
}
@Override
public void run() {
try {
while (true) {
think(); // 先思考
// ===== 产生死锁的关键部分 =====
synchronized (left) { // 先锁住左边的筷子
System.out.println(name + " 拿起了 " + left);
Thread.sleep(500); // 停顿一下,让死锁更容易发生
synchronized (right) { // 尝试锁住右边的筷子
// 如果能执行到这里,说明成功拿到两支筷子
System.out.println(name + " 拿起了 " + right);
eat(); // 开始吃饭
System.out.println(name + " 放下了 " + right);
}
System.out.println(name + " 放下了 " + left);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* 解决方案:保证拿筷子的顺序性
* ────────────────────────────────────
* 核心思想:所有哲学家都按照筷子编号顺序拿筷子
* 例如:总是先拿编号小的筷子,再拿编号大的筷子
*/
static class OrderedPhilosopher implements Runnable {
private final String name;
private final Chopstick first; // 编号小的筷子
private final Chopstick second; // 编号大的筷子
public OrderedPhilosopher(String name, Chopstick c1, Chopstick c2) {
this.name = name;
// 通过比较筷子名称,确保拿筷子的顺序一致
if (c1.name.compareTo(c2.name) < 0) {
this.first = c1; // c1的编号小,先拿c1
this.second = c2; // c2的编号大,后拿c2
} else {
this.first = c2; // c2的编号小,先拿c2
this.second = c1; // c1的编号大,后拿c1
}
}
@Override
public void run() {
try {
while (true) {
System.out.println(name + " 正在思考人生...");
Thread.sleep(1000);
// ===== 解决死锁的关键部分 =====
synchronized (first) { // 总是先拿编号小的筷子
System.out.println(name + " 拿起了 " + first);
Thread.sleep(500);
synchronized (second) { // 再拿编号大的筷子
System.out.println(name + " 拿起了 " + second);
System.out.println(name + " 正在优雅地进餐~");
Thread.sleep(1000);
System.out.println(name + " 放下了 " + second);
}
System.out.println(name + " 放下了 " + first);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* 演示死锁的测试方法
* ────────────────────────────────────
* 通过让两个哲学家相反顺序拿筷子,必然导致死锁
*/
@Test
public void testDeadLock() throws InterruptedException {
System.out.println("=== 开始死锁演示 ===");
System.out.println("提示:当程序停止输出时,说明死锁已经发生!");
// 创建两支筷子
Chopstick c1 = new Chopstick("筷子1");
Chopstick c2 = new Chopstick("筷子2");
// 创建两个哲学家,注意他们拿筷子的顺序不同!
Thread p1 = new Thread(new Philosopher("哲学家1", c1, c2)); // 先c1后c2
Thread p2 = new Thread(new Philosopher("哲学家2", c2, c1)); // 先c2后c1
p1.start();
p2.start();
Thread.sleep(5000);
System.out.println("\n程序已经死锁,两个哲学家都在等待对方的筷子...");
}
/**
* 演示解决方案的测试方法
* ────────────────────────────────────
* 通过确保所有哲学家按相同顺序拿筷子来避免死锁
*/
@Test
public void testOrderedSolution() throws InterruptedException {
System.out.println("=== 开始测试解决方案 ===");
System.out.println("提示:观察两个哲学家是否能正常交替就餐");
Chopstick c1 = new Chopstick("筷子1");
Chopstick c2 = new Chopstick("筷子2");
// 创建两个哲学家,他们会按照相同的顺序拿筷子
Thread p1 = new Thread(new OrderedPhilosopher("哲学家1", c1, c2));
Thread p2 = new Thread(new OrderedPhilosopher("哲学家2", c1, c2));
p1.start();
p2.start();
Thread.sleep(10000);
System.out.println("\n程序正常运行,没有发生死锁!");
}
}