線程是進程中辛勤勞作者,所以用好多線程就顯得十分必要
創建線程
-
重寫run方法,創建Thread對象start起來
new Thread(){ @Override public void run() { super.run(); System.out.println("Thread run"); }; }.start();
-
用Thread的構造器Thread(Runnable target)創建出一個Thread對象,start起來。
new Thread(new Runnable() { @Override public void run() { System.out.println("Runnable run"); }; }).start();
那么,問題來了,如果有一哥們,既要重寫Thread類中的run方法,又要用構造器Thread(Runnable target)來創建Thread對象會發生什么事呢?
new Thread(new Runnable() { @Override public void run() { System.out.println("Runnable run"); } }){ @Override public void run() { super.run(); System.out.println("Thread run"); } }.start();
不用多說看,運行結果
Runnable run Thread run
兩個都會執行哦,為什么呢?答案就在super.run()
@Override public void run() { if (target != null) { target.run(); } }
target是什么?,target就是Thread(Runnable target)傳進來的Runnable,也就是說,如果重寫了Thread的run方法,在run方法中有調用super.run()
則會先執行Runnable中的run方法,再執行重寫run的super.run()
之下的方法
線程的狀態
接下來我們分別來看看線程在運行的時候不同狀態的切換
臨時阻塞
- 線程一旦啟動run方法就會加入CPU執行隊列中,進入等待CPU執行的狀態.這個時候如果CPU沒有執行該線程而去執行其他任務,則該線程進入了臨時阻塞狀態,如果CPU的執行權切換到該線程,則繼續執行run方法。
- 還可以在得到執行權的時候調用
Thread.yeild()
語句,釋放執行權。
凍結
- Thread#sleep()方法:這個方法會使線程凍結,如果執行到的代碼塊具有原子性,凍結的時候不會釋放鎖。sleep時間到,則線程回到運行狀態。
- Thread#suspend()方法和Thread#resume()方法:當線程調用
suspend
方法時,與sleep一樣,但需要用resume來喚醒線程,目前該方法已棄用。容易造成死鎖,下面會介紹。 - Object#waite()方法和Object#notify()方法:這兩個方法時
Object
的方法,我們知道,代碼塊如果具備原子性,需要用一個鎖對象,而這個鎖對象可以是任意對象。所以線程在執行具有原子性的代碼塊時,可以調用Object
類的wait()
方法,來釋放該線程所持有的鎖,并且使線程凍結。如果需要喚醒線程,則需要讓其他線程執行原子性代碼塊時,調用鎖對象的notify()
或者nofityAll()
方法:notify表示隨機喚醒凍結線程池里面的任一線程,notifyAll表示喚醒凍結線程池中的所有線程。利用鎖對象的這兩個特性,可以用來設計多線程的同步機制,下面會介紹。需要注意的是,如果Object的這幾個方法不是作為鎖對象的時候調用就會java.lang.IllegalMonitorStateException
。
消亡
-
run()
方法結束,自然而然線程也就等待被回收了。可以使用volatile
修飾的標記位。 - 使用
stop()
。確實能退出,但是不安全,官方已棄用。例如,當線程執行一段具有原子性的代碼,執行到一半的情況下退出,是可以釋放鎖,但是造成了原子性的代碼沒執行部分。stop的線程會拋error:java.lang.ThreadDeath
,可用try run方法中的代碼塊來catch Throwable. - 使用
interrupt()
方法,可分為兩種情況- 線程處于凍結狀態,例如使用了
Thread.sleep()
方法,如果sleep時間還沒到,這個時候調用Thread#interrupt()
則會拋InterruptedException
,然后繼續執行run方法直到退出。 - run方法中使用
while(true)
, 則這個標志位用isInterrupted()
方法來得到。這個時候調用Thread#interrupt()
,Thread#isInterrupted()
返回false,run方法結束
- 線程處于凍結狀態,例如使用了
死鎖
范例
public class DeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new MyThread1().start();
new MyThread2().start();
}
private static class MyThread1 extends Thread {
private String nameString = "MyThread1 ";
@Override
public void run() {
while (true)
synchronized (lock1) {//1
System.out.println(nameString + "get loack1");
synchronized (lock2) {//2
System.out.println(nameString + "get loack2");
System.out.println(nameString + "release loack2");
}
System.out.println(nameString + "release loack1");
}
}
}
private static class MyThread2 extends Thread {
private String nameString = "MyThread2 ";
@Override
public void run() {
while (true)
synchronized (lock2) {//3
System.out.println(nameString + "get loack2");
synchronized (lock1) {//4
System.out.println(nameString + "get loack1");
System.out.println(nameString + "release loack1");
}
System.out.println(nameString + "release loack2");
}
}
}
}
運行結果:
MyThread1 get loack1
MyThread1 get loack2
MyThread1 release loack2
MyThread1 release loack1
MyThread1 get loack1
MyThread2 get loack2
上面的打印怎么來的呢?首先MyThread1啟動,進入while(true)代碼塊,拿著lock1進入代碼塊1,接著又拿著lock2進入代碼塊2,代碼塊2執行完之后釋放lock2,代碼塊1也執行完了,釋放lock1,這時候兩個鎖lock1,lock2都沒有被任一線程持有,這個時候MyThread2啟動了,MyThread2也進入了while(true)代碼塊,到這一步,兩個線程同時死循環執行while(true)里面的代碼,那么,就存在,MyThread1持有lock1進入代碼塊1,MyThread2持有lock2進入代碼塊3,由于MyThread1要進入代碼塊2,需要持有lock2,但是lock2已經被MyThread2持有,所以MyThread1阻塞,等待lock2被釋放,而MyThread2要進入代碼塊4,需要持有lock1,但lock1被阻塞的MyThread1持有,由于沒有鎖lock1,所以MyThread2也阻塞了,兩個線程都因等待而阻塞,就算等待天荒地老,需要的鎖也不會到來,造成兩個線程都無法繼續運行下去,這就是死鎖。
死鎖的必要條件
- 互斥條件:一個資源每次只能被一個進程使用。
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系
ps:出自《操作系統》
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
線程間的同步
主要就是用線程運行狀態與線程凍結兩個狀態的切換來控制線程間的同步,我們來做一道面試題:請用線程A,B,C三個線程來依次打印上午,中午,下午,各打印10次。這道題的解法就是線程間的同步機制。
public class ThreadDemo {
/**
* 控制哪個線程執行
*/
private static int which = 0;
public static void main(String[] args) {
//分別創建三個線程
MyThread[] threads = new MyThread[] { new MyThread("A", "早上"),
new MyThread("B", "中午"), new MyThread("C", "晚上") };
for (int i = 0; i < threads.length; i++) {
//設置線程的編號
threads[i].setIndex(i);
//啟動線程
threads[i].start();
}
}
private static class MyThread extends Thread {
/**
* 線程名字
*/
private String tName;
/**
* 需要打印的內容
*/
private String mPString;
/**
* 線程編號
*/
private int idx;
public MyThread(String name, String string) {
this.tName = name;
this.mPString = string;
}
public void setIndex(int i) {
this.idx = i;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (MyThread.class) {
while (this.idx != which) {//如果到了我的編號,跳出while執行打印
try {
//如果不到我的編號,我就繼續睡覺
MyThread.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.tName + ":" + this.mPString + ":"
+ (i + 1));
which = ++which % 3;//打印完畢,編號++
MyThread.class.notifyAll();//喚醒所有沉睡的線程
}
}
}
}
}
打印結果符合預期,開啟三個線程,瞬間走到了for循環內,這個時候三個線程要搶奪MyThread.class
這個鎖,假如線程線程2搶到了,進到while中去發現,咦,which=0,我不是0,我把鎖扔了睡覺去了。這時候線程3有可能搶到鎖,但結果跟線程2一樣,只有線程1搶到鎖才會跳出while去執行打印,打印完畢,++which,然后喚醒所有沉睡的線程,這個時候,喚醒所有沉睡的線程,線程1同步代碼塊走完,又到了三個線程搶鎖的過程,只有序號對的線程才可以執行打印,所以就可以實現在指定情況下指定線程做指定任務,這就是線程的同步機制,從這里看以看出可以用鎖對象的notify,notifyAll,wait來進行線程間的通信。
還可以用JDK1.5提供的Lock來替代sychronized代碼塊,用Condition的await(凍結,釋放鎖),signal(喚醒)方法來替代Object#wait和Object#notify,效率更高,這邊就不介紹了。
如果僅僅針對這道題,還有更簡便的做法,修改MyThread#run里面內容如下:
for (int i = 0; i < 10; i++) {
while (this.idx != which) {
Thread.yield();//如果我不該執行就放棄執行權
}
System.out.println(this.tName + ":" + this.mPString + ":"
+ (i + 1));
which = ++which % 3;
}
輸出的內容一樣.