線程是計算程序運行的最小載體,由於單個單核CPU的硬件水平發展到了一定的瓶頸期,因此就出現了多核多CPU的情況,直接就導致程序員多線程編程的復雜。由此可見線程對於高性能開發的重要性。
那么線程在計算機中有好幾種狀態,他們之間是怎么切換的?sleep和wait又有什么區別?notify和notifyAll怎么用?帶着這些問題,我們來看看Java的線程吧!
Thread的狀態
先來看看Thread類里面都有哪幾種狀態,在Thread.class中可以找到這個枚舉,它定義了線程的相關狀態:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
如下圖所示:
- NEW 新建狀態,線程創建且沒有執行start方法時的狀態
- RUNNABLE 可運行狀態,線程已經啟動,但是等待相應的資源(比如IO或者時間片切換)才能開始執行
- BLOCKED 阻塞狀態,當遇到synchronized或者lock且沒有取得相應的鎖,就會進入這個狀態
- WAITING 等待狀態,當調用
Object.wait
或者Thread.join()
且沒有設置時間,在或者LockSupport.park
時,都會進入等待狀態。 - TIMED_WAITING 計時等待,當調用
Thread.sleep()
或者Object.wait(xx)
或者Thread.join(xx)
或者LockSupport.parkNanos
或者LockSupport.partUntil
時,進入該狀態 - TERMINATED 終止狀態,線程中斷或者運行結束的狀態
先來sleep和wait的區別
由於wait方法是在Object上的,而sleep方法是在Thread上,當調用Thread.sleep時,並不能改變對象的狀態,因此也不會釋放鎖。
這讓我想起來我家的兩個主子,一只泰迪一只美短,雖然他們兩個是不同的物種,但是卻有着相同的愛好,就是愛吃牛肉。偶爾給它們兩個開葷,奈何只有一個食盆,每次只能一個主子吃肉。這就好比是兩個線程,在爭用同一個變量。如果使用thread.sleep,那么其中一個吃完一塊肉后,會霸占食盆,不給另一只吃(不會釋放鎖等資源);如果使用wait,那么吃肉時,會離開食盆,這樣就有機會讓另一只去吃了,即占用的資源會釋放。
詳細的看一下下面的代碼:
package cn.xingoo.test.basic.thread;
public class AnimalEat {
public static void main(String[] args) {
System.out.println("盆里有20塊肉");
Animal animal = new Animal();
try{
Thread tidy = new Thread(animal,"泰迪");
Thread cat = new Thread(animal,"美短");
tidy.start();
cat.start();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("盆里的肉吃完了!");
}
}
class Animal implements Runnable {
int count = 0;
@Override
public void run() {
while(count < 20){
synchronized (this){
try {
System.out.println(Thread.currentThread().getName()+"吃力第"+count+"塊肉");
count++;
//Thread.sleep(100);
this.wait(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
當使用this.wait(100)的時候,會輸出下面的信息:
盆里有20塊肉
泰迪吃力第0塊肉
美短吃力第1塊肉
盆里的肉吃完了!
泰迪吃力第2塊肉
美短吃力第3塊肉
泰迪吃力第4塊肉
美短吃力第5塊肉
泰迪吃力第6塊肉
美短吃力第7塊肉
泰迪吃力第8塊肉
美短吃力第9塊肉
泰迪吃力第10塊肉
美短吃力第11塊肉
美短吃力第12塊肉
泰迪吃力第13塊肉
美短吃力第14塊肉
泰迪吃力第15塊肉
美短吃力第16塊肉
泰迪吃力第17塊肉
美短吃力第18塊肉
泰迪吃力第19塊肉
可以發現,輸出的信息並不是完美的交替,這是因為調用wait之后,並不一定馬上時另一個線程執行,而是要根據CPU的時間分片輪轉等其他的條件來定,輪到誰就看運氣了。
當使用Thread.sleep(100)的時候,可以得到下面的信息:
盆里有20塊肉
泰迪吃力第0塊肉
盆里的肉吃完了!
泰迪吃力第1塊肉
泰迪吃力第2塊肉
泰迪吃力第3塊肉
泰迪吃力第4塊肉
泰迪吃力第5塊肉
泰迪吃力第6塊肉
泰迪吃力第7塊肉
泰迪吃力第8塊肉
泰迪吃力第9塊肉
泰迪吃力第10塊肉
泰迪吃力第11塊肉
泰迪吃力第12塊肉
泰迪吃力第13塊肉
泰迪吃力第14塊肉
泰迪吃力第15塊肉
泰迪吃力第16塊肉
泰迪吃力第17塊肉
泰迪吃力第18塊肉
美短吃力第19塊肉
泰迪吃力第20塊肉
注意看最后面有一只美短。這是因為synchronized的代碼同步時在while循環里面,因此最后一次兩個主子都進入到了while里面,然后才開始等待相應的鎖。這就導致第19次輪到了另一個主子。
總結來說,sleep不會釋放線程的鎖,wait會釋放線程的資源。
再談談wait與notify和notifyall
wait、notify、notifyall這幾個一般都一起使用。不過需要注意下面幾個重要的點:
- 調用wait\notify\notifyall方法時,需要與鎖或者synchronized搭配使用,不然會報錯
java.lang.IllegalMonitorStateException
,因為任何時刻,對象的控制權只能一個線程持有,因此調用wait等方法的時候,必須確保對其的控制權。 - 如果對簡單的對象調用wait等方法,如果對他們進行賦值也會報錯,因為賦值相當於修改的原有的對象,因此如果有修改需求可以外面包裝一層。
- notify可以喚醒一個在該對象上等待的線程,notifyAll可以喚醒所有等待的線程。
- wait(xxx) 可以掛起線程,並釋放對象的資源,等計時結束后自動恢復;wait()則必須要其他線程調用notify或者notifyAll才能喚醒。
舉個通俗點的例子,我記得在高中的時候,每天上午快放學的時候大家都很緊張——因為那個時候小飯館正好播放一些港台劇,大家就總願意搶電視機旁邊的位置,所以每次快要中午放學的時候,大家都做好沖刺跑步的准備。
但是有的老師總願意壓堂,搞的大家怨聲載道。
比如,下面這位老師有的時候會用notifyall通知大家集體放學;有的時候會檢查背書,背好了,才能走。
package cn.xingoo.test.basic.thread;
public class School {
private DingLing dingLing = new DingLing(false);
class Teacher extends Thread{
Teacher(String name){
super(name);
}
@Override
public void run() {
synchronized (dingLing){
try {
dingLing.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
dingLing.flag = true;
System.out.println("放學啦");
dingLing.notifyAll();
/*for (int i = 0; i < 3; i++) {
System.out.println("放一個走吧");
dingLing.notify();
try {
dingLing.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
}
}
}
class Student extends Thread{
Student(String name){
super(name);
}
@Override
public void run(){
synchronized (dingLing){
while(!dingLing.flag){
System.out.println(Thread.currentThread().getName()+"開始等待");
try {
dingLing.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"去吃飯啦");
}
}
}
public static void main(String[] args) {
School school = new School();
Teacher teacher = school.new Teacher("老師");
Student zhangsan = school.new Student("張三");
Student lisi = school.new Student("李四");
Student wangwu = school.new Student("王五");
teacher.start();
zhangsan.start();
lisi.start();
wangwu.start();
}
}
class DingLing{
Boolean flag = false;
public DingLing(Boolean flag){
this.flag = flag;
}
}
當老師統一喊放學的時候,即調用dingLing.notifyAll();
,會得到下面的輸出:
張三開始等待
李四開始等待
王五開始等待
放學啦
王五去吃飯啦
李四去吃飯啦
張三去吃飯啦
如果檢查背書,那么每次老師只會調用一次notify,讓一個同學(線程)走(工作),就會得到下面的輸出:
張三開始等待
李四開始等待
王五開始等待
放一個走吧
張三去吃飯啦
放一個走吧
李四去吃飯啦
放一個走吧
王五去吃飯啦
注意的是,調用wait可以釋放dingling的占用,這樣才能讓別的線程進行檢查,如果改成Thread.sleep,有興趣的童鞋就可以自己去看看效果啦!
參考
- 最簡單的實例說明wait、notify、notifyAll的使用方法:http://longdick.iteye.com/blog/453615
- Java sleep和wait的區別:http://www.jb51.net/article/113587.htm
- sleep和wait解惑:https://www.zhihu.com/question/23328075