多线程之线程状态,状态切换种类及代码实例
线程的六种基本状态为:
1.NEW(刚新建)
2.Runable(可运行)
3.Blocked(被阻塞)
4.Waiting ( 等待 )
5.Timed waiting (计时等待)
6.Terminated (被终止,即执行完毕或线程死亡)
以上为线程调度的基本知识需求,接下来进入线程的各个状态的流程细节。
线程执行实例:单线程,直接不中断执行,直至执行完毕
public class Demo1 { static class Thread1 extends Thread{ @Override public void run() { int i=0; while (i++<10){ System.out.println("now i = "+i); } } } public static void main(String[] args) { Thread1 t1 = new Thread1(); t1.start(); } }
输出为:
now i = 1 now i = 2 now i = 3 now i = 4 now i = 5 now i = 6 now i = 7 now i = 8 now i = 9 now i = 10 Process finished with exit code 0
这是一个基本的线程执行,可以说是最最最最简单的线程执行,它没有线程切换的过程。
那么线程的切换有哪几种呢?
首先直接上状态和状态切换总览图
图看不清,可以在新网页中打开,目前没有找到好办法能让这么大内容的图
二、线程切换的种类
从上图看,有三种:
第一种:由锁竞争带来的线程阻塞,如何重新启动被阻塞的线程
第二种:在线程进入等待状态后,如何唤醒该线程
第三种:在线程线程休眠时,如何重新唤醒该线程
其中,
第一种是在多条线程竞争锁产生的,是由于锁的机制而导致的线程问题,
第二种和第三种是对某条线程独立进行的控制,是程序员自己主动控制的。
总结如下
图看不清,可以在新网页中打开,目前没有找到好办法能让这么大内容的图
三、从运行如何切换到各种阻塞类型
3.1 锁等待状态如何产生,如何解决锁等待问题?
首先要知道锁竞争和控制方式,首先需要知道什么是锁.
什么是锁?锁用来标记一段代码或方法,让这段代码在某个时间段只能有单独一个线程访问的工具。简单来说,就是线程拿到了锁就能执行下去,没有拿到锁,就只能继续等待。
一般来说,基本的锁分为两种,对象锁和类锁。每个对象实例拥有一把对象锁,但多个对象实例对应的类只有一把类锁,无论有多少对象实例都共享同一把类锁,这两点是锁控制机制的基础,具体的类锁和对象锁,乃至于重入锁,条件锁,读写锁等更多的锁知识这里就不提及了,本文着重讲如何改变线程的运行状态。
下例中,因为无法获取对象锁,线程被阻塞的情况
package thread.stateChange; public class Demo2 { static class Thread1 extends Thread{ static final Object lock = 0; //由于以同一个线程类来创建线程实例,所以使用静态对象来提供对象锁 String str = ""; public Thread1(String str) { this.str = str; } @Override public void run() { int i=0; Print(str); } public void Print(String str){ synchronized (lock) { while (true){ System.out.println(str); try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public static void main(String[] args) { Thread1 t1 = new Thread1("线程1"); Thread1 t2 = new Thread1("线程2"); t1.start(); try { //同时启动线程并不能保证线程1先进入运行状态,所以暂停1s保证线程1比线程2先进入运行状态 Thread.sleep(1000); t2.start(); while (true){ Thread.sleep(1000); System.out.println("t1 state:"+t1.getState()); System.out.println("t2 state:"+t2.getState()); } } catch (InterruptedException e) { e.printStackTrace(); } } }
打印结果:
线程1
线程1
t1 state:TIMED_WAITING
t2 state:BLOCKED
线程1
t1 state:TIMED_WAITING
t2 state:BLOCKED
线程1
t1 state:TIMED_WAITING
t2 state:BLOCKED
线程1
Process finished with exit code -1
显然线程2被阻塞了。
由于synchronized关键字会给Print方法加上对象锁,进入之后会进入死循环,一直打印,不会释放掉锁。
所以线程2会一直等待该锁的资源,状态一直显示为BLOCKED,而已经获取锁对象的线程1则一直可以执行,之所以显示的是TIMED_WAITING,是为了降低输出速度,在代码里设置了打印一次休眠一秒钟,在休眠是打印状态,自然对应的是TIMED_WAITING,而不是RUNABLE。
注:这里有一个知识点,sleep方法会让出CPU的时间片资源,但是不会释放掉获取的锁,wait方法则是让出CPU时间片资源并且释放掉对象的锁。
现在要解决的问题是,如何让线程2也能执行呢?
1.等待线程1执行完毕后释放锁,线程2即可继续执行
2.中断线程1的执行,释放锁,让位给线程2执行
方法1是一个很平常的顺序等待执行逻辑,着重点在于让执行中的线程中断,让位给其他线程的情况。
下面是方法2的示例代码:
package thread.stateChange; public class Demo2 { static class Thread1 extends Thread{ static final Object lock = 0; //由于以同一个线程类来创建线程实例,所以使用静态对象来提供对象锁 String str = ""; public Thread1(String str) { this.str = str; } @Override public void run() { int i=0; Print(str); } public void Print(String str){ synchronized (lock) { while (true){ System.out.println(str); try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { System.out.println(e.toString()); //打印中断异常,并停止线程 break; } } } } } public static void main(String[] args) { Thread1 t1 = new Thread1("线程1"); Thread1 t2 = new Thread1("线程2"); t1.start(); try { //同时启动线程并不能保证线程1先进入运行状态,所以暂停1s保证线程1比线程2先进入运行状态 Thread.sleep(1000); t2.start(); int i=0; while (true){ //打印两次之后再中断线程1 if (i++> 2){ t1.interrupt(); i = -100000000; } Thread.sleep(1000); System.out.println("t1 state:"+t1.getState()); System.out.println("t2 state:"+t2.getState()); } } catch (InterruptedException e) { e.printStackTrace(); } } }
输出结果为:
线程1
线程1
线程1
t1 state:TIMED_WAITING
t2 state:BLOCKED
线程1
t1 state:TIMED_WAITING
t2 state:BLOCKED
线程1
t1 state:TIMED_WAITING
t2 state:BLOCKED
java.lang.InterruptedException: sleep interrupted
线程2
线程2
t1 state:TERMINATED
t2 state:TIMED_WAITING
线程2
t1 state:TERMINATED
t2 state:TIMED_WAITING
Process finished with exit code -1
显然线程1被打算之后,线程2获得了锁于是进入执行状态。
这里有几个关键点:
1.调用线程对象的interupt方法,只是更改中断信号的布尔值,并不对线程的执行造成影响,检测方法却有
isInterrupted,interrupted ,其中interrupted会重置中断状态的标志布尔值,isInterrupted不会。
2.中断信号的处理,必须自己在定义线程执行内容的时候定义好,也就是说,只能某个线程自己中断自己的执行,外部只能提供中断信号。若没有提供中断信号的处理逻辑,则线程会继续执行下去。
3.interupt方法,在线程处于wait,sleep,jion状态时,如果执行会抛出中断异常。
总结:如果想要让一个执行中的线程中止,并释放锁,可以调用interupt方法给运行中的线程发送中断信号,线程本身在执行中需要处理中断异常,否则会造成程序终止。
3.2 线程如何从运行中进入等待状态,如何从等待状态唤醒该线程?
进入等待状态:在同步代码块或同步方法里,调用wait方法
唤醒等待线程:在同步代码块或方法里,调用notify方法
注:为什么要在同步区域里操作呢?
因为wait隐含了一个释放锁的操作,那么首先需要持有一把锁,否则没有锁能被释放。而唤醒需要锁竞争的线程,首先也需要持有锁才能让其余线程来竞争锁。
原本锁是用来处理多线程争抢某个资源的,但是这里是对一条线程进行状态切换,是不同的。真实的多线程会有许多的线程,同时进入这个方法,然后处于wait状态,都在等待获取锁,并处于wait状态,即使没有任何对象持有锁,线程也无法主动去获取锁,继续下去,只有当受到notify信号激活所有等待的线程的信号后,程序才会重新执行起来。
上一段代码示例:
package thread.stateChange; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Demo4 { static class Printer { public void Print(String str) { synchronized (this) { System.out.println(str); try { System.out.println(str +" and enter wait state!" ); wait(); //进入无限等待状态 //等待被唤醒后,继续执行 int i = 0; while (i++ < 3) { Thread.currentThread().sleep(1000); System.out.println(str); } } catch (InterruptedException e) { System.out.println(e.toString()); //打印中断异常,并停止线程 } } } public void wakeup() { synchronized (this) { notify(); //唤醒该线程 } } } public static void main(String[] args) { Printer p = new Printer(); Runnable r1 = () -> p.Print("this is r1!"); Runnable r2 = () -> p.Print("this is r2!"); Runnable r3 = () -> p.Print("this is r3!"); ThreadPoolExecutor executor = new ThreadPoolExecutor(3,3,30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(30)); executor.execute(r1); executor.execute(r2); executor.execute(r3); try { Thread.sleep(2000); p.wakeup(); } catch (InterruptedException e) { e.printStackTrace(); } } }
输出结果为:
this is r2! this is r2! and enter wait state! this is r1! this is r1! and enter wait state! this is r3! this is r3! and enter wait state! this is r2! this is r2! this is r2! Process finished with exit code -1
显然,这里仅仅唤醒了一条线程。将wakeup换成notifyAll方法,再运行一下,输出结果为:
this is r1! this is r1! and enter wait state! this is r3! this is r3! and enter wait state! this is r2! this is r2! and enter wait state! this is r2! this is r2! this is r2! this is r3! this is r3! this is r3! this is r1! this is r1! this is r1! Process finished with exit code -1
这样,就将所有进入了等待状态的线程都唤醒了。
但是需要特别说明的是,唤醒过程中,是让三条需要竞争锁的线程来重新争夺锁,所以谁先抢到,就是谁先执行,运行起来会有顺序的不同是正常的。
如果在wait方法后,不结束方法调用,即永远不释放锁,那么其余线程都得不到运行机会。
但是在wait方法的源码里,有这样的说明:
/** * Causes the current thread to wait until another thread invokes the * {@link java.lang.Object#notify()} method or the * {@link java.lang.Object#notifyAll()} method for this object. * In other words, this method behaves exactly as if it simply * performs the call {@code wait(0)}. * <p> * The current thread must own this object's monitor. The thread * releases ownership of this monitor and waits until another thread * notifies threads waiting on this object's monitor to wake up * either through a call to the {@code notify} method or the * {@code notifyAll} method. The thread then waits until it can * re-obtain ownership of the monitor and resumes execution. * <p> * As in the one argument version, interrupts and spurious wakeups are * possible, and this method should always be used in a loop: * <pre> * synchronized (obj) { * while (<condition does not hold>) * obj.wait(); * ... // Perform action appropriate to condition * } * </pre> * This method should only be called by a thread that is the owner * of this object's monitor. See the {@code notify} method for a * description of the ways in which a thread can become the owner of * a monitor. * * @throws IllegalMonitorStateException if the current thread is not * the owner of the object's monitor. * @throws InterruptedException if any thread interrupted the * current thread before or while the current thread * was waiting for a notification. The <i>interrupted * status</i> of the current thread is cleared when * this exception is thrown. * @see java.lang.Object#notify() * @see java.lang.Object#notifyAll() */ public final void wait() throws InterruptedException { wait(0); }
上面的说明,简单说了两点:
1.拿到对象锁的线程才能调用该方法
2.存在中断和虚假唤醒,所以应当放在循环中使用
对于第一点,获取对象锁的方法在源码里存在说明:
* This method should only be called by a thread that is the owner * of this object's monitor. A thread becomes the owner of the * object's monitor in one of three ways: * <ul> * <li>By executing a synchronized instance method of that object. * <li>By executing the body of a {@code synchronized} statement * that synchronizes on the object. * <li>For objects of type {@code Class,} by executing a * synchronized static method of that class. * </ul>
翻译过来就是:
1。执行对象的同步方法
2。执行对象发同步代码块
3。执行静态同步方法
这些个都好懂,基本是复习一下。
但是虚假唤醒是什么东西?
上个代码说明问题:
package thread.stateChange; public class Demo6 { static class Thread1 { volatile int num = 3; public synchronized int add() { try { if (num >= 5) { wait(); } num++; System.out.println("add : " + num); notifyAll(); return num; } catch (InterruptedException e) { System.out.println(e.toString()); } return num; } public synchronized int minus() { try { if (num <= 0) { wait(); } num--; System.out.println("minus : " + num); } catch (InterruptedException e) { System.out.println(e.toString()); } return num; } } public static void main(String[] args) { Thread1 t1 = new Thread1(); Thread t2 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.add(); } }); Thread t5 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.add(); } }); Thread t3 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.minus(); } }); Thread t4 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.minus(); } }); t2.start(); t3.start(); t4.start(); t5.start(); } }
输出的结果为:
minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 minus : -1 add : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 Process finished with exit code 0
显然上面的结果,输出了负数,也就是说,我们设置的数字范围0到5失效。
要出现这样的结果应该是这样一个过程:
1.一个减法线程进入发现值为0,进入等待状态,
2.然后一个加法进入,符合条件执行加一操作,但在这个加法线程操作的同一时刻,另一个减法线程也进入发现值为0,也进入等待状态
3.等加法线程执行加一操作完毕之后,通知等待的减法线程可以操作了,不巧的是有两条减法线程在等待,而加法又只是从0加一使得num变成了1,这样一来,两条减法线程都得到通知可以执行减法操作了,于是都执行-1操作,导致num的值出现-1这样超出期望的值。
所以,虚假唤醒,就是唤醒了原本并不应唤醒的线程,造成了超出预期的结果,而原本我们编写代码的时候并不想唤醒某个线程或者并不想这样唤醒某个线程。
如何解决呢?
源码里面给出了建议就是在循环中使用判断,改进后应该如下所示:
package thread.stateChange; public class Demo6 { static class Thread1 { volatile int num = 3; public synchronized int add() { try { if (num >= 5) { wait(); } num++; System.out.println("add : " + num); notifyAll(); return num; } catch (InterruptedException e) { System.out.println(e.toString()); } return num; } public synchronized int minus() { try { while (num <= 0) { wait(); } num--; System.out.println("minus : " + num); } catch (InterruptedException e) { System.out.println(e.toString()); } return num; } } public static void main(String[] args) { Thread1 t1 = new Thread1(); Thread t2 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.add(); } }); Thread t5 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.add(); } }); Thread t3 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.minus(); } }); Thread t4 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.minus(); } }); t2.start(); t3.start(); t4.start(); t5.start(); } }
相比于之前会出现问题的代码,仅仅改动了红色部分,使用循环判断等待条件,现在输出为:
minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 Process finished with exit code 0
一切正常了!
翻到上文,wait方法源码上面的示例,就是这样处理的,这也是为什么需要这样处理的原因。
这里有一个问题,nitofyAll显然是通知所有的线程,我们只是加一,能否只通过notify方法通知一个线程就好呢?
这样就能避免通知多线程而出现问题呢?
将NotifyAll改成notify之后,输出为:
minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 add : 6 add : 7 add : 8 add : 9 add : 10 add : 11 add : 12 add : 13 add : 14 add : 15 add : 16 add : 17 add : 18 add : 19 add : 20 add : 21 add : 22 add : 23 add : 24 add : 25 add : 26 add : 27 add : 28 add : 29 add : 30 add : 31 add : 32 add : 33 add : 34 add : 35 add : 36 add : 37 add : 38 add : 39 add : 40 add : 41 add : 42 add : 43 add : 44 add : 45 add : 46 add : 47 add : 48 add : 49 add : 50 add : 51 add : 52 add : 53 add : 54 add : 55 add : 56 add : 57 add : 58 add : 59 add : 60 add : 61 add : 62 add : 63 add : 64 add : 65 add : 66 add : 67 add : 68 add : 69 add : 70 add : 71 add : 72 add : 73 add : 74 add : 75 add : 76 add : 77 add : 78 add : 79 add : 80 add : 81 add : 82 add : 83 add : 84 add : 85 add : 86 minus : 85 minus : 84 minus : 83 minus : 82 minus : 81 minus : 80 minus : 79 minus : 78 minus : 77 minus : 76 minus : 75 minus : 74 minus : 73 minus : 72 minus : 71 minus : 70 minus : 69 minus : 68 minus : 67 minus : 66 minus : 65 minus : 64 minus : 63 minus : 62 minus : 61 minus : 60 minus : 59 minus : 58 minus : 57 minus : 56 minus : 55 minus : 54 minus : 53 minus : 52 minus : 51 minus : 50 minus : 49 minus : 48 minus : 47 minus : 46 minus : 45 minus : 44 minus : 43 minus : 42 minus : 41 minus : 40 minus : 39 minus : 38 minus : 37 minus : 36 minus : 35 minus : 34 minus : 33 minus : 32 minus : 31 minus : 30 minus : 29 minus : 28 minus : 27 minus : 26 minus : 25 minus : 24 minus : 23 minus : 22 minus : 21 minus : 20 minus : 19 minus : 18 minus : 17 minus : 16 minus : 15 minus : 14 minus : 13 minus : 12 minus : 11 minus : 10 minus : 9 minus : 8 minus : 7 minus : 6 minus : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0
这样的输出是一个很奇葩的结果,但是为什么呢?
因为多线程的CPU竞争资源机制和notify的效果。首先,多个线程对CPU的竞争结果并没有稳定的结果预期,notify的确能通知其余的线程开始抢占资源,但是在等待竞争的线程到底谁会获得锁并获取资源是完全没有预期的。
问题1:上面的结果,相当明显的是全都超过了5,为什么还能一直往上加呢?
因为即使一个加法线程执行时,另外一个加法线程和两个减法线程都在等待状态,但是notify通知的时候,恰恰是加法线程获得了锁,于是又再次开始加1。于此同时,另外一个刚做完加法的线程也进入等待状态,并在下一次锁竞争中获胜,于是又执行+1,于此同时,刚做完加法的线程又来开始争夺锁,并获胜····一直循环,就造成了明明早就越过边界条件,但是一直在执行+1的荒谬情况。
改进一下,在加法线程中,也用while循环来判断条件
public synchronized int add() { try { while (num >= 5) { wait(); } num++; System.out.println("add : " + num); notify(); return num; } catch (InterruptedException e) { System.out.println(e.toString()); } return num; }
输出结果成为:
minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0
的确是不超过5了,但是有一个更严重的问题:我们四条线程会一共操作200次,但这里的输出远远少于200次,程序也并没有输出终止符,表示程序依然活着,但却没有做任何事。
结合代码思考一下,为什么会出现程序卡住?
1.要么因为synchronized而出现死锁
2.要么所有的状态都进入wait状态了。
检测一下,加一段输出线程状态的代码
package thread.stateChange; public class Demo6 { static class Thread1 { volatile int num = 3; public synchronized int add() { try { while (num >= 5) { wait(); } num++; System.out.println("add : " + num); notify(); return num; } catch (InterruptedException e) { System.out.println(e.toString()); } return num; } public synchronized int minus() { try { while (num <= 0) { wait(); } num--; System.out.println("minus : " + num); } catch (InterruptedException e) { System.out.println(e.toString()); } return num; } } public static void main(String[] args) { Thread1 t1 = new Thread1(); Thread t2 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.add(); } }); Thread t5 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.add(); } }); Thread t3 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.minus(); } }); Thread t4 = new Thread(() -> { int i = 0; while (i++ < 50) { t1.minus(); } }); t2.start(); t3.start(); t4.start(); t5.start(); // 打印状态的线程 Thread t6 = new Thread(new Runnable() { @Override public void run() { while (true){ try { // 1秒打印一次,不然疯狂打印,根本看不到其余的输出 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("加法线程1"+t2.getState()); System.out.println("加法线程2"+t3.getState()); System.out.println("减法线程1"+t4.getState()); System.out.println("减法线程2"+t5.getState()); } } }); t6.start(); } }
输出结果为:
add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 加法线程1WAITING 加法线程2WAITING 减法线程1WAITING 减法线程2WAITING 加法线程1WAITING 加法线程2WAITING 减法线程1WAITING 减法线程2WAITING 加法线程1WAITING 加法线程2WAITING 减法线程1WAITING 减法线程2WAITING 加法线程1WAITING 加法线程2WAITING 减法线程1WAITING 减法线程2WAITING
显然,是因为所有的线程都进入了等待状态,而不是因为死锁,因为死锁的话线程的状态是blocked。
这样的群体沉睡是如何发生的呢?
查看输出,我们会发现,程序停在了num为0的状态,并且前面全在执行减法操作,那么就是在执行减法操作之前,加法线程全在都在沉睡状态,但是当num被减到0的时刻,又是减法拿到了锁,加法仍旧在沉睡,但是num为0,减法又进入沉睡了,至此,所有线程都进入沉睡,群体GG。
上面导致全体进入沉睡的代码,只有加法有notify操作,减法没有通知操作,那么给减法加上呢?
public synchronized int minus() { try { while (num <= 0) { wait(); } num--; System.out.println("minus : " + num); notify(); } catch (InterruptedException e) { System.out.println(e.toString()); } return num;
}
输出变为:
minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 add : 4 add : 5 minus : 4 minus : 3 minus : 2 minus : 1 minus : 0 add : 1 add : 2 add : 3 Process finished with exit code 0
程序输出符合预期,并且打印终止字符串。
之所以是这样的情况,是因为notify和线程的执行次数都是200,所以能够保证一个notify推动一条线程执行一次,直接的结果就是所有的线程的都会得到通知,等于加notifyAll方法的效果。
notifyAll不仅仅通知减法线程,连加法线程也会得到通知。
虚假唤醒的总结就是:
要使用while循环来反复验证条件变量是否符合要求,避免因为锁争夺的不确定结果影响了代码的预期效果,其发生的本质原因就是唤醒操作并没有确定的唤醒线程对象,锁争夺的胜利者是所有锁等待线程中随机产生的。
3.3 如何让线程进入睡眠状态,又如何将线程从睡眠状态中唤醒呢?
如何进入睡眠状态:显然是sleep方法,时间参数决定睡眠多久。
从睡眠状态唤醒:一般等待休眠时间结束,要想提前唤醒线程,可以使用interupt方法,但会抛出中断异常需处理。
上代码:
package thread.stateChange; public class Demo5 { static class Thread1 extends Thread { static final Object lock = new Object(); //由于以同一个线程类来创建线程实例,所以使用静态对象来提供对象锁 String str = ""; public Thread1(String str) { this.str = str; } @Override public void run() { int i = 0; Print(str); } public void Print(String str) { System.out.println(str); try { sleep(10000000); //沉睡超长时间,模拟永远沉睡 System.out.println("沉睡被打断之后,并不会执行sleep之后的代码!"); } catch (InterruptedException e) { System.out.println(e.toString()); //打印中断异常,并停止线程 } System.out.println("但是会执行异常捕捉模块和捕捉代码块之后的代码!"); } } public static void main(String[] args) { Thread1 t1 = new Thread1("线程1"); t1.start(); try { Thread.sleep(1000); int i = 0; while (true) { System.out.println("t1 state:" + t1.getState()); //打印两次之后再唤醒线程1 if (i++ > 2) { t1.interrupt(); i = -100000000; } Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } }
输出结果为:
线程1 t1 state:TIMED_WAITING t1 state:TIMED_WAITING t1 state:TIMED_WAITING t1 state:TIMED_WAITING java.lang.InterruptedException: sleep interrupted 但是会执行异常捕捉模块和捕捉代码块之后的代码! t1 state:TERMINATED t1 state:TERMINATED t1 state:TERMINATED t1 state:TERMINATED t1 state:TERMINATED t1 state:TERMINATED Process finished with exit code -1
显然,sleep之后的代码,在沉睡时间到之前被打断,就不会执行。并且打断沉睡会抛出中断异常,需要处理这个异常。
以上就是一条线程的状态切换操作,纯是个人基于尝试和瞧源码,搜博客整理而来,如有任何错误或不同意见,欢迎留言交流,共同进步。