一. 概述
使用的線程的目的有如下幾點:
- 異步。所謂異步,字義上來講就是同時做多個不同的事。
例如,你正在和戀人聊QQ,而此時你正在發送一個文件,如果收發消息和上傳文件在同一個線程,那么當你發送文件開始,你便需要等待文件上傳完成后,才能發送下一條消息,如果再加上文件太大、網速很差等等因素,就可能導致你和你戀人在幾個小時內只說了一句話,那你們之間可能就GG了。
- 并發。所謂并發,字義上來講就是同時做多個相同的事。
例如,雙11上淘寶買東西,在同一時間內,阿里的服務器收到一多個購買的請求,如果處理這些請求的方式是處理完一條再處理下一條的話,那么你買個衣服坑你就要等到地老天荒了。
雖然我在這里將線程的目的分成了兩種,但它們的本質是一樣:在同一時間做多個不同的事。值得注意的是,如果你電腦的CPU是單核單線程的話,這個“同一時間”是有事件差的,可能CPU這一毫秒在執行線程A,下一毫秒在執行線程B,但完全同一時間同時執行線程A和線程B是不可能的。
二. 如何創建線程
實現線程有兩種方式,涉及到的類有兩個,分別是:java.lang.Thread和java.lang.Runnable。創建并啟動線程的方式如下:
Thread thread = new Thread();//創建線程實例
thread.start();//啟動線程
但是這僅僅是創建了一條新的線程并啟動了它,該線程并不會執行任何邏輯,那么我們怎么讓它執行相關邏輯呢?
方式1:繼承Thread類,重寫run方法。
public class DemoThread extends Thread {
public void run() {
//TODO 線程中需要執行的相關邏輯
}
public static void main(String[] args) {
Thread thread = new DemoThread();//創建線程實例
thread.start();//啟動線程
}
}
方式二:實現Runnable接口,并在Thread構造方法中傳入實現的Runnable實例。
public class DemoRunnable implements Runnable {
public void run() {
//TODO 線程中需要執行的相關邏輯
}
public static void main(String[] args) {
Runnable runnable = new DemoRunnable();//創建一個實現了Runnable接口的類的實例
Thread thread = new Thread(runnable);//創建線程實例,并在構造方法中傳入實現了Runnable類的實例
thread.start();//啟動線程
}
}
注: 這兩種方法都能讓線程執行我們需要執行的邏輯代碼。當你調用start()函數啟動線程后,程序會在該線程中調用Thread類自己的run()方法,如果你在創建線程時傳入了Runnable的實例,那么在Thread類的run()方法中,會調用Runnable的run()方法。
特別需要注意的是,new Thread()只是創建了Thread類的一個實例,此時并沒有創建出一條線程。而啟動線程是調用start()方法而不是run()方法:直接調用run()方法時你只是調用的一個普通方法去執行相關邏輯代碼,邏輯依然執行再你調用run()方法的線程中;而調用start()方法,jvm才會創建一條新線程,而此時run()才會在該線程中自動被回調執行。
三、線程同步
什么是同步?
同步是某個任務在同一時間只能由一個線程在執行,等這個線程執行完成后,下一個線程才能執行。那么既然線程的目的是為了異步,那么又為什么需要同步呢?這主要是由于數據安全造成的。多個線程在同時操作一個數據,當線程A還沒沒來得急使用該數據時,線程B就改了該數據的狀態或值,這是就可能導致結果的錯誤,甚至是程序運行的異常。就像由此你和朋友都很餓,然后看到一個蘋果,你正準備吃,然后你朋友直接給你搶來吃的還剩核,然后......例子可能不精確,見諒。
實現線程同步主要要使用兩個點:鎖和synchronized關鍵字。
1.鎖。
鎖是java中一種機制,分為對象鎖和類鎖。每個對象/類都有一個單一的鎖(需要注意的是,一個類可以有多個對象,每個對象的鎖也都是獨立且單一的,互不干擾)。當一個線程獲取到某個對象/類的鎖后,除非該鎖被釋放,否則其他線程是不能獲取到該對象/類的鎖,而此時如果其他線程要獲取該對象的鎖,就只能等待。而上面所說的某個任務,便是獲取到同一個鎖的代碼塊,他可能里面的邏輯并不相同,但是獲取的鎖是相同的。
2、synchronized關鍵字用于需要同步的邏輯中,實現方式分為:同步方法和同步塊。synchronized的目的就是獲取某個對象/類的鎖。分為:
同步塊。其中的object參數便是你要獲取的鎖的對象。
synchronized (object) {
//TODO 這里是同步塊中需要執行的邏輯
}
對象同步方法。該synchronized獲取到的鎖是類A時候化后的對象的鎖。
public class A {
public synchronized void syncMethod() {
//TODO 這里是同步方法中需要執行的邏輯
}
}
類同步方法,即靜態同步方法。該synchronized獲取到的鎖是類A的鎖。
public class A {
public static synchronized void staicSyncMethod() {
}
}
同步的例子
public class ThreadSyncDemo {
private static Object locker = new Object();//需要獲取鎖的對象
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker) {
for (int i = 0; i < 1000; ++i) {
System.out.println(i);
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker) {
for (int i = 0; i < 1000; ++i) {
System.out.println("aaa");
}
}
}
});
t1.start();
t2.start();
}
}
執行這段代碼時,你先將synchronized塊去掉,保留里面的邏輯,然后執行,此時你會返現控制臺輸出中,數字和字母是交叉出現的,說明是異步執行的。然后加上同步快,再執行,你會發現此時數字先打印完后才打印的字母(或者字母先打印完后才打印的字母),說明是同步執行的。對象同步方法和靜態同步方法原理的列子這里就不再舉出,只要知道到底是獲取到那個對象或類的鎖,其他都是類似的。
死鎖
死鎖,顧名思義就是鎖死了,執行不下去了。若有兩個線程A和B,若線程A在執行時獲取到對象X的鎖,在同步塊中又獲取對象Y的鎖,而此時線程B在執行時獲取到對象Y的鎖,而B的同步塊中又在獲取X的鎖。再某種比較的極端情況下,A持有X的鎖,B持有Y的鎖,A執行到獲取Y的鎖時B未執行完,A阻塞等待,然后B又獲取X的鎖,而此時A還在阻塞等待B持有的Y的鎖,未釋放X的鎖,導致B也阻塞等待。A和B都在阻塞等待,然后就沒有然后了~~~~
所以避免死鎖的其中之一便是盡量不要交叉獲取鎖。當然這不是唯一導致死鎖的可能。
下面是一個死鎖的列子:
public class ThreadSyncDemo {
private static Object locker = new Object();
public static void main(String[] args) {
final Thread t1 = new Thread() {
@Override
@Deprecated
public void run() {
synchronized (locker) {//獲取ThreadSyncDemo的類鎖
for (int i = 0; i < 10000; ++i) {
System.out.println(i);
if (i == 1000) {
this.suspend();//掛起該線程,此方法暫停線程執行,但不會釋放鎖
}
}
}
}
};
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker) {//獲取ThreadSyncDemo的類鎖
for (int i = 0; i < 10000; ++i) {
System.out.println("aaa");
}
}
}
});
t1.start();
try{
Thread.sleep(50)//暫停下,保證他t1先執行。
}catch(Exception e){}
t2.start();
}
}
上面例子中,線程t1和線程t2的同步塊都是獲取的ThreadSyncDemo.class的鎖,在t1同步塊中,當i=1000時,暫停了t1線程的執行,此時t1的同步塊未釋放鎖,而t2一直在等待t1釋放鎖,如果t1線程不繼續執行,則t2也執行不了。
四. Thread類的相關方法
wait()與notify()/notifyAll()方法
實際上,wait()、notify()、notifyAll()這三個方法并不是Thread類的專有方法,而是Object的方法,也就是說,每個對象都存在這三個方法。
需要注意的是,這三個方法都只能在同步塊/同步方法中執行,其他地方執行時沒有意義的。而且需要同步塊中獲取到的鎖的對象來調用才有效果。
wait()顧名思義讓同步塊暫停執行并等待,此時該同步塊會讓出獲取到的鎖,讓其他線程執行獲取同一把鎖的同步塊。而在其他線程執行完后,調用notify()/notifyAll()方法,之前等待的的同步塊就會繼續執行。但是,如果調用了notify()/notifyAll()之后,后面有長時間任務二導致鎖未被釋放,等待中的同步塊也需要等鎖被釋放后才會繼續往下執行。如果同一個鎖有多個地方等待,就需要使用notifyAll()來全部喚醒,使他們重新爭奪鎖的行列中,誰先獲取到鎖就誰先執行。注意如果等待的是多個,nofity()和nofityAll()后最先獲取的鎖的是那個,由jvm決定。
wait()方法有兩個個重載方法,wait(long timeout)和wait(long timeout, int nanos),其中timeout是等待時間,如果timeout=0,則表示一直等待知道nofity()/nofityAll()被調用切獲取到鎖后繼續執行,timeout>0則表示等待多少時間后,只要獲取到鎖就繼續執行。至于nanos,表示納秒,值在0-999999之間,為了更好的控制時間。
public class ThreadSyncDemo {
private static Object locker_1 = new Object();
public static void main(String[] args) {
final Thread t1 = new Thread() {
@Override
@Deprecated
public void run() {
synchronized (locker_1) {
for (int i = 0; i < 1000; ++i) {
System.out.println(i);
if (i == 100) {
try {
locker_1.wait();//當i=100是,執行wait()方法,釋放鎖,進入等待狀態
} catch (Exception e) {
}
}
}
}
}
};
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker_1) {
for (int i = 0; i < 100; ++i) {
System.out.println("aaa");
}
locker_1.notify();//當執行完后,通知等待的線程繼續執行。
}
}
});
t1.start();
try{
Thread.sleep(50) //保證t1先執行
}catch(Exception e)
t2.start();
}
}
上例,控制臺先會打印0-100,當線程t1中i=100時,調用locker_1.wait()使其釋放鎖并進入等待狀態,此時線程t2獲取到鎖,控制臺打印100個aaa,然后調用locker_1.notify()后,線程t1會繼續在控制臺打印101-999。
interrupt(),interrupted(),isInterrupt()方法
interrupt()方法看起來是中斷線程,但實際上,當你調用interrupt()方法后,你發現線程該干嘛還是再干嘛,除非你的線程中存再調用sleep()、wait()等方法的時候,此時會拋出InterruptedException異常,以供手動處理線程停止。isInterrupt()返回線程是否是中斷狀態。interrupted()方法是類方法。從Thread類的源碼看,isInterrupt()和interrupted()都會調用以下方法:
private native boolean isInterrupted(boolean ClearInterrupted);
不同是isInterrupt()傳入的ClearInterrupted=false,
interrupted()方法傳入的ClearInterrupted=true,當ClearInterrupted=true時,線程的中斷狀態會被清除,也就是說此時isInterrupt()方法的返回值是false。
suspend()與resume()方法
已廢棄的方法,用于暫停和重新開始執行線程。和wait()不同的是,suspend()是通過線程的實例調用的,而不是鎖對象,調用也不需要在同步塊中調用,而且suspend()方法調用后并不會釋放鎖。列子:
public class ThreaSuspendResumeDemo {
public static void main(String[] args) {
final Timer timer = new Timer();
final Thread t = new Thread() {
@Deprecated
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
if (i == 1000) {
this.suspend();//暫停線程
}
}
timer.cancel();
}
};
t.start();
timer.schedule(new TimerTask() {
@Override
@Deprecated
public void run() {
t.resume();//3秒后將線程喚醒
}
}, 3000);
}
}
stop()方法
廢棄的方法。暴力終止線程,調用此方法后,線程中未執行的語句將不會再執行。但是isAlive()方法和isInterrupt方法的返回值依然是false,所以,暴力如此,想想都可怕,謹慎使用。
public class ThreadStopDemo {
public static void main(String[] args) {
final Thread t = new Thread() {
@Deprecated
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
if (i == 1000) {
this.stop();//i=1000時就停止了,控制臺只會打印0-1000。
}
}
System.out.println("Is this thread alive?" + this.isAlive());//線程已經終止,這條語句是不會執行的
}
};
t.start();
final Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
@Deprecated
public void run() {
System.out.println("Is this thread alive?" + t.isAlive());//false
System.out.println("Is this thread interrupt?" + t.interrupted());//false
}
}, 3000);
}
}
destroy()方法
額,為什么要講這個方法呢?因為我以為會和stop()方法一樣的喪心病狂,但是我錯了。從Thread.destroy()方法的源碼來看,結果讓人發呆流鼻涕。源碼如下:
/**
* Throws {@link NoSuchMethodError}.
*
* @deprecated This method was originally designed to destroy this
* thread without any cleanup. Any monitors it held would have
* remained locked. However, the method was never implemented.
* If if were to be implemented, it would be deadlock-prone in
* much the manner of {@link #suspend}. If the target thread held
* a lock protecting a critical system resource when it was
* destroyed, no thread could ever access this resource again.
* If another thread ever attempted to lock this resource, deadlock
* would result. Such deadlocks typically manifest themselves as
* "frozen" processes. For more information, see
* <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">
* Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
* @throws NoSuchMethodError always
*/
@Deprecated
public void destroy() {
throw new NoSuchMethodError();
}
是不是亮瞎了鈦合金狗眼!!!!!
五.守護線程
線程分為用戶(User)線程和守護(Daemon)線程,守護(Daemon)線程的實現就是線程在調用start()方法前,先調用setDaemon(true)方法。區別是,普通用戶(User)線程,只要線程還在執行,那么程序就永遠不會退出;而守護(Daemon)線程只要程序主線程執行完后,守護(Daemon)線程也就被終止。守護(Daemon)線程常作為輔佐的的作用。
以上便是此次線程學習的第一部分總結,如有意見或建議歡迎提出,相互探討才能共同成長與進步。后面講繼續研究線程池和線程調度。