----?为什么要用到多线程
CPU是以时间片的方式为进程分配CUP处理时间的,如果当一个进程同时要完成几件事的时候,如当从网上下载文件的时候,需要一边下载一边显示进度而且还要一边保存,如果按照单线程的思想,那么这种情况就只能一个完成之后才能继续下一个,如果将CPU看成有100个时间片的话,那么完成一件事可能只占了10%,这样剩下的就白白浪费了,没有充分的利用到CPU的资源。如果同时处理3件事的话,那CPU的利用率就提升到了30%,这样就大幅地提升了CPU的利用率。
总的一句话就是:CPU还是要花同样多的时间去完成所有的事情,但多线程可以让CPU掺插地同时做多件事情,在视觉上让用户觉得计算机在同时帮他处理多件事情,更好地改善用户体验。
----? 程序、进程和线程
程序:一段静态代码的集合。为完成特定的功能,编写的一组指令的集合。即指一段静态的代码,静态对象。
进程:程序的一次运行,或者是在运行一个程序。动态进程:有它自身的产生、存亡和销毁过程
程序是静态的,进程是动态的
线程:每一个进程都包含有多个线程,相当于进程的细化,每一个Java程序都隐含一个进程,即main方法
注意:多线程不是为了提高执行速度,而是提高应用程序的使用率.,线程和线程共享”堆内存和方法区内存”.栈内存是独立的,一个线程一个栈.
----? 创建多线程的方式
继承Thread类,实现Runnable接口,实现Callable接口,开辟线程池(线程池提供了线程队列,队列包含多个等待状态线程)
--->继承Thread类
public class ThreadTest01 { public static void main(String[] args) { //设置线程名字 Thread.currentThread().setName("main thread"); MyThread myThread = new MyThread(); myThread.setName("子线程:"); //开启线程 myThread.start(); for(int i=0; i<5; i++){ System.out.println(Thread.currentThread().getName()+i); } } } class MyThread extends Thread{ @Override public void run() { for(int i=0; i<10; i++){ System.out.println(Thread.currentThread().getName()+i); } } }
--->实现Runnable接口
public class ThreadTest02 { public static void main(String[] args) { Thread.currentThread().setName("main thread"); Thread myThread = new Thread(new MyRunnable()); myThread.setName("子线程:"); //开启线程 myThread.start(); for(int i=0; i<5; i++){ System.out.println(Thread.currentThread().getName()+i); } } } class MyRunnable implements Runnable{ @Override public void run() { for(int i=0; i<10; i++){ System.out.println(Thread.currentThread().getName()+i); } } }
以上两段程序输出内容相同,只是实现的方式不同
main thread0 子线程:0 子线程:1 子线程:2 子线程:3 子线程:4 子线程:5 子线程:6 子线程:7 子线程:8 子线程:9 main thread1 main thread2 main thread3 main thread4
!!!!Thread和Runnable区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
--->实现Callable接口
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ThreadTest03 { public static void main(String[] args) { //执行Callable 方式,需要FutureTask 实现实现,用于接收运算结果 FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); new Thread(futureTask).start(); //接收线程运算后的结果 Integer sum; try { sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; for(int i=0; i<100; i++){ sum += i; } return sum; } }
相较于实现Runnable 接口的实现,方法可以有返回值,并且抛出异常
--->线程池
线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提交了响应速度
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; //线程池实现 public class ThreadPoolExecutorTest { public static void main(String[] args) { //创建线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); ThreadPool threadPool = new ThreadPool(); for(int i =0;i<5;i++){ //为线程池分配任务 executorService.submit(threadPool); } //关闭线程池 executorService.shutdown(); } } class ThreadPool implements Runnable { @Override public void run() { for(int i = 0 ;i<10;i++){ System.out.println(Thread.currentThread().getName() + ":" + i); } }
}
----? 线程状态
----? 线程的调度与控制
线程的调度模型分为: 分时调度模型和抢占式调度模型,Java使用抢占式调度模型
通常我们的计算机只有一个 CPU,CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。在单 CPU 的机器上线程不是并行运行的,只有在多个 CPU 上线程才可以并行运行。Java 虚拟机要负责线程的调度,取得 CPU 的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型,Java 使用抢占式调度模型。
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
那么CPU的抢占式调度是如何实现?
CPU是通过设置时间中断来实现抢占机制的,在进入保护模式之前,先初始化中断向量表,在时钟中断入口处放置任务切换代码,然后设置好时钟中断的时间。
当某一线程的执行用光了时间片时,时钟中断产生,CPU转去执行中断处的任务切换代码,保存当前线程状态,得到并恢复下一个线程的状态,然后转去执行那个线程,以此类推。
为了尽可能地提高CPU的利用率,当某一线程必须进入等待状态,如果要获取某一低速外设的数据时,这里的读操作程序会主动放弃当前的没有用光的时间片,主动调用任务切换代码,把CPU的时间让给其他已准备好可以继续的线程。
----? 线程的优先级
public class ThreadTest04 { public static void main(String[] args) { Runnable runnable = new Processor(); Thread thread_1 = new Thread(runnable,"r1"); thread_1.start(); Thread thread_2 = new Thread(runnable,"r2"); thread_2.start(); } } class Processor implements Runnable{ @Override public void run() { for(int i=0; i<100; i++){ System.out.println(Thread.currentThread().getName() + "," + i); if(i%10 == 0){ try {
//睡眠100毫秒,将CPU时间片交给其他线程使用 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
结果:当哪个线程运行时自己的i的值能被10整除的,那么这个线程就进入休眠状态,让另外一个线程运行。多次运行发现,也有可能依旧是当前的线程休眠好后仍然占用资源继续运行,估计是睡眠时间比较短的问题
3、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态
为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
public class ThreadTest05 { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); try { mTh1.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { mTh2.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); } } class Thread1 extends Thread{ private String name; public Thread1(String name) { super(name); this.name=name; } public void run() { System.out.println(Thread.currentThread().getName() + " 线程运行开始!"); for (int i = 0; i < 5; i++) { System.out.println("子线程"+name + "运行 : " + i); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " 线程运行结束!"); } }
结果:每次运行,主线程都要等子线程运行结束后自己才结束运行
main主线程运行开始! B 线程运行开始! A 线程运行开始! 子线程B运行 : 0 子线程A运行 : 0 子线程B运行 : 1 子线程A运行 : 1 子线程B运行 : 2 子线程A运行 : 2 子线程B运行 : 3 子线程A运行 : 3 子线程B运行 : 4 子线程A运行 : 4 B 线程运行结束! A 线程运行结束! main主线程运行结束!
4、线程让步:Thread.yield() 方法
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中
结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
public class ThreadTest06 { public static void main(String[] args) { MyThread_2 yt1 = new MyThread_2("张三"); MyThread_2 yt2 = new MyThread_2("李四"); yt1.start(); yt2.start(); } } class MyThread_2 extends Thread{ MyThread_2(String s){ super(s); } @Override public void run() { for(int i=0; i<=50; i++){ System.out.println(Thread.currentThread().getName()+":"+i); if(i==30){ // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行) this.yield(); } } } }
5、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
~~~各种区别
sleep()、wait()?
- . wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException - sleep()是Thread类的方法,wait()是Object类的方法
- sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度)
- sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用
-
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态
举个栗子
package com.xxg.thread; public class ThreadTest07 { public static void main(String[] args) { new Thread(new Thread01()).start(); try { Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } new Thread(new Thread02()).start(); } } class Thread01 implements Runnable { @Override public void run() { synchronized (ThreadTest07.class) { System.out.println("enter thread1..."); System.out.println("thread1 is waiting..."); try { // 调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池 ThreadTest07.class.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("thread1 is going on ...."); System.out.println("thread1 is over!!!"); } } } class Thread02 implements Runnable { @Override public void run() { synchronized (ThreadTest07.class) { System.out.println("enter thread2..."); System.out.println("thread2 is sleeping..."); // 只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。 ThreadTest07.class.notify(); // 区别 // 如果我们把代码:TestD.class.notify();给注释掉,即TestD.class调用了wait()方法,但是没有调用notify() // 方法,则线程永远处于挂起状态。 try { // sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程, // 但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 // 在调用sleep()方法的过程中,线程不会释放对象锁。 Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("thread2 is going on ...."); System.out.println("thread2 is over!!!"); } } }
结果:
注释掉 ThreadTest07.class.notify();,运行结果如下,且程序出于挂起状态
sleep()、yield()?
- sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行
- sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
- 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 IO 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
- sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
----? 线程常见名词
线程类的一些常用方法:
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
----? 线程同步
多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了A线程处理的数理。显然这是由于全局资源造成的,有时为了解决此问题,优先考虑使用局部变量,退而求其次使用同步代码块,出于这样的安全考虑就必须牺牲系统处理性能,加在多线程并发时资源挣夺最激烈的地方,这就实现了线程的同步机制
要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。
为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的。这归因于java语言规范的内存模型,它规定了:一个线程所做的变化何时以及如何变成对其它线程可见。
同步和多线程关系:没多线程环境就不需要同步;有多线程环境也不一定需要同步。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。
互斥 即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。
可见性 要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题
小结:为了防止多个线程并发对同一数据的修改,所以需要同步,否则会造成数据不一致(就是所谓的:线程安全。如java集合框架中Hashtable和Vector是线程安全的。我们的大部分程序都不是线程安全的,因为没有进行同步,而且我们没有必要,因为大部分情况根本没有多线程环境)。
--->同步机制实现方法
1)synchronized
synchronized的作用域有两个,在对象实例内,如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
在某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。
Public synchronized void methodAAA() { //do something. } ---------------------------------------------------- public void methodAAA() { synchronized (this) //this指的是调用该方法的对象 { //do something } }
关于synchronize()的静态函数和非静态函数
synchronize修饰非静态方法,实际就是对调用该方法的对象加锁,俗称“对象锁”
synchronize修饰静态方法,实际就是对该类对象加锁,俗称“类锁”
所以,当一个对象在两个线程中分别调用一个静态同步方法和非静态同步方法,因为虽然这是同一个对象调用的,但是这两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,故不会产生排斥
2)wait()、notify()
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait() 是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
(如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)
3)volatile
首先需要先大致了解一下Java的主内存和工作内存的工作机制,Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成
volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且当成员变量发生变化时,强迫线程将变化值回写到共享内存。在两个或者更多的线程访问的成员变量上使用 volatile 。当要访问的变量已在synchronized 代码块中,或者为常量时,不必使用。
举个栗子
public class ThreadTest08 { private static boolean flag = false; public static void main(String[] args) throws InterruptedException { Runnable runnable = new Runnable() { @Override public void run() { while(!flag){ System.out.println("running"); } } }; new Thread(runnable).start(); //等待两秒发出暂停信号 TimeUnit.SECONDS.sleep(2); flag = true; System.out.println("已发出暂停信号"); } }
分析:这段代码的意思是通过主线程发出暂停信号使得子线程停止打印,暂停的信号由一个boolean类型的变量stop表示,这个看似毫无问题的代码在某些特殊的情况下是无法正常运行的,比如当主线程更改了工作内存中的变量stop的值为true但是,并未立刻同步到主内存,或者是同步到主内存后子线程并未去主线程中获取最新的变量值,这都会导致主线程发出暂停信号而子线程继续工作的情况。
private volatile boolean flag = false;
使用volatile来修饰我们的stop变量,volatile修饰的变量能够保证在多个线程中的可见性,也就是每个线程都能看到最新的值,当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此子线程再需要读取从主内存中去读取该变量的最新值。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性(如同步中有i++之类的)。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束
4)ThreadLocal
ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程的副本冲突。
ThreadLocal一般都是声明在静态变量中,如果不断地创建ThreadLocal而没有调用其remove方法,将导致内存泄露,特别是在高并发的Web容器当中。
ThreadLocal在处理线程的局部变量时比synchronized同步机制解决线程安全问题更简单,更方便,且程序拥有更高的并发性。
package com.test; public class ThreadMain { // 通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { public Integer initialValue() { return 0; } }; // 获取下一个序列值 public int getNextNum() { seqNum.set(seqNum.get() + 1); return seqNum.get(); } public static void main(String[] args) { ThreadMain sn = new ThreadMain(); // 3个线程共享sn,各自产生序列号 TestClient t1 = new TestClient(sn); TestClient t2 = new TestClient(sn); TestClient t3 = new TestClient(sn); t1.start(); t2.start(); t3.start(); } private static class TestClient extends Thread { private ThreadMain sn; public TestClient(ThreadMain sn) { this.sn = sn; } public void run() { for (int i = 0; i < 3; i++) { // 每个线程打出3个序列值 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]"); } } } }
结果:
thread[Thread-0] --> sn[1] thread[Thread-2] --> sn[1] thread[Thread-1] --> sn[1] thread[Thread-2] --> sn[2] thread[Thread-0] --> sn[2] thread[Thread-2] --> sn[3] thread[Thread-1] --> sn[2] thread[Thread-1] --> sn[3] thread[Thread-0] --> sn[3]
----?死锁
java中产生死锁的原因及如何避免https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
https://www.cnblogs.com/hongten/p/hongten_java_sleep_wait.html
https://blog.csdn.net/u011033906/article/details/53840525