1. 程式人生 > >第4篇 Java多執行緒

第4篇 Java多執行緒

多執行緒:★★★★

程序:正在進行中的程式。其實程序就是一個應用程式執行時的記憶體分配空間。

執行緒:其實就是程序中一個程式執行控制單元,一條執行路徑。程序負責的是應用程式的空間的標示。執行緒負責的是應用程式的執行順序。

 

一個程序至少有一個執行緒在執行,當一個程序中出現多個執行緒時,就稱這個應用程式是多執行緒應用程式,每個執行緒在棧區中都有自己的執行空間,自己的方法區、自己的變數。

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();

     }

   }

 }