Java011-多執行緒

姚根深發表於2019-01-19

多執行緒:
程式:正在進行中的程式。其實程式就是一個應用程式執行時的記憶體分配空間。
執行緒:其實就是程式中一個程式執行控制單元,一條執行路徑。程式負責的是應用程式的空間的標示。執行緒負責的是應用程式的執行順序。

一個程式至少有一個執行緒在執行,當一個程式中出現多個執行緒時,就稱這個應用程式是多執行緒應用程式,每個執行緒在棧區中都有自己的執行空間,自己的方法區、自己的變數。
jvm在啟動的時,首先有一個主執行緒,負責程式的執行,呼叫的是main函式。主執行緒執行的程式碼都在main方法中。
當產生垃圾時,收垃圾的動作,是不需要主執行緒來完成,因為這樣,會出現主執行緒中的程式碼執行會停止,會去執行垃圾回收器程式碼,效率較低,所以由單獨一個執行緒來負責垃圾回收。

隨機性的原理:因為cpu的快速切換造成,哪個執行緒獲取到了cpu的執行權,哪個執行緒就執行。

返回當前執行緒的名稱:Thread.currentThread().getName()
執行緒的名稱是由:Thread-編號定義的。編號從0開始。
執行緒要執行的程式碼都統一存放在了run方法中。

執行緒要執行必須要通過類中指定的方法開啟。start方法。(啟動後,就多了一條執行路徑)
start方法:1)、啟動了執行緒;2)、讓jvm呼叫了run方法。

建立執行緒的第一種方式:繼承Thread ,由子類複寫run方法。
步驟:
1,定義類繼承Thread類;
2,目的是複寫run方法,將要讓執行緒執行的程式碼都儲存到run方法中;
3,通過建立Thread類的子類物件,建立執行緒物件;
4,呼叫執行緒的start方法,開啟執行緒,並執行run方法。

執行緒狀態:
被建立:start()
執行:具備執行資格,同時具備執行權;
凍結:sleep(time),wait()—notify()喚醒;執行緒釋放了執行權,同時釋放執行資格;
臨時阻塞狀態:執行緒具備cpu的執行資格,沒有cpu的執行權;
消亡:stop()

建立執行緒的第二種方式:實現一個介面Runnable。
步驟:
1,定義類實現Runnable介面。
2,覆蓋介面中的run方法(用於封裝執行緒要執行的程式碼)。
3,通過Thread類建立執行緒物件;
4,將實現了Runnable介面的子類物件作為實際引數傳遞給Thread類中的建構函式。
為什麼要傳遞呢?因為要讓執行緒物件明確要執行的run方法所屬的物件。
5,呼叫Thread物件的start方法。開啟執行緒,並執行Runnable介面子類中的run方法。
Ticket t = new Ticket();
/*
直接建立Ticket物件,並不是建立執行緒物件。
因為建立物件只能通過new Thread類,或者new Thread類的子類才可以。
所以最終想要建立執行緒。既然沒有了Thread類的子類,就只能用Thread類。
*/
Thread t1 = new Thread(t); //建立執行緒。
/*
只要將t作為Thread類的建構函式的實際引數傳入即可完成執行緒物件和t之間的關聯
為什麼要將t傳給Thread類的建構函式呢?其實就是為了明確執行緒要執行的程式碼run方法。
*/
t1.start();

為什麼要有Runnable介面的出現?
1:通過繼承Thread類的方式,可以完成多執行緒的建立。但是這種方式有一個侷限性,如果一個類已經有了自己的父類,就不可以繼承Thread類,因為java單繼承的侷限性。
可是該類中的還有部分程式碼需要被多個執行緒同時執行。這時怎麼辦呢?
只有對該類進行額外的功能擴充套件,java就提供了一個介面Runnable。這個介面中定義了run方法,其實run方法的定義就是為了儲存多執行緒要執行的程式碼。
所以,通常建立執行緒都用第二種方式。
因為實現Runnable介面可以避免單繼承的侷限性。

2:其實是將不同類中需要被多執行緒執行的程式碼進行抽取。將多執行緒要執行的程式碼的位置單獨定義到介面中。為其他類進行功能擴充套件提供了前提。
所以Thread類在描述執行緒時,內部定義的run方法,也來自於Runnable介面。

