10.2.3 两种实现多线程方式的对比分析中的售票案例极有可能碰到“意外”情况,如一张票被打印多次,或打印出的票号为 0 甚至负数等等。这些“意外”都是由多线程操作共享资源 ticket 导致的线程安全问题。

    接下来针对

    1. public class example05 {
    2. public static void main(String[] args) {
    3. //创建线程对象 TicketWindow 并开启
    4. TicketWinodw task = new TicketWinodw();
    5. new Thread(task, "窗口1").start();
    6. new Thread(task, "窗口2").start();
    7. new Thread(task, "窗口3").start();
    8. new Thread(task, "窗口4").start();
    9. }
    10. }
    11. class TicketWinodw implements Runnable{
    12. private int tickets = 100;
    13. @Override
    14. public void run() {
    15. while (true) { //通过死循环语句打印语句
    16. if (tickets > 0) {
    17. Thread thread = Thread.currentThread(); //获取当前线程
    18. String thread_name = thread.getName(); //获取当前线程的名称
    19. System.out.println(thread_name + " 正在发售第 " + tickets-- + " 张票");
    20. }
    21. }
    22. }
    23. }

    进行修改,模拟 4 个窗口出售 10 张票,并在售票的代码中使用 sleep()方法,令每次售票时线程休眠 10 毫秒,如下所示。

    1. public class example10 {
    2. public static void main(String[] args) {
    3. //创建线程对象 TicketWindow 并开启
    4. TicketWinodw task = new TicketWinodw();
    5. new Thread(task, "窗口1").start();
    6. new Thread(task, "窗口2").start();
    7. new Thread(task, "窗口3").start();
    8. new Thread(task, "窗口4").start();
    9. }
    10. }
    11. class TicketWinodw implements Runnable {
    12. private int tickets = 10;
    13. @Override
    14. public void run() {
    15. while (tickets > 0) {
    16. try {
    17. Thread.sleep(10); //线程休眠 10 毫秒
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. System.out.println(Thread.currentThread().getName() + " 正在发售第 " + tickets-- + " 张票");
    22. }
    23. }
    24. }

    运行结果如下所示。
    QQ截图20200709202122.png
    在上图中,最后打印售出的票,第一张出现了两次,最后居然出现了 0 和负数,这种现象是不应该出现的,因为在售票程序中做了判断,只有当票号大于 0 时才会进行售票且每张票只能出售一次。运行结果中之所以出现这样的意外是因为多线程在售票时出现了安全问题。接下来对问题进行简单的分析。

    在售票程序的 while 循环中添加了 sleep()方法,这样模拟了售票过程中线程的延迟。由于线程有延迟,但票号减为 1时,假设线程“窗口 1”此时出售 1号票,对票号进行判断后,进入 while 循环,在售票之前通过 sleep()方法让线程休眠,这时线程“窗口 2”会进行售票,由于此时票号仍为 1,因此线程二也会进入循环。同理,4 个线程都会进入 while 循环,休眠结束后,4 个线程都会进行售票,这样就相当于将票号减了 4 次,导致结果中出现了 0、-1 这样的票号。
    **
    至于为什么会打印两次“第 10 张票”,不知道,现在的我还没有能力解决它。

    Question
    这里有疑问,之前也隐隐约约察觉到了。上面这个例子大概思路是,开启四个线程,建立一个 while 循环,循环内 4 个线程每运行一次,线程休眠 10 毫秒,每次线程执行都会将 tickets 减 1,直到 tickets <= 0,结束循环。

    按照定义,应该一共进行四次循环。每次循环里,四个线程并发执行、tickets—。到了第三次循环,tickets 最小值为 2,然后休眠 10 毫秒后进入第四次循环,tickets 最终值为 -1。可从上图可以看到,出错了,最小值为 -2。

    于是开始找原因,发现在第一次循环就出了错误,“第 10 张票”被“发售”了两次。

    从这本书里是肯定找不到答案的了,我猜测是因为处理器有多个核心的缘故。在处理多线程程序时,在非常短的时间内,CPU 的多个核心恰巧在同一时间点“发售”了同一张“票”,于是造成了第一次循环里出现的“第 10 张票”被发售了两次。

    2020/7/26 updata
    这他妈不是什么教材出了问题,这些“意外”就是由多线程操作共享资源 ticket 导致的线程安全问题。

    拓展阅读**