什么是线程安全问题?

image-sape.png


两个示例

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程序正常运行,没有发生死锁!");
    }
}

跟github copilot愉快的记录

synchronized锁有哪些?