實現Runnable介面可以避免單繼承的侷限性。而且,繼承Thread,是可以對Thread類中的方法,進行子類複寫的。但是不需要做這個複寫動作的話,只為定義執行緒程式碼存放位置,實現Runnable介面更方便一些。所以Runnable介面將執行緒要執行的任務封裝成了物件。

//面試
new Thread(new Runnable(){ //匿名
public void run(){
System.out.println(“runnable run”);
}
})
{
public void run(){
System.out.println(“subthread run”);
}

}.start(); //結果:subthread run

Try {
Thread.sleep(10);
}catch(InterruptedException e){}// 當刻意讓執行緒稍微停一下,模擬cpu切換情況。

多執行緒安全問題的原因:
通過圖解:發現一個執行緒在執行多條語句時,並運算同一個資料時,在執行過程中,其他執行緒參與進來,並操作了這個資料。導致到了錯誤資料的產生。

涉及到兩個因素:
1,多個執行緒在操作共享資料。
2,有多條語句對共享資料進行運算。
原因:這多條語句,在某一個時刻被一個執行緒執行時,還沒有執行完,就被其他執行緒執行了。

解決安全問題的原理:
只要將操作共享資料的語句在某一時段讓一個執行緒執行完,在執行過程中,其他執行緒不能進來執行就可以解決這個問題。

如何進行多句操作共享資料程式碼的封裝呢?
java中提供了一個解決方式:就是同步程式碼塊。
格式:
synchronized(物件) { // 任意物件都可以。這個物件就是鎖。
需要被同步的程式碼;

}

同步:★★★★★
好處:解決了執行緒安全問題。
弊端:相對降低效能,因為判斷鎖需要消耗資源,產生了死鎖。

定義同步是有前提的:
1,必須要有兩個或者兩個以上的執行緒,才需要同步。
2,多個執行緒必須保證使用的是同一個鎖。

同步的第二種表現形式:
同步函式:其實就是將同步關鍵字定義在函式上,讓函式具備了同步性。

同步函式是用的哪個鎖呢?
通過驗證,函式都有自己所屬的物件this,所以同步函式所使用的鎖就是this鎖。

當同步函式被static修飾時,這時的同步用的是哪個鎖呢?
靜態函式在載入時所屬於類,這時有可能還沒有該類產生的物件,但是該類的位元組碼檔案載入進記憶體就已經被封裝成了物件,這個物件就是該類的位元組碼檔案物件。
所以靜態載入時,只有一個物件存在,那麼靜態同步函式就使用的這個物件。
這個物件就是 類名.class

同步程式碼塊和同步函式的區別?
同步程式碼塊使用的鎖可以是任意物件。
同步函式使用的鎖是this,靜態同步函式的鎖是該類的位元組碼檔案物件。

在一個類中只有一個同步,可以使用同步函式。如果有多同步,必須使用同步程式碼塊,來確定不同的鎖。所以同步程式碼塊相對靈活一些。

★考點問題:請寫一個延遲載入的單例模式?寫懶漢式;當出現多執行緒訪問時怎麼解決?加同步,解決安全問題;效率高嗎?不高;怎樣解決?通過雙重判斷的形式解決。
//懶漢式:延遲載入方式。
當多執行緒訪問懶漢式時,因為懶漢式的方法內對共性資料進行多條語句的操作。所以容易出現執行緒安全問題。為了解決,加入同步機制,解決安全問題。但是卻帶來了效率降低。
為了效率問題,通過雙重判斷的形式解決。
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){ //鎖是誰?位元組碼檔案物件;
if(s == null){
synchronized(Single.class){
if(s == null)
s = new Single();
}
}
return s;
}

}

同步死鎖:通常只要將同步進行巢狀,就可以看到現象。同步函式中有同步程式碼塊,同步程式碼塊中還有同步函式。

執行緒間通訊:思路:多個執行緒在操作同一個資源,但是操作的動作卻不一樣。
1:將資源封裝成物件。
2:將執行緒執行的任務(任務其實就是run方法。)也封裝成物件。

等待喚醒機制:涉及的方法:
wait:將同步中的執行緒處於凍結狀態。釋放了執行權,釋放了資格。同時將執行緒物件儲存到執行緒池中。
notify:喚醒執行緒池中某一個等待執行緒。
notifyAll:喚醒的是執行緒池中的所有執行緒。

