• 1. 第6章 线程 (时间:3次课,6学时)
  • 2. 第6章 线程 教学提示:计算机世界要想真正地反映现实世界,必须解决事情的同步问题,即解决程序实现多线程的问题。 因此可编写有几条执行路径的程序,使得程序能够同时执行多个任务,借此实现多线程运行。Java语言的一大特点就是内置对多线程的支持。 本章主要介绍:Java中的线程作用机制 、线程的实现方法、线程的控制和线程的同步与死锁 。
  • 3. 第6章 线程 6.1 线程简介 6.2 线程的实现方法 6.3 线程的控制 6.4 Java的多线程实例 6.5 线程的同步与死锁 6.6 ThreadLocal问题 6.7 课后练习
  • 4. 6.1 线程简介 6.1.1 程序、进程和线程 6.1.2 线程的生命周期 6.1.3 线程的优先级及其调度6.1.4 线程组
  • 5. 6.1 线程简介 对于许多编程人员来说,线程并不是那么的陌生。但是在Java中,线程的作用机制又是如何工作的呢?本节将重点介绍Java中的线程作用机制。
  • 6. 6.1.1 程序、进程和线程 程序是由若干条语句组成的语句序列,是一段静态代码。 进程是程序的一次动态执行过程。 需要特别指出的是,进程不仅包括程序代码,还包括系统资源。即一个进程既包括其所要执行的指令,也包括执行指令所需的任何系统资源,如CPU、内存空间等。不同进程所占用的系统资源相对独立。
  • 7. 6.1.1 程序、进程和线程 线程又是一个抽象的概念,它包含了一个计算机执行传统程序时所做的每一件事情。线程是一种在CPU上调度的程序状态,它在某一瞬时看来只是计算过程的一个状态。一个进程中的所有线程共享该进程的状态,它们存储在相同的内存空间中,共享程序的代码和数据。所以当其中一个线程改变了进程的变量时,那么其他线程下次访的将是改变后的变量。 多线程是指同一个应用程序中有多个顺序流同时执行。在一个程序中可以同时运行多个不同的线程来执行不同的任务,各个线程并行地完成各自的任务。浏览器就是一个典型的多线程例子。
  • 8. 6.1.2 线程的生命周期 每个Java程序都有一个默认的主线程。对于应用程序,主线程是main()方法执行的路径。图6-1说明线程的生命周期及其状态转换。
  • 9. 6.1.2 线程的生命周期 图6-1 线程的状态转换
  • 10. 6.1.2 线程的生命周期 从图6-1中可以看出:一个线程从创建到消亡的整个生命周期中,总是处于下面5个状态中的某个状态。 1. 新建状态 通过new命令创建一个Thread类或其子类的线程对象时,该线程对象处于新建状态。创建一个新的线程对象可以用下面的语句实现: Thread thread=new Thread(); 该语句是最简单的创建线程的语句,但该语句创建的线程是一个空的线程对象,系统还未对这个线程分配任何资源。
  • 11. 6.1.2 线程的生命周期 2. 就绪状态 该状态又可称为可运行状态。处于新建状态的线程可通过调用start()方法启动该线程。Start()方法产生了线程运行需要的系统资源。启动后的线程将进入线程就绪队列排队等待CPU服务,此时线程已经具备了运行的条件,一旦它获得CPU等资源时就可以脱离创建它的主线程而独立运行。
  • 12. 6.1.2 线程的生命周期 3. 运行状态 当处于就绪状态的线程被调度并获得CPU资源时,使进入运行状态。每个线程对象都有一个重要的run()方法,run()方法定义了该线程的操作和功能。当线程对象被调度执行时,它将自动调用其run()方法并从第一条语句开始顺次执行。
  • 13. 6.1.2 线程的生命周期4. 阻塞状态 又称不可运行状态。当发生下列情况之一时,线程就进入阻塞状态。 (1) 等待输入输出操作完成。 (2) 线程调用wait()方法等待一个条件变量。 (3) 调用了该线程的sleep()休眠方法。 (4) 调用了suspend()挂起方法。
  • 14. 6.1.2 线程的生命周期5. 消亡状态 消亡状态又称死亡状态,当调用run()方法结束后,线程就进入消亡状态,这是线程的正常消亡。另外线程还可能被提前强制性消亡。不管何种情况,处于消亡状态的线程不具有继续运行的能力。
  • 15. 6.1.3 线程的优先级及其调度 线程被创建之后,每个Java线程的优先级都在Thread.MIN_PRIORITY(常量1)和Thread.MAX_PRIORITY(常量10)的范围之内。每个新建线程的默认优先级都为Thread.NORM_PRIORITY(常量5)。可以用方法int getPriority()来获得线程的优先级,同时也可以用方法 void setPriority(int p)在线程被创建后改变线程的优先级。 一个线程将始终保持运行状态,直到出现下列情况:由于I/O(或其他一些原因)而使该线程阻塞;调用sleep、wait、join 或yield 方法也将阻塞该线程;更高优先级的线程将抢占该线程;时间片的时间期满而退出运行状态或线程执行结束。
  • 16. 6.1.3 线程的优先级及其调度 【例6.3】综合使用线程的方法来控制线程的工作举例,程序如下。 //一个实现Runnable接口的SimpleRunnable类 class SimpleRunnable implements Runnable { protected String message; protected int iterations; public SimpleRunnable(String msg, int iter) { message = msg; iterations = iter; } public void run() { for (int i=0; i
  • 17. 6.1.3 线程的优先级及其调度 //ThreadExample类运行这个线程 public class ThreadExample { public static void main(String args[]) { Thread t1, t2; t1 = new Thread(new SimpleRunnable("Thread 1", 10)); t2 = new Thread(new SimpleRunnable("Thread 2", 15)); System.out.println("T1 p is: " + t1.getPriority()); System.out.println("T2 p is: " + t2.getPriority()); t2.setPriority(7); System.out.println("T2 after set p is: " + t2.getPriority()); t2.yield(); System.out.println("T2 after yield p is: " + t2.getPriority()); t1.start(); t2.start(); } }
  • 18. 6.1.3 线程的优先级及其调度 运行结果如下: T1 p is: 5 T2 p is: 5 T2 after set p is: 7 T2 after yield p is: 7 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 1
  • 19. 6.1.3 线程的优先级及其调度 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 1 Thread 2 Thread 2 Thread 2 Thread 2 Thread 2
  • 20. 6.1.4 线程组 线程组(Thread Group)允许把一组线程统一管理。例如,可以对一组线程同时调用interrupt()方法,中断这个组中所有线程的运行。创建线程组的构造方法为: ThreadGroup(String groupName); 线程组构造方法的字符串参数用来标识该线程组,并且它必须是独一无二的。线程组对象创建后,可以将各个线程添加到该线程组,在线程构造方法中指定所加入的线程组: Thread(groupName, threadName);
  • 21. 6.1.4 线程组 线程组下可以设子线程组,默认创建线程组将成为与当前线程同组的子线程组。当线程组中的某个线程由于一个异常而中止运行时,ThreadGroup类的uncaughtException (Threadt,Throwable e)方法将会打印这个异常的堆栈跟踪记录。
  • 22. 6.2 线程的实现方法 6.2.1 继承Thread类 6.2.2 实现Runnable接口
  • 23. 6.2 线程的实现方法 要使用多线程实现程序的流程控制,需要首先创建线程,生成线程实例,然后需要控制线程的调度。线程的创建分两步:定义线程体和创建线程对象。线程体决定线程的功能,它由run()方法实现,系统通过调用线程的run()方法实现线程的具体功能。Java中可通过继承Thread类或实现Runnable接口这两种途径来构造线程的run()方法。
  • 24. 6.2.1 继承Thread类 可通过创建Thread类的子类并重写其中的run()方法来定义线程体以实现线程的具体功能,然后创建该子类的对象以创建线程。 Thread类在包java.lang中,它定义了Java程序中一个线程需要拥有的属性和方法。如表6-1和表6-2所示。
  • 25. 6.2.1 继承Thread类 表6-1 Thread类的属性
  • 26. 6.2.1 继承Thread类 表6-2 Thread类的构造方法和关键方法
  • 27. 6.2.2 实现Runnable接口 另一种实现多线程的方法是实现Runnable接口。Runnable接口只定义了一个方法——run()方法,该方法是一个抽象方法,所有实现Runnable接口的类都必须具体实现这个方法,为它提供方法体并定义具体操作。Runnable接口中的 run()方法与Thread 类中的run()方法一样,是被系统自动识别执行的。
  • 28. 6.2.2 实现Runnable接口 通过实现Runnable接口实现多线程的一般步骤如下。 (1) 创建实现Runnable接口的类ClassName。它的一般格式为: class ClassName implements Runnable { public void run() { //编写代码 } } (2) 创建Runnable类ClassName的对象。一般格式为: ClassName RunnableObject=new ClassName(); (3) 用带有Runnable参数的Thread类构造方法创建线程对象,对象RunnableObject作为构造方法的参数,作为新建线程的目标对象为线程提供 run()方法。如用表 8-2 中列出的构造方法Thread(Runnable target)创建线程对象: Thread ThreadObject=new Thread(RunnableObject);
  • 29. 6.2.2 实现Runnable接口Thread 类中除了上述构造方法带有Runnable 参数外,还有下面3个构造方法也带有Runnable参数。 Thread(Runnable target,String name) Thread(ThreadGroup group,Runnable target) Thread(ThreadGroup group ,Runnable target,String name) 其中Runnable参数是一个实现了Runnable接口的某个类的对象。第2个和第3个方法的第一个参数是指明新创建的线程属于哪一个线程组。
  • 30. 6.3 线程的控制 6.3.1 启动线程 6.3.2 线程休眠 6.3.3 中断线程
  • 31. 6.3 线程的控制 父线程通过创建Thread对象并调用其start方法来启动子线程。start方法使该对象成为一个准备运行的新线程。第一次轮到该线程执行时,JVM就会调用run方法。线程可以处于以下5种状态的任何一种状态:新建、就绪、运行、阻塞和消亡状态。 在Thread类里面,提供了一些控制多线程状态的方法,如表6-3所示。
  • 32. 6.3 线程的控制表6-3 线程的控制方法
  • 33. 6.3 线程的控制通过这些方法,线程的各个状态可以相互转换。转换的情况和Thread类的控制方法对线程的影响参见图6.3。 下面详细讲解这些方法的用法。 (1) start()启动线程 当Thread类的实例对象已经创建了,但没做任何事情时,就需要我们调用start()方法来启动线程。start()方法使Thread对象表示的虚拟CPU开始执行,也就是切换到就绪状态,在这之后,新线程开始执行可运行的run()方法。如果我们不是调用start()方法使线程执行,而是寄希望于直接调用run()方法来执行代码,实际上只是调用了一个名称为run()的方法,而不是一个线程。
  • 34. 6.3 线程的控制 1:start();2:stop();3:suspend();4:sleep();5:resume();6:yield();7:interrupt();8:等待处理资源 图6-3 线程的生命周期
  • 35. 6.3 线程的控制(2) stop()停止线程 stop()方法使线程停止,它激发死亡状态并且给出错误。但是,stop()方法只是Thread类的方法,当我们用Runnable接口创建线程时,我们是不能用stop()方法使线程停止的。 (3) suspend()暂停线程 suspend()方法也只是Thread类的方法,调用这个方法可以使线程处于封锁状态,直至我们调用resume()方法来唤醒它,suspend()方法可以被自己和别的线程调用,通常是被别的线程调用,比如被鼠标单击事件处理程序调用。暂停后,该线程不能做任何事情,甚至没有办法调用resume()方法使自己重新启动,直至有别的线程调用resume()方法使之恢复。
  • 36. 6.3 线程的控制(4) sleep()暂停线程 sleep()方法使控制流程暂停,在给定的—段时间内睡眠,所以特别适合需要在一定时间间隔内完成一个动作的线程。睡眠的时间由参数来决定,毫秒级和纳秒级分别对应两个方法: public static void sleep(long millis) throws InterruptException; //睡眠millis毫秒 public static void sleep (long millis,int nanos)throws InterruptException; //睡眠millis毫秒加上nanos纳秒
  • 37. 6.3 线程的控制(5) resume()恢复线程 调用resume()方法使线程重新启动。 (6) yield()使线程由运行状态转为就绪状态 如果要编写多个合作线程,则可能浪费CPU时间。 (7) interrupt()唤醒正在睡眠的线程 如果某个线程因调用了sleep()方法而暂停,可以调用interrupt()方法将之唤醒,后者用异常InterruptException来强制使sleep的调用提前返回。 另外,还有join()方法,调用join()方法后线程暂停工作,并一直等待直到线程停止。
  • 38. 6.3.1 启动线程 【例6.5】使用start方法启动线程,程序如下。 class ThreadBody implements Runnable { int i; public void run() { try { Thread.currentThread().sleep(100); System.out.println(Thread.currentThread().getName()); ThreadBody tb=new ThreadBody(); Thread th=new Thread(tb); th.start(); } catch(InterruptedException e) { e.printStackTrace(); } } } class Test
  • 39. 6.3.1 启动线程 { public static void main(String []args) { ThreadBody body=new ThreadBody(); Thread newThr=new Thread(body); newThr.start(); } } 运行结果如图6-4所示。 由图可知,这个程序是个相当于死循环的程序,因为它在线程里启动新的线程,这样无休止地启动,最终导致了死循环的结果。
  • 40. 6.3.1 启动线程 图6-4 运行结果(例6.5)
  • 41. 6.3.2 线程休眠 【例6.6】使用sleep方法使线程休眠,程序如下。 class EvenOdd extends Thread { private int f,delay; public EvenOdd(int first,int interval) { f=first; delay=interval; } public void run() { try { for(int i=f;i<=100;i+=2) { System.out.println(Thread.currentThread().getName()+" "+i); sleep(delay); }
  • 42. 6.3.2 线程休眠 } catch(InterruptedException e) { e.printStackTrace(); } } } class Test { public static void main(String []args)throws InterruptedException { EvenOdd th1=new EvenOdd(1,20); EvenOdd th2=new EvenOdd(0,30); th1.start(); th2.start(); th1.join(); th2.join(); System.out.println("Main thread done"); } }
  • 43. 6.3.2 线程休眠 运行结果如图6-5所示。 图6-5 运行结果(例6.6)
  • 44. 6.3.3 中断线程 中断一个线程意味着在完成其任务以前,停止线程正在进行的工作,即有效地中止当前操作。线程中断后是等待新的任务还是继续进行下一步操作将取决于应用程序。 这里就必须注意的问题提出了一些建议。 (1) 不要使用Thread.stop方法 尽管它的确可以中止一个正在运行的线程,由于它的安全问题而遭到了开发人员普遍的反对。这也可能意味着在未来的Java版本中它可能不会出现。 (2) 不建议使用Thread.interrupt Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。
  • 45. 6.3.3 中断线程 【例6.8】通过Thread.interrupt()中断线程举例,程序如下。 class Example1 extends Thread { public static void main( String args[] ) throws Exception { Example1 thread = new Example1(); System.out.println( "Starting thread..." ); thread.start(); thread.sleep( 3000 ); System.out.println( "Interrupting thread..." ); thread.interrupt(); Thread.sleep( 3000 ); System.out.println( "Stopping application..." ); System.exit( 0 ); } public void run() { while ( true )
  • 46. 6.3.3 中断线程 { System.out.println( "Thread is running..." ); long time = System.currentTimeMillis(); //当前系统毫秒级单位的时间,返回长整型的数 while ( System.currentTimeMillis()-time < 1000 ) { } } } } 运行结果如图6-7所示。 代码中通过让thread 线程sleep,再使用interrupt方法来中断线程,但实际上从运行结果上看线程并没有中断。
  • 47. 6.3.3 中断线程 图6-7 运行结果(例6.8)
  • 48. 6.4 Java的多线程实例 Java语言中,线程有如下特点。 (1) 在一个程序中而言,主线程的执行位置就是main。而其他线程执行的位置,程序员是可以自定义的。 (2) 每个线程执行其代码的方式都是依次顺序执行的。 (3) 一个线程执行其代码是与其他线程独立开来的。如果诸线程之间又相互协作的话,就必须采用一定的交互机制。 (4) 前面已经说过,线程是共享地址空间的,如果控制不当,这里很有可能出现死锁。关于死锁的概念将在下节介绍。
  • 49. 6.4 Java的多线程实例 【例6.10】继承Thread类举例,通过它讲述多线程的作用机制。程序如下。 class MultiThread { public static void main(String[] args) { MyThread mt=new MyThread(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); } } class MyThread extends Thread { int index=100; public void run()
  • 50. 6.4 Java的多线程实例 { while(true) { if(index>0) { try { Thread.sleep(10);//让线程等待10毫秒 System.out.println(Thread.currentThread().getName() +"卖了第"+index+"张饭票"); index--; } catch(InterruptedException e) { e.printStackTrace();//打印异常出现的轨迹 } } } } } 运行结果如图6-9所示。
  • 51. 6.4 Java的多线程实例 图6-9 运行结果(例6.10)
  • 52. 6.5 线程的同步与死锁 6.5.1 线程的同步 6.5.2 死锁 6.5.3 线程同步示例 6.5.4 设置线程优先级示例
  • 53. 6.5 线程的同步与死锁 在使用线程时,往往会出现意料不到的结果,为了解决这些问题,必须了解Java中线程的死锁和同步的问题。
  • 54. 6.5.1 线程的同步 通常来说,同时运行的线程需要共同数据,在这种情况下,就需要考虑其他线程对当前线程的影响 。 在Java中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有—个线程访问该对象。关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
  • 55. 6.5.1 线程的同步 引入互斥锁之后,需要使用wait(),notify()和notifyAll()方法来同步线程的执行,这些方法的使用说明如下。 (1) wait(),notify()和notifyAll()三种方法必须在已经持有锁的情况下执行,只能出现在synchronized作用的范围内。 (2) wait()方法的作用是释放已持有的锁,进入wait()队列。 (3) notify()的作用是唤醒wait队列中的第一个线程,并将该线程移入互斥锁申请队列。 (4) notifyAll()方法的作用是唤醒wait队列中的所有的线程,并将这些线程移入互斥锁申请队列。
  • 56. 6.5.1 线程的同步 Java中实现互斥锁是通过使用synchronized关键字,它包括两种用法:synchronized方法和synchronized块(也说同步方法和同步块)。 1. synchronized 方法 通过在方法声明中加入synchronized关键字来声明synchronized 方法。使用的格式为: public synchronized void accessVal(int newVal);
  • 57. 6.5.1 线程的同步2. synchronized 块 通过synchronized关键字来声明synchronized块。语法如下: synchronized(syncObject) { //允许访问控制的代码 }
  • 58. 6.5.2 死锁 多线程在使用互斥机制实现同步的时候,存在“死锁”的潜在危险。如果多个线程都是处于等待状态而无法被唤醒时,就构成死锁。例如一个线程进入对象ObjA上的监视器,而另一个线程进入对象ObjB上的监视器。如果ObjA中的线程试图调用ObjB上的任何 synchronized 方法,就将发生死锁。此时处于等待状态的多个线程占用系统资源,但又无法运行,因此不会释放自己的资源,由于系统资源有限,程序停止运行。 Java技术既不能发现死锁也不能避免死锁。所以编程时应注意死锁问题,尽量避免。
  • 59. 6.5.2 死锁 如何去解决死锁的问题,看起来是一件很头痛的事,因为它涉及CPU的时间片的分配等问题。最有效方法还是避免死锁的发生,一般有以下两种方法: 线程因为某个条件未满足而受阻,不能让其继续占有资源。 如果有多个对象需要互斥访问,应确定线程获得锁的顺序。
  • 60. 6.5.3 线程同步示例 【例6.13】线程同步示例。 public class Producer extends Thread { private CubbyHole cubbyhole; private int number; public Producer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { for (int i = 0; i < 10; i++) { cubbyhole.put(i); System.out.println("Producer #" + this.number + " put: " + i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } } }
  • 61. 6.5.3 线程同步示例 public class CubbyHole { private int contents; private boolean available = false; public synchronized int get() { while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; notifyAll(); return contents; } public synchronized void put(int value) { while (available == true) { try { wait(); } catch (InterruptedException e) { } }
  • 62. 6.5.3 线程同步示例 contents = value; available = true; notifyAll(); } } public class Consumer extends Thread { private CubbyHole cubbyhole; private int number; public Consumer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = cubbyhole.get(); System.out.println("Consumer #" + this.number + " got: " + value);
  • 63. 6.5.3 线程同步示例 } } } class ProducerConsumerTest { public static void main(String[] args) { CubbyHole c = new CubbyHole(); Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1); p1.start(); c1.start(); } } 运行结果如图6-12所示。
  • 64. 6.5.3 线程同步示例 图6-12 运行结果(例6.13)
  • 65. 6.5.3 线程同步示例本程序就好比一个卧底与一个情报人员专门约定在一个树洞通信一样。当卧底往树洞里投放了情报后,只有等着情报员将情报拿走了,才能继续往树洞里面投放情报,否则只有等待。 程序中定义了三个类,Producer、Consumer和CubbyHole三个类。 值得注意的是,Producer类和Consumer类所调用CubbyHole类的put和get方法都是同步方法,这样就保证了卧底和情报员在情报的投放和领取上保持了一致。
  • 66. 6.5.4 设置线程优先级示例 本节举例介绍如何设置线程的优先级。程序如下。 【例6.14】设置线程优先级。例子详见书188~191页 运行的结果如图6-13所示。
  • 67. 6.5.4 设置线程优先级示例 图6-13 运行结果(例6.14)
  • 68. 6.6 ThreadLocal问题 ThreadLocal类中有以下三个方法: Object get()。检索变量的当前线程的值。 protected Object initialValue()。可选的,如果线程未使用过某个变量,那么可以用这个方法来设置这个变量的初始值;它允许延迟初始化。 void set(Object value)。修改当前线程的值。
  • 69. 6.6 ThreadLocal问题 【例6.15】ThreadLocal的具体应用举例,程序如下。 import java.lang.ThreadLocal; import java.util.*; class DebugLogger { private static class ThreadLocalList extends ThreadLocal { public Object initialValue() { return new ArrayList(); } public List getList() { return (List) super.get(); } } private ThreadLocalList list = new ThreadLocalList(); private static String[] stringArray = new String[0]; public void clear() { list.getList().clear(); } public void put(String text) { list.getList().add(text); } public String[] get() { list.getList().toArray(stringArray); return stringArray; } }
  • 70. 6.7 课后练习 1. 填空题 (1) 可以通过__________和__________来编写一个线程类。 (2) 线程有__________、__________、__________和__________状态。 2. 选择题 (1) 一个线程想让另一个线程不能执行,它对第二个线程调用yield()方法,能实现吗? ( ) A. True B. False (2) 一个线程的run()方法代码如下: try{ sleep(100); }catch(InterruptedException e) {} 假设线程没有被中断,下列为真的是( )。 A. 代码不会被编译,因为异常不会在线程的run()方法中捕获 B. 在代码的第2 行,线程将停止运行,至多100ms 后恢复执行 C. 在代码的第2 行,线程将停止运行,恰好在100ms 恢复执行 D. 在代码的第2 行,线程将停止运行,在100ms 后的某个时间恢复执行
  • 71. 6.7 课后练习3. 判断题 (1) C和Java都是多线程语言。 ( ) (2) 如果线程死亡,它便不能运行。 ( ) 4. 简答题 (1) 创建线程的两种方法是什么? (2) 线程的四种状态是什么? 5. 操作题 (1) 编写一个Race类,它模拟兔子和乌龟之间的赛跑。用Math.random()方法使比赛更有趣。 (2) 编写一个StackTest类,其中实现后进先出的数据结构,并且线程是安全的,不会产生死锁。
  • 72. Q & A? Thanks!