Java 多线程同步:模拟售票、年会入场案例详解

在多线程编程中,线程安全问题是需要重点关注的。当多个线程同时访问共享资源时,如果不进行同步处理,就会出现数据错误。本文将通过模拟售票和年会入场两个典型案例,详细介绍 Java 多线程同步机制,帮助你理解并解决线程安全问题。

1. 模拟售票系统

**题目:**请编写程序,不使用任何同步技术,模拟三个窗口同时卖 100 张票的情况,运行并打印结果,观察到错误的数据,并解释出现错误的原因。

代码(未同步)

public class TicketSale implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (tickets > 0) {
            try {
                Thread.sleep(100); // 模拟其他操作耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + '卖出了第' + tickets-- + '张票');
        }
    }

    public static void main(String[] args) {
        TicketSale ticketSale = new TicketSale();

        new Thread(ticketSale, '窗口1').start();
        new Thread(ticketSale, '窗口2').start();
        new Thread(ticketSale, '窗口3').start();
    }
}

运行结果:

窗口1卖出了第100张票
窗口1卖出了第98张票
窗口2卖出了第99张票
窗口1卖出了第97张票
窗口3卖出了第96张票
...

**错误原因:**多个线程同时操作 tickets 变量,导致数据错误。例如,当 tickets 为 99 时,多个线程可能同时读取到该值,并将其减 1,最终导致卖出的票数超过 100 张。

**解决方法:**使用同步技术来保证对 tickets 变量的访问是互斥的,即同一时间只有一个线程可以访问该变量。

同步代码块

public class TicketSale implements Runnable {
    private int tickets = 100;
    private final Object lock = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100); // 模拟其他操作耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + '卖出了第' + tickets-- + '张票');
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        TicketSale ticketSale = new TicketSale();

        new Thread(ticketSale, '窗口1').start();
        new Thread(ticketSale, '窗口2').start();
        new Thread(ticketSale, '窗口3').start();
    }
}

同步方法

public class TicketSale implements Runnable {
    private int tickets = 100;

    @Override
    public synchronized void run() {
        while (tickets > 0) {
            try {
                Thread.sleep(100); // 模拟其他操作耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + '卖出了第' + tickets-- + '张票');
        }
    }

    public static void main(String[] args) {
        TicketSale ticketSale = new TicketSale();

        new Thread(ticketSale, '窗口1').start();
        new Thread(ticketSale, '窗口2').start();
        new Thread(ticketSale, '窗口3').start();
    }
}

Lock 锁

public class TicketSale implements Runnable {
    private int tickets = 100;
    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100); // 模拟其他操作耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + '卖出了第' + tickets-- + '张票');
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TicketSale ticketSale = new TicketSale();

        new Thread(ticketSale, '窗口1').start();
        new Thread(ticketSale, '窗口2').start();
        new Thread(ticketSale, '窗口3').start();
    }
}

2. 模拟年会入场

**题目:**某公司组织年会,会议入场时有两个入口,在入场时每位员工都能获取一张双色球彩票,假设公司有 100 个员工,利用多线程模拟年会入场过程,并分别统计每个入口入场的人数,以及每个员工拿到的彩票的号码。

线程运行后打印格式如下:

1 编号为: 2 的员工 从后门 入场! 拿到的双色球彩票号码是:[17, 24, 29, 30, 31, 32, 07]
2 编号为: 1 的员工 从后门 入场! 拿到的双色球彩票号码是:[06, 11, 14, 22, 29, 32, 15]
3 //.....
4 从后门入场的员工总共: 13 位员工
5 从前门入场的员工总共: 87 位员工

代码(同步代码块)

import java.util.Arrays;
import java.util.Random;

public class DoubleColorBallUtil {
    // 产生双色球的代码
    public static String create() {
        String[] red = {'01', '02', '03', '04', '05', '06', '07', '08', '09', '10',
                '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '2
                4', '25', '26', '27', '28', '29', '30', '31', '32', '33'};
        //创建蓝球
        String[] blue = '01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16'.s
                plit(',');
        boolean[] used = new boolean[red.length];
        Random r = new Random();
        String[] all = new String[7];
        for (int i = 0; i < 6; i++) {
            int idx;
            do {
                idx = r.nextInt(red.length);//0‐32
            } while (used[idx]);//如果使用了继续找下一个
            used[idx] = true;//标记使用了
            all[i] = red[idx];//取出一个未使用的红球
        }
        all[all.length - 1] = blue[r.nextInt(blue.length)];
        Arrays.sort(all);
        return Arrays.toString(all);
    }
}

public class Entrance implements Runnable {
    private static final int TOTAL_EMPLOYEES = 100;
    private static final int FRONT_DOOR_EMPLOYEES = 87;
    private static final int BACK_DOOR_EMPLOYEES = 13;
    private static final Object lock = new Object();
    private static int frontDoorCount = 0;
    private static int backDoorCount = 0;

    private final int id;

    public Entrance(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        for (int i = 0; i < TOTAL_EMPLOYEES; i++) {
            String lottery = DoubleColorBallUtil.create();
            synchronized (lock) {
                if (id == 1) {
                    System.out.printf('%d 编号为: %d 的员工 从后门 入场! 拿到的双色球彩票号码是:%s%n', i + 1, Thread.currentThread().getId(), lottery);
                    backDoorCount++;
                } else {
                    System.out.printf('%d 编号为: %d 的员工 从前门 入场! 拿到的双色球彩票号码是:%s%n', i + 1, Thread.currentThread().getId(), lottery);
                    frontDoorCount++;
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Entrance(1), '后门').start();
        new Thread(new Entrance(2), '前门').start();

        try {
            Thread.sleep(1000); // 等待两个线程执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.printf('从后门入场的员工总共: %d 位员工%n', backDoorCount);
        System.out.printf('从前门入场的员工总共: %d 位员工%n', frontDoorCount);
    }
}

总结

本文通过售票、年会入场等经典案例,详细介绍了 Java 多线程同步机制,包括使用同步代码块、同步方法、Lock 锁解决线程安全问题,并附带完整代码示例。建议你动手尝试运行代码,加深对多线程同步的理解。

在实际开发中,选择合适的同步方式取决于具体的需求,需要根据实际情况进行判断。例如,如果需要在多个线程之间进行更复杂的协调,则可以使用 Lock 锁,它提供了更灵活的控制机制。

Java 多线程同步:模拟售票、年会入场案例详解

原文地址: https://www.cveoy.top/t/topic/oe9q 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录