注意:
1:這些方法都需要定義在同步中。
2:因為這些方法必須要標示所屬的鎖。
你要知道 A鎖上的執行緒被wait了,那這個執行緒就相當於處於A鎖的執行緒池中,只能A鎖的notify喚醒。
3:這三個方法都定義在Object類中。為什麼操作執行緒的方法定義在Object類中?
因為這三個方法都需要定義同步內,並標示所屬的同步鎖,既然被鎖呼叫,而鎖又可以是任意物件,那麼能被任意物件呼叫的方法一定定義在Object類中。

wait和sleep區別: 分析這兩個方法:從執行權和鎖上來分析:
wait:可以指定時間也可以不指定時間。不指定時間,只能由對應的notify或者notifyAll來喚醒。
sleep:必須指定時間,時間到自動從凍結狀態轉成執行狀態(臨時阻塞狀態)。
wait:執行緒會釋放執行權,而且執行緒會釋放鎖。
Sleep:執行緒會釋放執行權,但不是不釋放鎖。

執行緒的停止:通過stop方法就可以停止執行緒。但是這個方式過時了。
停止執行緒:原理就是:讓執行緒執行的程式碼結束,也就是結束run方法。
怎麼結束run方法?一般run方法裡肯定定義迴圈。所以只要結束迴圈即可。
第一種方式:定義迴圈的結束標記。
第二種方式:如果執行緒處於了凍結狀態,是不可能讀到標記的,這時就需要通過Thread類中的interrupt方法,將其凍結狀態強制清除。讓執行緒恢復具備執行資格的狀態,讓執行緒可以讀到標記,並結束。

———< java.lang.Thread >———-
interrupt():中斷執行緒。
setPriority(int newPriority):更改執行緒的優先順序。
getPriority():返回執行緒的優先順序。
toString():返回該執行緒的字串表示形式,包括執行緒名稱、優先順序和執行緒組。
Thread.yield():暫停當前正在執行的執行緒物件,並執行其他執行緒。
setDaemon(true):將該執行緒標記為守護執行緒或使用者執行緒。將該執行緒標記為守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出。該方法必須在啟動執行緒前呼叫。
join:臨時加入一個執行緒的時候可以使用join方法。

當A執行緒執行到了B執行緒的join方式。A執行緒處於凍結狀態,釋放了執行權,B開始執行。A什麼時候執行呢?只有當B執行緒執行結束後,A才從凍結狀態恢復執行狀態執行。

Lock介面:多執行緒在JDK1.5版本升級時,推出一個介面Lock介面。
解決執行緒安全問題使用同步的形式,(同步程式碼塊,要麼同步函式)其實最終使用的都是鎖機制。

到了後期版本,直接將鎖封裝成了物件。執行緒進入同步就是具備了鎖,執行完,離開同步,就是釋放了鎖。
在後期對鎖的分析過程中,發現,獲取鎖,或者釋放鎖的動作應該是鎖這個事物更清楚。所以將這些動作定義在了鎖當中,並把鎖定義成物件。

所以同步是隱示的鎖操作,而Lock物件是顯示的鎖操作,它的出現就替代了同步。

在之前的版本中使用Object類中wait、notify、notifyAll的方式來完成的。那是因為同步中的鎖是任意物件,所以操作鎖的等待喚醒的方法都定義在Object類中。

而現在鎖是指定物件Lock。所以查詢等待喚醒機制方式需要通過Lock介面來完成。而Lock介面中並沒有直接操作等待喚醒的方法,而是將這些方式又單獨封裝到了一個物件中。這個物件就是Condition,將Object中的三個方法進行單獨的封裝。並提供了功能一致的方法 await()、signal()、signalAll()體現新版本物件的好處。

< java.util.concurrent.locks > Condition介面:await()、signal()、signalAll();

class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {

 lock.lock();
 try {
   while (count == items.length)
     notFull.await();
   items[putptr] = x;
   if (++putptr == items.length) putptr = 0;
   ++count;
   notEmpty.signal();
 }

finally {

   lock.unlock();
 }

}
public Object take() throws InterruptedException {

 lock.lock();
 try {
   while (count == 0)
     notEmpty.await();
   Object x = items[takeptr];
   if (++takeptr == items.length) takeptr = 0;
   --count;
   notFull.signal();
   return x;
 }

finally {

   lock.unlock();
 }

}

}

相關文章