轉自
Java執行緒:概念與原理
一、進程與執行緒
進程是指一個記憶體中執行的應用程式,每個進程都有自己獨立的一塊記憶體空間,即進程空間或(虛空間)。進程不依賴於執行緒而獨立存在,一個進程中可以啓動多個執行緒。比如在Windows系統中,一個執行的exe就是一個進程。
執行緒是指進程中的一個執行流程,一個進程中可以執行多個執行緒。比如java.exe進程中可以執行很多執行緒。執行緒總是屬於某個進程,執行緒沒有自己的虛擬地址空間,與進程內的其他執行緒一起共用分配給該進程的所有資源。
「同時」執行是人的感覺,線上程之間實際上輪換執行。
進程在執行過程中擁有獨立的記憶體單元,進程有獨立的地址空間,而多個執行緒共用記憶體,從而極大地提高了程式的執行效率。
執行緒在執行過程中與進程還是有區別的。每個獨立的執行緒有一個程式執行的入口、順序執行序列和程式的出口。但是執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。
進程是具有一定獨立功能的程式關於某個數據集合上的一次執行活動,進程是系統進行資源分配和排程的一個獨立單位。
執行緒是進程的一個實體,是CPU排程和分派的基本單位,它是比進程更小的能獨立執行的基本單位。執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個進程的其他的執行緒共用進程所擁有的全部資源。
執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒包含以下內容:
- 一個指向當前被執行指令的指令指針;
- 一個棧;
- 一個暫存器值的集合,定義了一部分描述正在執行執行緒的處理器狀態的值
- 一個私有的數據區。
我們使用Join()方法掛起當前執行緒,直到呼叫Join()方法的執行緒執行完畢。該方法還存在包含參數的過載版本,其中的參數用於指定等待執行緒結束的最長時間(即超時)所花費的毫秒數。如果執行緒中的工作在規定的超時時段內結束,該版本的Join()方法將返回一個布爾量True。
簡而言之:
- 一個程式至少有一個進程,一個進程至少有一個執行緒。
- 執行緒的劃分尺度小於進程,使得多進程程式的併發性高。
- 另外,進程在執行過程中擁有獨立的記憶體單元,而多個執行緒共用記憶體,從而極大地提高了程式的執行效率。
- 執行緒在執行過程中與進程還是有區別的。每個獨立的執行緒有一個程式執行的入口、順序執行序列和程式的出口。但是執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。
- 從邏輯角度來看,多執行緒的意義在於一個應用程式中,有多個執行部分可以同時執行。但操作系統並沒有將多個執行緒看做多個獨立的應用,來實現進程的排程和管理以及資源分配。這就是進程和執行緒的重要區別。
在Java中,每次程式執行至少啓動2個執行緒:一個是main執行緒,一個是垃圾收集執行緒。因爲每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM實際上就是在操作系統中啓動了一個進程。
二、Java中的執行緒
在Java中,「執行緒」指兩件不同的事情:
1、java.lang.Thread類的一個範例;
2、執行緒的執行。
在 Java程式中,有兩種方法建立執行緒:
一是對 Thread 類進行派生並覆蓋 run方法;
二是通過實現Runnable介面建立。
使用java.lang.Thread類或者java.lang.Runnable介面編寫程式碼來定義、範例化和啓動新執行緒。
一個Thread類範例只是一個物件,像Java中的任何其他物件一樣,具有變數和方法,生死於堆上。
Java中,每個執行緒都有一個呼叫棧,即使不在程式中建立任何新的執行緒,執行緒也在後台執行着。
一個Java應用總是從main()方法開始執行,main()方法執行在一個執行緒內,他被稱爲主執行緒。
一旦建立一個新的執行緒,就產生一個新的呼叫棧。
執行緒總體分兩類:使用者執行緒和守候執行緒。
當所有使用者執行緒執行完畢的時候,JVM自動關閉。但是守候執行緒卻不獨立於JVM,守候執行緒一般是由操作系統或者使用者自己建立的。
Java執行緒:建立與啓動
一、定義執行緒
1、擴充套件java.lang.Thread類。
此類中有個run()方法,應該注意其用法:public void run()
如果該執行緒是使用獨立的Runnable執行物件構造的,則呼叫該Runnable物件的run方法;否則,該方法不執行任何操作並返回。
Thread的子類應該重寫該方法。
2、實現java.lang.Runnable介面。
void run()
使用實現介面Runnable的物件建立一個執行緒時,啓動該執行緒將導致在獨立執行的執行緒中呼叫物件的run方法。
方法run的常規協定是,它可能執行任何所需的操作。
二、範例化執行緒
1、如果是擴充套件java.lang.Thread類的執行緒,則直接new即可。
2、如果是實現了java.lang.Runnable介面的類,則用Thread的構造方法:
[java] view plain copy
- Thread(Runnabletarget)
- Thread(Runnabletarget, String name)
- Thread(ThreadGroupgroup, Runnable target)
- Thread(ThreadGroupgroup, Runnable target, String name)
- Thread(ThreadGroupgroup, Runnable target, String name, long stackSize)
其中:
Runnable target:實現了Runnable介面的類的範例。
- Thread類也實現了Runnable介面,因此,從Thread類繼承的類的範例也可以作爲target傳入這個構造方法。
- 直接實現Runnable介面類的範例。
- 執行緒池建立多執行緒。
String name:執行緒的名子。這個名子可以在建立Thread範例後通過Thread類的setName方法設定。預設執行緒名:Thread-N,N是執行緒建立的順序,是一個不重複的正整數。
ThreadGroup group:當前建立的執行緒所屬的執行緒組。如果不指定執行緒組,所有的執行緒都被加到一個預設的執行緒組中。
long stackSize:執行緒棧的大小,這個值一般是CPU頁面的整數倍。如x86的頁面大小是4KB.在x86平臺下,預設的執行緒棧大小是12KB。
三、啓動執行緒
線上程的Thread物件上呼叫start()方法,而不是run()或者別的方法。
在呼叫start()方法之前:執行緒處於新狀態中,新狀態指有一個Thread物件,但還沒有一個真正的執行緒。
在呼叫start()方法之後:發生了一系列複雜的事情——
啓動新的執行執行緒(具有新的呼叫棧);
該執行緒從新狀態轉移到可執行狀態;
當該執行緒獲得機會執行時,其目標run()方法將執行。
注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新執行緒知道呼叫的方法名稱(和簽名)。因此,在Runnable上或者Thread上呼叫run方法是合法的。但並不啓動新的執行緒。
四、例子
1、實現Runnable介面的多執行緒例子
[java] view plain copy
- /**
- * 實現Runnable介面的類
- */
- public class RunnableImpl implements Runnable{
- private Stringname;
- public RunnableImpl(String name) {
- this.name = name;
- }
- @Override
- public void run() {
- for (int i = 0; i < 5; i++) {
- for(long k=0;k<100000000;k++);
- System.out.println(name+":"+i);
- }
- }
- }
-
- /**
- * 測試Runnable類實現的多執行緒程式
- */
- public class TestRunnable {
-
- public static void main(String[] args) {
- RunnableImpl ri1=new RunnableImpl("李白");
- RunnableImpl ri2=new RunnableImpl("屈原");
- Thread t1=new Thread(ri1);
- Thread t2=new Thread(ri2);
- t1.start();
- t2.start();
- }
- }
執行結果:
[java] view plain copy
- 屈原:0
- 李白:0
- 屈原:1
- 李白:1
- 屈原:2
- 李白:2
- 李白:3
- 屈原:3
- 李白:4
- 屈原:4
2、擴充套件Thread類實現的多執行緒例子
[java] view plain copy
- /**
- * 測試擴充套件Thread類實現的多執行緒程式
- */
- public class TestThread extends Thread {
- public TestThread(String name){
- super(name);
- }
- @Override
- public void run() {
- for(int i=0;i<5;i++){
- for(long k=0;k<100000000;k++);
- System.out.println(this.getName()+":"+i);
- }
- }
- public static void main(String[] args){
- Thread t1=new TestThread("李白");
- Thread t2=new TestThread("屈原");
- t1.start();
- t2.start();
- }
- }
執行結果:
[java] view plain copy
- 屈原:0
- 李白:0
- 屈原:1
- 李白:1
- 屈原:2
- 李白:2
- 屈原:3
- 屈原:4
- 李白:3
- 李白:4
對於上面的多執行緒程式程式碼來說,輸出的結果是不確定的。其中的一條語句for(long k=0;k<100000000;k++);是用來模擬一個非常耗時的操作的。
五、一些常見問題
1、執行緒的名字,一個執行中的執行緒總是有名字的,名字有兩個來源,一個是虛擬機器自己給的名字,一個是你自己的定的名字。在沒有指定執行緒名字的情況下,虛擬機器總會爲執行緒指定名字,並且主執行緒的名字總是mian,非主執行緒的名字不確定。
2、執行緒都可以設定名字,也可以獲取執行緒的名字,連主執行緒也不例外。
3、獲取當前執行緒的物件的方法是:Thread.currentThread();
4、在上面的程式碼中,只能保證:每個執行緒都將啓動,每個執行緒都將執行直到完成。一系列執行緒以某種順序啓動並不意味着將按該順序執行。對於任何一組啓動的執行緒來說,排程程式不能保證其執行次序,持續時間也無法保證。
5、當執行緒目標run()方法結束時該執行緒完成。
6、一旦執行緒啓動,它就永遠不能再重新啓動。只有一個新的執行緒可以被啓動,並且只能一次。一個可執行的執行緒或死執行緒可以被重新啓動。
7、執行緒的排程是JVM的一部分,在一個CPU的機器上上,實際上一次只能執行一個執行緒。一次只有一個執行緒棧執行。JVM執行緒排程程式決定實際執行哪個處於可執行狀態的執行緒。
衆多可執行執行緒中的某一個會被選中做爲當前執行緒。可執行執行緒被選擇執行的順序是沒有保障的。
8、儘管通常採用佇列形式,但這是沒有保障的。佇列形式是指當一個執行緒完成「一輪」時,它移到可執行佇列的尾部等待,直到它最終排隊到該佇列的前端爲止,它才能 纔能被再次選中。事實上,我們把它稱爲可執行池而不是一個可執行佇列,目的是幫助認識執行緒並不都是以某種有保障的順序排列而成一個一個佇列的事實。
9、儘管我們沒有無法控制執行緒排程程式,但可以通過別的方式來影響執行緒排程的方式。
Java執行緒:執行緒棧模型與執行緒的變數
要理解執行緒排程的原理,以及執行緒執行過程,必須理解執行緒棧模型。
執行緒棧是指某時刻時記憶體中執行緒排程的棧資訊,當前呼叫的方法總是位於棧頂。執行緒棧的內容是隨着程式的執行動態變化的,因此研究執行緒棧必須選擇一個執行的時刻(實際上指程式碼執行到什麼地方)。
下面 下麪通過一個範例性的程式碼說明執行緒(呼叫)棧的變化過程。
這幅圖描述在程式碼執行到兩個不同時刻1、2時候,虛擬機器執行緒呼叫棧示意圖。
當程式執行到t.start();時候,程式多出一個分支(增加了一個呼叫棧B),這樣,棧A、棧B並行執行。
從這裏就可以看出方法呼叫和執行緒啓動的區別了。
Java執行緒:執行緒狀態的轉換
一、執行緒狀態
執行緒的狀態轉換是執行緒控制的基礎。執行緒狀態總的可以分爲五大狀態。用一個圖來描述如下:
1、新狀態:執行緒物件已經建立,還沒有在其上呼叫start()方法。
2、可執行狀態:當執行緒有資格執行,但排程程式還沒有把它選定爲執行執行緒時執行緒所處的狀態。當start()方法呼叫時,執行緒首先進入可執行狀態。線上程執行之後或者從阻塞、等待或睡眠狀態回來後,也返回到可執行狀態。
3、執行狀態:執行緒排程程式從可執行池中選擇一個執行緒作爲當前執行緒時執行緒所處的狀態。這也是執行緒進入執行狀態的唯一一種方式。
4、等待/阻塞/睡眠狀態:這是執行緒有資格執行時它所處的狀態。實際上這個三狀態組合爲一種,其共同點是:執行緒仍舊是活的,但是當前沒有條件執行。換句話說,它是可執行的,但是如果某件事件出現,他可能返回到可執行狀態。
5、死亡態:當執行緒的run()方法完成時就認爲它死去。這個執行緒物件也許是活的,但是,它已經不是一個單獨執行的執行緒。執行緒一旦死亡,就不能復生。如果在一個死去的執行緒上呼叫start()方法,會拋出java.lang.IllegalThreadStateException異常。
二、阻止執行緒執行
對於執行緒的阻止,考慮一下三個方面,不考慮IO阻塞的情況:
睡眠;
等待;
因爲需要一個物件的鎖定而被阻塞。
1、睡眠
Thread.sleep(longmillis)和Thread.sleep(long millis, int nanos)靜態方法強制當前正在執行的執行緒休眠(暫停執行),以「減慢執行緒」。當執行緒睡眠時,它入睡在某個地方,在甦醒之前不會返回到可執行狀態。當睡眠時間到期,則返回到可執行狀態。
執行緒睡眠的原因:執行緒執行太快,或者需要強制進入下一輪,因爲Java規範不保證合理的輪換。
睡眠的實現:呼叫靜態方法。
[java] view plain copy
- try {
- Thread.sleep(123);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
睡眠的位置:爲了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣才能 纔能保證該執行緒執行過程中會睡眠。
例如,在前面的例子中,將一個耗時的操作改爲睡眠,以減慢執行緒的執行。可以這麼寫:
[java] view plain copy
- for(int i=0;i<5;i++){
- // 很耗時的操作,用來減慢執行緒的執行
- //for(longk=0;k<100000000;k++);
- try {
- Thread.sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(this.getName()+":"+i);
- }
執行結果:
[java] view plain copy
- 李白:0
- 李白:1
- 屈原:0
- 李白:2
- 屈原:1
- 李白:3
- 屈原:2
- 李白:4
- 屈原:3
- 屈原:4
這樣,執行緒在每次執行過程中,總會睡眠3毫秒,睡眠了,其他的執行緒就有機會執行了。
注意:
1、執行緒睡眠是幫助所有執行緒獲得執行機會的最好方法。
2、執行緒睡眠到期自動甦醒,並返回到可執行狀態,不是執行狀態。sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就開始執行。
3、sleep()是靜態方法,只能控制當前正在執行的執行緒。
下面 下麪給個例子:
[java] view plain copy
- /**
- * 一個計數器,計數到100,在每個數位之間暫停1秒,每隔10個數字輸出一個字串
- */
- public class CalcThread extends Thread {
- public void run(){
- for(int i=0;i<100;i++){
- if ((i)%10==0) {
- System.out.println("--------"+i);
- }
- System.out.print(i);
- try {
- Thread.sleep(1);
- System.out.print(" 執行緒睡眠1毫秒!\n");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
-
- public static void main(String[] args) {
- new CalcThread().start();
- }
- }
執行結果:
[java] view plain copy
- --------0
- 0 執行緒睡眠1毫秒!
- 1 執行緒睡眠1毫秒!
- 2 執行緒睡眠1毫秒!
- 3 執行緒睡眠1毫秒!
- 4 執行緒睡眠1毫秒!
- 5 執行緒睡眠1毫秒!
- 6 執行緒睡眠1毫秒!
- 7 執行緒睡眠1毫秒!
- 8 執行緒睡眠1毫秒!
- 9 執行緒睡眠1毫秒!
- --------10
- 10 執行緒睡眠1毫秒!
- 11 執行緒睡眠1毫秒!
- 12 執行緒睡眠1毫秒!
- 13 執行緒睡眠1毫秒!
- 14 執行緒睡眠1毫秒!
- 15 執行緒睡眠1毫秒!
- 16 執行緒睡眠1毫秒!
- 17 執行緒睡眠1毫秒!
- 18 執行緒睡眠1毫秒!
- 19 執行緒睡眠1毫秒!
- --------20
- 20 執行緒睡眠1毫秒!
- 21 執行緒睡眠1毫秒!
- 22 執行緒睡眠1毫秒!
- 23 執行緒睡眠1毫秒!
- 24 執行緒睡眠1毫秒!
- 25 執行緒睡眠1毫秒!
- 26 執行緒睡眠1毫秒!
- 27 執行緒睡眠1毫秒!
- 28 執行緒睡眠1毫秒!
- 29 執行緒睡眠1毫秒!
- --------30
- 30 執行緒睡眠1毫秒!
- 31 執行緒睡眠1毫秒!
- 32 執行緒睡眠1毫秒!
- 33 執行緒睡眠1毫秒!
- 34 執行緒睡眠1毫秒!
- 35 執行緒睡眠1毫秒!
- 36 執行緒睡眠1毫秒!
- 37 執行緒睡眠1毫秒!
- 38 執行緒睡眠1毫秒!
- 39 執行緒睡眠1毫秒!
- --------40
- 40 執行緒睡眠1毫秒!
- 41 執行緒睡眠1毫秒!
- 42 執行緒睡眠1毫秒!
- 43 執行緒睡眠1毫秒!
- 44 執行緒睡眠1毫秒!
- 45 執行緒睡眠1毫秒!
- 46 執行緒睡眠1毫秒!
- 47 執行緒睡眠1毫秒!
- 48 執行緒睡眠1毫秒!
- 49 執行緒睡眠1毫秒!
- --------50
- 50 執行緒睡眠1毫秒!
- 51 執行緒睡眠1毫秒!
- 52 執行緒睡眠1毫秒!
- 53 執行緒睡眠1毫秒!
- 54 執行緒睡眠1毫秒!
- 55 執行緒睡眠1毫秒!
- 56 執行緒睡眠1毫秒!
- 57 執行緒睡眠1毫秒!
- 58 執行緒睡眠1毫秒!
- 59 執行緒睡眠1毫秒!
- --------60
- 60 執行緒睡眠1毫秒!
- 61 執行緒睡眠1毫秒!
- 62 執行緒睡眠1毫秒!
- 63 執行緒睡眠1毫秒!
- 64 執行緒睡眠1毫秒!
- 65 執行緒睡眠1毫秒!
- 66 執行緒睡眠1毫秒!
- 67 執行緒睡眠1毫秒!
- 68 執行緒睡眠1毫秒!
- 69 執行緒睡眠1毫秒!
- --------70
- 70 執行緒睡眠1毫秒!
- 71 執行緒睡眠1毫秒!
- 72 執行緒睡眠1毫秒!
- 73 執行緒睡眠1毫秒!
- 74 執行緒睡眠1毫秒!
- 75 執行緒睡眠1毫秒!
- 76 執行緒睡眠1毫秒!
- 77 執行緒睡眠1毫秒!
- 78 執行緒睡眠1毫秒!
- 79 執行緒睡眠1毫秒!
- --------80
- 80 執行緒睡眠1毫秒!
- 81 執行緒睡眠1毫秒!
- 82 執行緒睡眠1毫秒!
- 83 執行緒睡眠1毫秒!
- 84 執行緒睡眠1毫秒!
- 85 執行緒睡眠1毫秒!
- 86 執行緒睡眠1毫秒!
- 87 執行緒睡眠1毫秒!
- 88 執行緒睡眠1毫秒!
- 89 執行緒睡眠1毫秒!
- --------90
- 90 執行緒睡眠1毫秒!
- 91 執行緒睡眠1毫秒!
- 92 執行緒睡眠1毫秒!
- 93 執行緒睡眠1毫秒!
- 94 執行緒睡眠1毫秒!
- 95 執行緒睡眠1毫秒!
- 96 執行緒睡眠1毫秒!
- 97 執行緒睡眠1毫秒!
- 98 執行緒睡眠1毫秒!
- 99 執行緒睡眠1毫秒!
2、執行緒的優先順序和執行緒讓步yield()
執行緒的讓步是通過Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。
要理解yield(),必須瞭解執行緒的優先順序的概念。執行緒總是存在優先順序,優先順序範圍在1~10之間。JVM執行緒排程程式是基於優先順序的搶先排程機制 機製。在大多數情況下,當前執行的執行緒優先順序將大於或等於執行緒池中任何執行緒的優先順序。但這僅僅是大多數情況。
注意:當設計多執行緒應用程式的時候,一定不要依賴於執行緒的優先順序。因爲執行緒排程優先順序操作是沒有保障的,只能把執行緒優先順序作用作爲一種提高程式效率的方法,但是要保證程式不依賴這種操作。
當執行緒池中執行緒都具有相同的優先順序,排程程式的JVM實現自由選擇它喜歡的執行緒。這時候排程程式的操作有兩種可能:一是選擇一個執行緒執行,直到它阻塞或者執行完成爲止。二是時間分片,爲池內的每個執行緒提供均等的執行機會。
設定執行緒的優先順序:執行緒預設的優先順序是建立它的執行執行緒的優先順序。可以通過setPriority(int newPriority)更改執行緒的優先順序。例如:
[java] view plain copy
- Thread t = new MyThread();
- t.setPriority(8);
- t.start();
執行緒優先順序爲1~10之間的正整數,JVM從不會改變一個執行緒的優先順序。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先順序進行每兩個或多個合併,變成少於10個的優先順序,則兩個或多個優先順序的執行緒可能被對映爲一個優先順序。
執行緒預設優先順序是5,Thread類中有三個常數,定義執行緒優先順序範圍:
[java] view plain copy
- static intMAX_PRIORITY:執行緒可以具有的最高優先順序。
- static intMIN_PRIORITY:執行緒可以具有的最低優先順序。
- static intNORM_PRIORITY:分配給執行緒的預設優先順序。
3、Thread.yield()方法
Thread.yield()方法作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。
yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的執行緒還有可能被執行緒排程程式再次選中。
結論:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。
4、join()方法
Thread的非靜態方法join()讓一個執行緒B「加入」到另外一個執行緒A的尾部。在A執行完畢之前,B不能工作。例如:
[java] view plain copy
- Thread t = new MyThread();
- t.start();
- t.join();
另外,join()方法還有帶超時限制的過載版本。例如t.join(5000);則讓執行緒等待5000毫秒,如果超過這個時間,則停止等待,變爲可執行狀態。
執行緒的加入join()對執行緒棧導致的結果是執行緒棧發生了變化,當然這些變化都是瞬時的。下面 下麪給示意圖:
小結
到目前位置,介紹了執行緒離開執行狀態的3種方法:
1、呼叫Thread.sleep():使當前執行緒睡眠至少多少毫秒(儘管它可能在指定的時間之前被中斷)。
2、呼叫Thread.yield():不能保障太多事情,儘管通常它會讓當前執行執行緒回到可執行性狀態,使得有相同優先順序的執行緒有機會執行。
3、呼叫join()方法:保證當前執行緒停止執行,直到該執行緒所加入的執行緒完成爲止。然而,如果它加入的執行緒沒有存活,則當前執行緒不需要停止。
除了以上三種方式外,還有下面 下麪幾種特殊情況可能使執行緒離開執行狀態:
1、執行緒的run()方法完成。
2、在物件上呼叫wait()方法(不是線上程上呼叫)。
3、執行緒不能在物件上獲得鎖定,它正試圖執行該物件的方法程式碼。
4、執行緒排程程式可以決定將當前執行狀態移動到可執行狀態,以便讓另一個執行緒獲得執行機會,而不需要任何理由。
Java執行緒:執行緒的同步與鎖
一、同步問題提出
執行緒的同步是爲了防止多個執行緒存取一個數據物件時,對數據造成的破壞。
例如:兩個執行緒ThreadA、ThreadB都操作同一個物件Foo物件,並修改Foo物件上的數據。
[java] view plain copy
- public class Foo {
- private int x = 100;
- public int getX() {
- return x;
- }
- public int fix(int y) {
- x = x - y;
- return x;
- }
- }
-
- public class FooRunnable implements Runnable {
- private Foo foo =new Foo();
-
- public static void main(String[] args) {
- FooRunnable r = new FooRunnable();
- Thread ta = new Thread(r,"Thread-A");
- Thread tb = new Thread(r,"Thread-B");
- ta.start();
- tb.start();
- }
-
- @Override
- public void run() {
- for (int i = 0; i < 3; i++) {
- this.fix(30);
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()+ " :當前foo物件的x值= " + foo.getX());
- }
- }
-
- public int fix(int y) {
- return foo.fix(y);
- }
- }
執行結果:
[java] view plain copy
- Thread-B :當前foo物件的x值= 40
- Thread-A :當前foo物件的x值= 10
- Thread-B :當前foo物件的x值= -20
- Thread-A :當前foo物件的x值= -50
- Thread-B :當前foo物件的x值= -80
- Thread-A :當前foo物件的x值= -80
從結果發現,這樣的輸出值明顯是不合理的,原因是兩個執行緒不加控制的存取Foo物件並修改其數據所致。
如果要保持結果的合理性,只需要達到一個目的,就是將對Foo的存取加以限制,每次只能有一個執行緒在存取。這樣就能保證Foo物件中數據的合理性了。
在具體的Java程式碼中需要完成以下兩個操作:
把競爭存取的資源類Foo變數x標識爲private;
同步修改變數的程式碼,使用synchronized關鍵字同步方法或程式碼。
二、同步和鎖定
1、鎖的原理
Java中每個物件都有一個內建鎖。
當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前範例(this範例)有關的鎖。獲得一個物件的鎖也稱爲獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。
當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。
一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味着任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。
釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。
關於鎖和同步,有一下幾個要點:
1)只能同步方法,而不能同步變數和類;
2)每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步?
3)不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
4)如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的範例來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。
5)如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由存取而不受鎖的限制。
6)執行緒睡眠時,它所持的任何鎖都不會釋放。
7)執行緒可以獲得多個鎖。比如,在一個物件的同步方法裏面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。
8)同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。
9)在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:
[java] view plain copy
- public int fix(int y) {
- synchronized (this) {
- x = x - y;
- }
- return x;
- }
當然,同步方法也可以改寫爲非同步方法,但功能完全一樣的,例如:
[java] view plain copy
- public synchronized int getX() {
- return x++;
- }
與
[java] view plain copy
- public int getX() {
- synchronized (this) {
- return x;
- }
- }
效果是完全一樣的。
三、靜態方法同步
要同步靜態方法,需要一個用於整個類物件的鎖,這個物件是就是這個類(XXX.class)。
例如:
[java] view plain copy
- public staticsynchronized int setName(String name){
- Xxx.name = name;
- }
等價於
[java] view plain copy
- public static intsetName(String name){
- synchronized(Xxx.class){
- Xxx.name = name;
- }
- }
四、如果執行緒不能獲得鎖會怎麼樣
如果執行緒試圖進入同步方法,而其鎖已經被佔用,則執行緒在該物件上被阻塞。實質上,執行緒進入該物件的一種池中,必須在那裏等待,直到其鎖被釋放,該執行緒再次變爲可執行或執行爲止。
當考慮阻塞時,一定要注意哪個物件正被用於鎖定:
1、呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同對象,則每個執行緒有自己的物件的鎖,執行緒間彼此互不幹 不乾預。
2、呼叫同一個類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的Class物件上。
3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因爲靜態方法鎖定在Class物件上,非靜態方法鎖定在該類的物件上。
4、對於同步程式碼塊,要看清楚什麼物件已經用於鎖定(synchronized後面括號的內容)。在同一個物件上進行同步的執行緒將彼此阻塞,在不同對象上鎖定的執行緒將永遠不會彼此阻塞。
五、何時需要同步
在多個執行緒同時存取互斥(可交換)數據時,應該同步以保護數據,確保兩個執行緒不會同時修改更改它。
對於非靜態欄位中可更改的數據,通常使用非靜態方法存取。
對於靜態欄位中可更改的數據,通常使用靜態方法存取。
如果需要在非靜態方法中使用靜態欄位,或者在靜態欄位中呼叫非靜態方法,問題將變得非常複雜。
六、執行緒安全類
當一個類已經很好的同步以保護它的數據時,這個類就稱爲「執行緒安全的」。
即使是執行緒安全類,也應該特別小心,因爲操作的執行緒之間仍然不一定安全。
舉個形象的例子,比如一個集合是執行緒安全的,有兩個執行緒在操作同一個集合物件,當第一個執行緒查詢集合非空後,刪除集閤中所有元素的時候。第二個執行緒也來執行與第一個執行緒相同的操作,也許在第一個執行緒查詢後,第二個執行緒也查詢出集合非空,但是當第一個執行清除後,第二個再執行刪除顯然是不對的,因爲此時集合已經爲空了。
舉個例子:
[java] view plain copy
- public class NameList {
- private List nameList = Collections.synchronizedList(newLinkedList());
-
- public void add(String name) {
- nameList.add(name);
- }
-
- public String removeFirst() {
- if (nameList.size()>0) {
- return (String) nameList.remove(0);
- } else {
- return null;
- }
- }
- }
-
- public class TestNameList {
- public static void main(String[] args) {
- final NameList nl =new NameList();
- nl.add("蘇東坡");
- class NameDropper extends Thread{
- @Override
- public void run() {
- String name = nl.removeFirst();
- System.out.println(name);
- }
- }
- Thread t1=new NameDropper();
- Thread t2=new NameDropper();
- t1.start();
- t2.start();
- }
- }
執行結果:
[java] view plain copy
- 蘇東坡
- null
雖然集合物件
[java] view plain copy
- private List nameList =Collections.synchronizedList(new LinkedList());
是同步的,但是程式還不是執行緒安全的。
出現這種事件的原因是,上例中一個執行緒操作列表過程中無法阻止另外一個執行緒對列表的其他操作。
解決上面問題的辦法是,在操作集合物件的NameList上面做一個同步。改寫後的程式碼如下:
[java] view plain copy
- public class NameList {
- private List nameList = Collections.synchronizedList(newLinkedList());
-
- public synchronized void add(String name) {
- nameList.add(name);
- }
-
- public synchronized StringremoveFirst() {
- if (nameList.size()>0) {
- return (String) nameList.remove(0);
- } else {
- return null;
- }
- }
- }
這樣,當一個執行緒存取其中一個同步方法時,其他執行緒只有等待。
七、執行緒死鎖
死鎖對Java程式來說,是很複雜的,也很難發現問題。當兩個執行緒被阻塞,每個執行緒在等待另一個執行緒時就發生死鎖。
還是看一個比較直觀的死鎖例子:
[java] view plain copy
- public class Deadlock {
- private static class Resource{
- public int value;
- }
- private Resource resourceA=new Resource();
- private Resource resourceB=new Resource();
- public int read(){
- synchronized (resourceA) {
- synchronized (resourceB) {
- return resourceB.value+resourceA.value;
- }
- }
- }
- public void write(int a,int b){
- synchronized(resourceB){
- synchronized (resourceA) {
- resourceA.value=a;
- resourceB.value=b;
- }
- }
- }
- }
假設read()方法由一個執行緒啓動,write()方法由另外一個執行緒啓動。讀執行緒將擁有resourceA鎖,寫執行緒將擁有resourceB鎖,兩者都堅持等待的話就出現死鎖。
實際上,上面這個例子發生死鎖的概率很小。因爲在程式碼內的某個點,CPU必須從讀執行緒切換到寫執行緒,所以,死鎖基本上不能發生。
但是,無論程式碼中發生死鎖的概率有多小,一旦發生死鎖,程式就死掉。有一些設計方法能幫助避免死鎖,包括始終按照預定義的順序獲取鎖這一策略。已經超出SCJP的考試範圍。
八、執行緒同步小結
1、執行緒同步的目的是爲了保護多個執行緒反問一個資源時對資源的破壞。
2、執行緒同步方法是通過鎖來實現,每個物件都有切僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他存取該物件的執行緒就無法再存取該物件的其他同步方法。
3、對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件。靜態和非靜態方法的鎖互不幹 不乾預。一個執行緒獲得鎖,當在一個同步方法中存取另外物件上的同步方法時,會獲取這兩個物件鎖。
4、對於同步,要時刻清醒在哪個物件上同步,這是關鍵。
5、編寫執行緒安全的類,需要時刻注意對多個執行緒競爭存取資源的邏輯和安全做出正確的判斷,對「原子」操作做出分析,並保證原子操作期間別的執行緒無法存取競爭資源。
6、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞。
7、死鎖是執行緒間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程式,不一定好使,呵呵。但是,一旦程式發生死鎖,程式將死掉。
Java執行緒:執行緒的互動
執行緒互動是比較複雜的問題,SCJP要求不很基礎:給定一個場景,編寫程式碼來恰當使用等待、通知和通知所有執行緒。
一、執行緒互動的基礎知識
SCJP所要求的執行緒互動知識點需要從java.lang.Object的類的三個方法來學習:
[java] view plain copy
- void notify()——喚醒在此物件監視器上等待的單個執行緒。
- void notifyAll()——喚醒在此物件監視器上等待的所有執行緒。
- void wait()——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法。
當然,wait()還有另外兩個過載方法:
[java] view plain copy
- void wait(longtimeout)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
- void wait(longtimeout, int nanos)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
以上這些方法是幫助執行緒傳遞執行緒關心的時間狀態。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內呼叫wait()、notify()、notifyAll()方法。執行緒不能呼叫物件上等待或通知的方法,除非它擁有那個物件的鎖。
wait()、notify()、notifyAll()都是Object的實體方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該信號(通知)。執行緒通過執行物件上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的notify()方法爲止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證以何種順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。
下面 下麪看個例子就明白了:
[java] view plain copy
- /**
- * 計算輸出其他執行緒鎖計算的數據
- */
- public class ThreadA {
- public static void main(String[] args) {
- ThreadB b=new ThreadB();
- //啓動計算執行緒
- b.start();
- //執行緒A擁有b物件上的鎖。執行緒爲了呼叫wait()或notify()方法,該執行緒必須是那個物件鎖的擁有者
- synchronized (b) {
- try {
- System.out.println("等待物件b完成計算......");
- b.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("b物件計算的總和是:" + b.total);
- }
- }
- }
-
- /**
- * 計算1+2+3+...+100的和
- */
- public class ThreadB extends Thread {
- int total;
- public void run(){
- synchronized (this) {
- for (int i=0;i<101;i++){
- total+=i;
- }
- //(完成計算了)喚醒在此物件監視器上等待的單個執行緒,在本例中執行緒A被喚醒
- notify();
- }
- }
- }
執行結果:
[java] view plain copy
- 等待物件b完成計算......
- b物件計算的總和是:5050
千萬注意:
當在物件上呼叫wait()方法時,執行該程式碼的執行緒立即放棄它在物件上的鎖。然而呼叫notify()時,並不意味着這時執行緒會放棄其鎖。如果執行緒榮然在完成同步程式碼,則執行緒在移出之前不會放棄鎖。因此,只要呼叫notify()並不意味着這時該鎖變得可用。
二、多個執行緒在等待一個物件鎖時候使用notifyAll()
在多數情況下,最好通知等待某個物件的所有執行緒。如果這樣做,可以在物件上使用notifyAll()讓所有在此物件上等待的執行緒衝出等待區,返回到可執行狀態。
舉個例子:
[java] view plain copy
- /**
- * 計算執行緒
- */
- public class Calculator extends Thread {
- int total;
- @Override
- public void run() {
- synchronized (this) {
- for(int i=0;i<101;i++){
- total+=i;
- }
- }
- //通知所有在此物件上等待的執行緒
- notifyAll();
- }
- }
-
- /**
- * 獲取計算結果並輸出
- */
- public class ReaderResult extends Thread {
- Calculator c;
- public ReaderResult(Calculator c) {
- this.c = c;
- }
- public void run(){
- synchronized (c) {
- try {
- System.out.println(Thread.currentThread() + "等待計算結果......");
- c.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread()+ "計算結果爲:" + c.total);
- }
- }
- public static void main(String[] args) {
- Calculator calculator=new Calculator();
- //啓動三個執行緒,分別獲取計算結果
- new ReaderResult(calculator).start();
- new ReaderResult(calculator).start();
- new ReaderResult(calculator).start();
- //啓動計算執行緒
- calculator.start();
- }
- }
執行結果:
[java] view plain copy
- Thread[Thread-1,5,main]等待計算結果......
- Thread[Thread-2,5,main]等待計算結果......
- Thread[Thread-3,5,main]等待計算結果......
- Exception in thread"Thread-0" java.lang.IllegalMonitorStateException
- atjava.lang.Object.notifyAll(Native Method)
- attest.Calculator.run(Calculator.java:15)
- Thread[Thread-3,5,main]計算結果爲:5050
- Thread[Thread-2,5,main]計算結果爲:5050
- Thread[Thread-1,5,main]計算結果爲:5050
執行結果表明,程式中有異常,並且多次執行結果可能有多種輸出結果。這就是說明,這個多執行緒的互動程式還存在問題。究竟是出了什麼問題,需要深入的分析和思考,下面 下麪將做具體分析。
實際上,上面這個程式碼中,我們期望的是讀取結果的執行緒在計算執行緒呼叫notifyAll()之前等待即可。但是,如果計算執行緒先執行,並在讀取結果執行緒等待之前呼叫了notify()方法,那麼又會發生什麼呢?這種情況是可能發生的。因爲無法保證執行緒的不同部分將按照什麼順序來執行。幸運的是當讀取執行緒執行時,它只能馬上進入等待狀態----它沒有做任何事情來檢查等待的事件是否已經發生。 ----因此,如果計算執行緒已經呼叫了notifyAll()方法,那麼它就不會再次呼叫notifyAll(),----並且等待的讀取執行緒將永遠保持等待。這當然是開發者所不願意看到的問題。
因此,當等待的事件發生時,需要能夠檢查notifyAll()通知事件是否已經發生。
通常,解決上面問題的最佳方式是利用某種回圈,該回圈檢查某個條件表達式,只有當正在等待的事情還沒有發生的情況下,它才繼續等待。
Java執行緒:執行緒的排程-休眠
Java執行緒排程是Java多執行緒的核心,只有良好的排程,才能 纔能充分發揮系統的效能,提高程式的執行效率。
這裏要明確的一點,不管程式設計師怎麼編寫排程,只能最大限度的影響執行緒執行的次序,而不能做到精準控制。
執行緒休眠的目的是使執行緒讓出CPU的最簡單的做法之一,執行緒休眠時候,會將CPU資源交給其他執行緒,以便能輪換執行,當休眠一定時間後,執行緒會甦醒,進入準備狀態等待執行。
執行緒休眠的方法是Thread.sleep(long millis)和Thread.sleep(long millis, int nanos),均爲靜態方法,那呼叫sleep休眠的哪個執行緒呢?簡單說,哪個執行緒呼叫sleep,就休眠哪個執行緒。
[java] view plain copy
- /**
- * Java執行緒:執行緒的排程-休眠
- */
- public class TestSleep {
- public static void main(String[] args) {
- Thread t1=new MyThread1();
- Thread t2=new Thread(new MyRunnable());
- t1.start();
- t2.start();
- }
- }
- class MyThread1 extends Thread{
- @Override
- public void run() {
- for(int i=0;i<3;i++){
- System.out.println("執行緒1第"+i+"次執行!");
- try {
- Thread.sleep(50);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- class MyRunnable implements Runnable{
- @Override
- public void run() {
- for(int i=0;i<3;i++){
- System.out.println("執行緒2第"+i+"次執行!");
- try {
- Thread.sleep(50);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
執行結果:
[java] view plain copy
- 執行緒1第0次執行!
- 執行緒2第0次執行!
- 執行緒2第1次執行!
- 執行緒1第1次執行!
- 執行緒2第2次執行!
- 執行緒1第2次執行!
從上面的結果輸出可以看出,無法精準保證執行緒執行次序。
Java執行緒:執行緒的排程-優先順序
與執行緒休眠類似,執行緒的優先順序仍然無法保障執行緒的執行次序。只不過,優先順序高的執行緒獲取CPU資源的概率較大,優先順序低的並非沒機會執行。
執行緒的優先順序用1-10之間的整數表示,數值越大優先順序越高,預設的優先順序爲5。
在一個執行緒中開啓另外一個新執行緒,則新開執行緒稱爲該執行緒的子執行緒,子執行緒初始優先順序與父執行緒相同。
[java] view plain copy
- /**
- * Java執行緒:執行緒的排程-優先順序
- */
- public class TestPriority {
- public static void main(String[] args) {
- Thread t1=new MyThread1();
- Thread t2=new Thread(new MyRunnable());
- t1.setPriority(10);
- t2.setPriority(1);
- t1.start();
- t2.start();
- }
- }
- class MyThread1 extends Thread{
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println("執行緒1第"+i+"次執行!");
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- class MyRunnable implements Runnable{
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println("執行緒2第"+i+"次執行!");
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
執行結果:
[java] view plain copy
- 執行緒1第0次執行!
- 執行緒1第1次執行!
- 執行緒1第2次執行!
- 執行緒2第0次執行!
- 執行緒1第3次執行!
- 執行緒2第1次執行!
- 執行緒1第4次執行!
- 執行緒2第2次執行!
- 執行緒1第5次執行!
- 執行緒2第3次執行!
- 執行緒1第6次執行!
- 執行緒2第4次執行!
- 執行緒1第7次執行!
- 執行緒2第5次執行!
- 執行緒1第8次執行!
- 執行緒2第6次執行!
- 執行緒1第9次執行!
- 執行緒2第7次執行!
- 執行緒2第8次執行!
- 執行緒2第9次執行!
Java執行緒:執行緒的排程-讓步
執行緒的讓步含義就是使當前執行着執行緒讓出CPU資源,但是讓給誰不知道,僅僅是讓出,執行緒狀態回到可執行狀態。
執行緒的讓步使用Thread.yield()方法,yield()爲靜態方法,功能是暫停當前正在執行的執行緒物件,並執行其他執行緒。
[java] view plain copy
- /**
- * Java執行緒:執行緒的排程-讓步
- */
- public class Test {
- public static void main(String[] args) {
- Thread t1=new MyThread1();
- Thread t2=new Thread(new MyRunnable());
- t1.start();
- t2.start();
- }
- }
- class MyThread1 extends Thread{
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println("執行緒1第"+i+"次執行!");
- }
- }
- }
- class MyRunnable implements Runnable{
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println("執行緒2第"+i+"次執行!");
- Thread.yield();
- }
- }
- }
執行結果:
[java] view plain copy
- 執行緒2第0次執行!
- 執行緒1第0次執行!
- 執行緒1第1次執行!
- 執行緒1第2次執行!
- 執行緒1第3次執行!
- 執行緒1第4次執行!
- 執行緒1第5次執行!
- 執行緒1第6次執行!
- 執行緒1第7次執行!
- 執行緒1第8次執行!
- 執行緒1第9次執行!
- 執行緒2第1次執行!
- 執行緒2第2次執行!
- 執行緒2第3次執行!
- 執行緒2第4次執行!
- 執行緒2第5次執行!
- 執行緒2第6次執行!
- 執行緒2第7次執行!
- 執行緒2第8次執行!
- 執行緒2第9次執行!
Java執行緒:執行緒的排程-合併
執行緒的合併的含義就是將幾個並行執行緒的執行緒合併爲一個單執行緒執行,應用場景是當一個執行緒必須等待另一個執行緒執行完畢才能 纔能執行時可以使用join方法。
join爲非靜態方法,定義如下:
[java] view plain copy
- void join()——等待該執行緒終止。
- void join(longmillis)——等待該執行緒終止的時間最長爲 millis毫秒。
- void join(longmillis,int nanos)——等待該執行緒終止的時間最長爲 millis毫秒 + nanos 納秒。
[java] view plain copy
- /**
- * Java執行緒:執行緒的排程-合併
- */
- public class Test {
- public static void main(String[] args) {
- Thread t1=new MyThread1();
- t1.start();
- for (int i = 0; i < 20; i++) {
- System.out.println("主執行緒第" + i +"次執行!");
- if (i>2) {
- try {
- ///t1執行緒合併到主執行緒中,主執行緒停止執行過程,轉而執行t1執行緒,直到t1執行完畢後繼續。
- t1.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
- class MyThread1 extends Thread{
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- System.out.println("執行緒1第"+i+"次執行!");
- }
- }
- }
執行結果:
[java] view plain copy
- 主執行緒第0次執行!
- 主執行緒第1次執行!
- 主執行緒第2次執行!
- 主執行緒第3次執行!
- 執行緒1第0次執行!
- 執行緒1第1次執行!
- 執行緒1第2次執行!
- 執行緒1第3次執行!
- 執行緒1第4次執行!
- 執行緒1第5次執行!
- 執行緒1第6次執行!
- 執行緒1第7次執行!
- 執行緒1第8次執行!
- 執行緒1第9次執行!
- 主執行緒第4次執行!
- 主執行緒第5次執行!
- 主執行緒第6次執行!
- 主執行緒第7次執行!
- 主執行緒第8次執行!
- 主執行緒第9次執行!
- 主執行緒第10次執行!
- 主執行緒第11次執行!
- 主執行緒第12次執行!
- 主執行緒第13次執行!
- 主執行緒第14次執行!
- 主執行緒第15次執行!
- 主執行緒第16次執行!
- 主執行緒第17次執行!
- 主執行緒第18次執行!
- 主執行緒第19次執行!
Java執行緒:執行緒的排程-守護執行緒
守護執行緒與普通執行緒寫法上基本麼啥區別,呼叫執行緒物件的方法setDaemon(true),則可以將其設定爲守護執行緒。
守護執行緒使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做數據庫應用時候,使用的數據庫連線池,連線池本身也包含着很多後臺執行緒,監控連線個數、超時時間、狀態等等。
setDaemon方法的詳細說明:
[java] view plain copy
- public final void setDaemon(boolean on)將該執行緒標記爲守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java虛擬機器退出。
該方法必須在啓動執行緒前呼叫。
該方法首先呼叫該執行緒的 checkAccess方法,且不帶任何參數。這可能拋出 SecurityException(在當前執行緒中)。
參數:on - 如果爲true,則將該執行緒標記爲守護執行緒。
拋出:
IllegalThreadStateException- 如果該執行緒處於活動狀態。
SecurityException- 如果當前執行緒無法修改該執行緒。
另請參見:
isDaemon(),checkAccess()
[java] view plain copy
- /**
- * Java執行緒:執行緒的排程-守護執行緒
- */
- public class Test {
- public static void main(String[] args) {
- Thread t1=new MyCommon();
- Thread t2=new Thread(new MyDaemon());
- t2.setDaemon(true);//設定爲守護執行緒
- t2.start();
- t1.start();
- }
- }
- class MyCommon extends Thread{
- @Override
- public void run() {
- for(int i=0;i<5;i++){
- System.out.println("執行緒1第"+i+"次執行!");
- try {
- Thread.sleep(7);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- class MyDaemon implements Runnable{
- @Override
- public void run() {
- for (long i = 0; i < 9999999L; i++) {
- System.out.println("後臺執行緒第" + i +"次執行!");
- try {
- Thread.sleep(7);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
執行結果:
[java] view plain copy
- 執行緒1第0次執行!
- 後臺執行緒第0次執行!
- 後臺執行緒第1次執行!
- 執行緒1第1次執行!
- 後臺執行緒第2次執行!
- 執行緒1第2次執行!
- 後臺執行緒第3次執行!
- 執行緒1第3次執行!
- 後臺執行緒第4次執行!
- 執行緒1第4次執行!
- 後臺執行緒第5次執行!
- 後臺執行緒第6次執行!
- 後臺執行緒第7次執行!
- 後臺執行緒第8次執行!
- 後臺執行緒第9次執行!
- 後臺執行緒第10次執行!
從上面的執行結果可以看出:
前臺執行緒是保證執行完畢的,後臺執行緒還沒有執行完畢就退出了。
實際上:JRE判斷程式是否執行結束的標準是所有的前臺執執行緒行完畢了,而不管後臺執行緒的狀態,因此,在使用後臺縣城時候一定要注意這個問題。
Java執行緒:執行緒的同步-同步方法
執行緒的同步是保證多執行緒安全存取競爭資源的一種手段。
執行緒的同步是Java多執行緒程式設計的難點,往往開發者搞不清楚什麼是競爭資源、什麼時候需要考慮同步,怎麼同步等等問題,當然,這些問題沒有很明確的答案,但有些原則問題需要考慮,是否有競爭資源被同時改動的問題?
在本部分之前,請參閱《Java執行緒:執行緒的同步與鎖》部分,本部分是在此基礎上所寫的。
對於同步,在具體的Java程式碼中需要完成一下兩個操作:
把競爭存取的資源標識爲private;
同步哪些修改變數的程式碼,使用synchronized關鍵字同步方法或程式碼。
當然這不是唯一控制併發安全的途徑。
synchronized關鍵字使用說明
synchronized只能標記非抽象的方法,不能標識成員變數。
爲了演示同步方法的使用,構建了一個信用卡賬戶,起初信用額爲100w,然後模擬透支、存款等多個操作。顯然銀行賬戶User物件是個競爭資源,而多個併發操作的是賬戶方法oper(int x),當然應該在此方法上加上同步,並將賬戶的餘額設爲私有變數,禁止直接存取。
[java] view plain copy
- /**
- * Java執行緒:執行緒的同步
- */
- public class Test {
- public static void main(String[] args) {
- User u = new User("張三", 100);
- MyThread t1 = new MyThread("執行緒A", u, 20);
- MyThread t2 = new MyThread("執行緒B", u, -60);
- MyThread t3 = new MyThread("執行緒C", u, -80);
- MyThread t4 = new MyThread("執行緒D", u, -30);
- MyThread t5 = new MyThread("執行緒E", u, 32);
- MyThread t6 = new MyThread("執行緒F", u, 21);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- t5.start();
- t6.start();
- }
- }
-
- class MyThread extends Thread {
- private User u;
- private int y = 0;
-
- MyThread(String name, User u, int y) {
- super(name);
- this.u = u;
- this.y = y;
- }
- public void run() {
- u.oper(y);
- }
- }
-
- class User {
- private String code;
- private int cash;
- User(String code, int cash) {
- this.code = code;
- this.cash = cash;
- }
- public String getCode() {
- return code;
- }
- public void setCode(String code) {
- this.code = code;
- }
-
- /**
- * 業務方法
- * @param x 新增x萬元
- */
- public synchronized void oper(int x) {
- try {
- Thread.sleep(10L);
- this.cash += x;
- System.out.println(Thread.currentThread().getName() + "執行結束,增加「"
- + x + "」,當前使用者賬戶餘額爲:" + cash);
- Thread.sleep(10L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- @Override
- public String toString() {
- return "User{" + "code='" + code + '\'' + ",cash=" + cash + '}';
- }
- }
執行結果:
[java] view plain copy
- 執行緒A執行結束,增加「20」,當前使用者賬戶餘額爲:120
- 執行緒F執行結束,增加「21」,當前使用者賬戶餘額爲:141
- 執行緒D執行結束,增加「-30」,當前使用者賬戶餘額爲:111
- 執行緒B執行結束,增加「-60」,當前使用者賬戶餘額爲:51
- 執行緒E執行結束,增加「32」,當前使用者賬戶餘額爲:83
- 執行緒C執行結束,增加「-80」,當前使用者賬戶餘額爲:3
反面教材,不同步的情況,也就是去掉oper(int x)方法的synchronized修飾符,然後執行程式,結果如下:
[java] view plain copy
- 執行緒F執行結束,增加「21」,當前使用者賬戶餘額爲:121
- 執行緒D執行結束,增加「-30」,當前使用者賬戶餘額爲:91
- 執行緒B執行結束,增加「-60」,當前使用者賬戶餘額爲:31
- 執行緒E執行結束,增加「32」,當前使用者賬戶餘額爲:63
- 執行緒A執行結束,增加「20」,當前使用者賬戶餘額爲:3
- 執行緒C執行結束,增加「-80」,當前使用者賬戶餘額爲:-17
很顯然,上面的結果是錯誤的,導致錯誤的原因是多個執行緒併發訪問了競爭資源u,並對u的屬性做了改動。
可見同步的重要性。
注意:
通過前文可知,執行緒退出同步方法時將釋放掉方法所屬物件的鎖,但還應該注意的是,同步方法中還可以使用特定的方法對執行緒進行排程。這些方法來自於java.lang.Object類。
[java] view plain copy
- void notify()
- 喚醒在此物件監視器上等待的單個執行緒。
- void notifyAll()
- 喚醒在此物件監視器上等待的所有執行緒。
- void wait()
- 導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法。
- void wait(long timeout)
- 導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
- void wait(long timeout,int nanos)
- 導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
結合以上方法,處理多執行緒同步與互斥問題非常重要,著名的生產者-消費者例子就是一個經典的例子,任何語言多執行緒必學的例子。
Java執行緒:執行緒的同步-同步塊
對於同步,除了同步方法外,還可以使用同步程式碼塊,有時候同步程式碼塊會帶來比同步方法更好的效果。
追其同步的根本的目的,是控制競爭資源的正確的存取,因此只要在存取競爭資源的時候保證同一時刻只能一個執行緒存取即可,因此Java引入了同步程式碼快的策略,以提高效能。
在上個例子的基礎上,對oper方法做了改動,由同步方法改爲同步程式碼塊模式,程式的執行邏輯並沒有問題。
[java] view plain copy
- /**
- * Java執行緒:執行緒的同步-同步程式碼塊
- */
- public class Test {
- public static void main(String[] args) {
- User u = new User("張三", 100);
- MyThread t1 = new MyThread("執行緒A", u, 20);
- MyThread t2 = new MyThread("執行緒B", u, -60);
- MyThread t3 = new MyThread("執行緒C", u, -80);
- MyThread t4 = new MyThread("執行緒D", u, -30);
- MyThread t5 = new MyThread("執行緒E", u, 32);
- MyThread t6 = new MyThread("執行緒F", u, 21);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- t5.start();
- t6.start();
- }
- }
-
- class MyThread extends Thread{
- private User u;
- private int y = 0;
-
- MyThread(String name, User u, int y) {
- super(name);
- this.u = u;
- this.y = y;
- }
- public void run() {
- u.oper(y);
- }
- }
-
- class User {
- private String code;
- private int cash;
- User(String code, int cash) {
- this.code = code;
- this.cash = cash;
- }
- public String getCode() {
- return code;
- }
- public void setCode(String code) {
- this.code = code;
- }
-
- /**
- * 業務方法
- * @param x 新增x萬元
- */
- public void oper(int x) {
- try {
- Thread.sleep(10L);
- synchronized (this) {
- this.cash += x;
- System.out.println(Thread.currentThread().getName() + "執行結束,增加「"
- + x + "」,當前使用者賬戶餘額爲:" + cash);
- }
- Thread.sleep(10L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- @Override
- public String toString() {
- return "User{" + "code='" + code + '\'' + ",cash=" + cash + '}';
- }
- }
執行結果:
[java] view plain copy
- 執行緒B執行結束,增加「-60」,當前使用者賬戶餘額爲:40
- 執行緒D執行結束,增加「-30」,當前使用者賬戶餘額爲:10
- 執行緒F執行結束,增加「21」,當前使用者賬戶餘額爲:31
- 執行緒E執行結束,增加「32」,當前使用者賬戶餘額爲:63
- 執行緒C執行結束,增加「-80」,當前使用者賬戶餘額爲:-17
- 執行緒A執行結束,增加「20」,當前使用者賬戶餘額爲:3
注意:
在使用synchronized關鍵字時候,應該儘可能避免在synchronized方法或synchronized塊中使用sleep或者yield方法,因爲synchronized程式塊佔有着物件鎖,你休息那麼其他的執行緒只能一邊等着你醒來執行完了才能 纔能執行。不但嚴重影響效率,也不合邏輯。
同樣,在同步程式塊內呼叫yeild方法讓出CPU資源也沒有意義,因爲你佔用着鎖,其他互斥執行緒還是無法存取同步程式塊。當然與同步程式塊無關的執行緒可以獲得更多的執行時間。
Java執行緒:併發共同作業-生產者消費者模型
對於多執行緒程式來說,不管任何程式語言,生產者和消費者模型都是最經典的。就像學習每一門程式語言一樣,Hello World!都是最經典的例子。
實際上,準確說應該是「生產者-消費者-倉儲」模型,離開了倉儲,生產者消費者模型就顯得沒有說服力了。
對於此模型,應該明確一下幾點:
- 生產者僅僅在倉儲未滿時候生產,倉滿則停止生產。
- 消費者僅僅在倉儲有產品時候才能 纔能消費,倉空則等待。
- 當消費者發現倉儲沒產品可消費時候會通知生產者生產。
- 生產者在生產出可消費產品時候,應該通知等待的消費者去消費。
此模型將要結合java.lang.Object的wait與notify、notifyAll方法來實現以上的需求。這是非常重要的。
[java] view plain copy
- /**
- * Java執行緒:併發共同作業-生產者消費者模型
- */
- public class Test {
- public static void main(String[] args) {
- Godown godown=new Godown(30);
- Consumer c1=new Consumer(50,godown);
- Consumer c2=new Consumer(20,godown);
- Consumer c3=new Consumer(30,godown);
- Producer p1=new Producer(10,godown);
- Producer p2=new Producer(10,godown);
- Producer p3=new Producer(10,godown);
- Producer p4=new Producer(10,godown);
- Producer p5=new Producer(10,godown);
- Producer p6=new Producer(10,godown);
- Producer p7=new Producer(80,godown);
- c1.start();
- c2.start();
- c3.start();
- p1.start();
- p2.start();
- p3.start();
- p4.start();
- p5.start();
- p6.start();
- p7.start();
- }
- }
- /**
- * 倉庫
- */
- class Godown{
- public static final int max_size=100;//最大庫存量
- public int curnum;//當前庫存量
- Godown() {
- }
- Godown(int curnum){
- this.curnum=curnum;
- }
- /**
- * 生產指定數量的產品
- */
- public synchronized void produce(int neednum){
- //測試是否需要生產
- while(neednum+curnum>max_size){
- System.out.println("要生產的產品數量" + neednum +"超過剩餘庫存量" + (max_size - curnum) +",暫時不能執行生產任務!");
- try {
- //當前的生產執行緒等待
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- //滿足生產條件,則進行生產,這裏簡單的更改當前庫存量
- curnum+=neednum;
- System.out.println("已經生產了"+neednum+"個產品,現倉儲量爲"+curnum);
- //喚醒在此物件監視器上等待的所有執行緒
- notifyAll();
- }
- /**
- * 消費指定數量的產品
- */
- public synchronized void consume(int neednum){
- //測試是否可以消費
- while(curnum<neednum){
- try {
- //當前的生產執行緒等待
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- //滿足消費條件,則進行消費,這裏簡單的更改當前庫存
- curnum-=neednum;
- System.out.println("已經消費了" + neednum +"個產品,現倉儲量爲" + curnum);
- //喚醒在此物件監視器上等待的所有執行緒
- notifyAll();
- }
- }
- //生產者
- class Producer extends Thread{
- private int neednum;//生產產品的數量
- private Godown godown;//倉庫
- Producer(int neednum, Godown godown) {
- this.neednum = neednum;
- this.godown = godown;
- }
- public void run(){
- //生產指定數量的產品
- godown.produce(neednum);
- }
- }
- //消費者
- class Consumer extends Thread{
- private int neednum;//消費產品的數量
- private Godown godown;//倉庫
- Consumer(int neednum, Godown godown) {
- this.neednum = neednum;
- this.godown = godown;
- }
- public void run(){
- //消費指定數量的產品
- godown.consume(neednum);
- }
- }
執行結果:
[java] view plain copy
- 已經消費了20個產品,現倉儲量爲10
- 已經生產了10個產品,現倉儲量爲20
- 已經生產了10個產品,現倉儲量爲30
- 已經生產了10個產品,現倉儲量爲40
- 要生產的產品數量80超過剩餘庫存量60,暫時不能執行生產任務!
- 已經消費了30個產品,現倉儲量爲10
- 已經生產了10個產品,現倉儲量爲20
- 已經生產了10個產品,現倉儲量爲30
- 已經生產了10個產品,現倉儲量爲40
- 要生產的產品數量80超過剩餘庫存量60,暫時不能執行生產任務!
說明:
對於本例,要說明的是當發現不能滿足生產或者消費條件的時候,呼叫物件的wait方法,wait方法的作用是釋放當前執行緒的所獲得的鎖,並呼叫物件的notifyAll()方法,通知(喚醒)該物件上其他等待執行緒,使得其繼續執行。這樣,整個生產者、消費者執行緒得以正確的共同作業執行。
notifyAll() 方法,起到的是一個通知作用,不釋放鎖,也不獲取鎖。只是告訴該物件上等待的執行緒「可以競爭執行了,都醒來去執行吧」。
本例僅僅是生產者消費者模型中最簡單的一種表示,本例中,如果消費者消費的倉儲量達不到滿足,而又沒有生產者,則程式會一直處於等待狀態,這當然是不對的。實際上可以將此例進行修改,修改爲,根據消費驅動生產,同時生產兼顧倉庫,如果倉不滿就生產,並對每次最大消費量做個限制,這樣就不存在此問題了,當然這樣的例子更復雜,更難以說明這樣一個簡單模型。
Java執行緒:併發共同作業-死鎖
執行緒發生死鎖可能性很小,即使看似可能發生死鎖的程式碼,在執行時發生死鎖的可能性也是小之又小。
發生死鎖的原因一般是兩個物件的鎖相互等待造成的。
在《Java執行緒:執行緒的同步與鎖》部分,簡述了死鎖的概念與簡單例子,但是所給的例子是不完整的,這裏給出一個完整的例子。
[java] view plain copy
- /**
- * Java執行緒:併發共同作業-死鎖
- */
- public class Test {
- public static void main(String[] args) {
- DeadlockRisk dead = new DeadlockRisk();
- MyThread t1 = new MyThread(dead, 1, 2);
- MyThread t2 = new MyThread(dead, 3, 4);
- MyThread t3 = new MyThread(dead, 5, 6);
- MyThread t4 = new MyThread(dead, 7, 8);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- }
- }
-
- class MyThread extends Thread {
- private DeadlockRisk dead;
- private int a, b;
-
- MyThread(DeadlockRisk dead, int a, int b) {
- this.dead = dead;
- this.a = a;
- this.b = b;
- }
- @Override
- public void run() {
- dead.read();
- dead.write(a, b);
- }
- }
-
- class DeadlockRisk {
- private static class Resource {
- public int value;
- }
- private Resource resourceA = new Resource();
- private Resource resourceB = new Resource();
- public int read() {
- synchronized (resourceA) {
- System.out.println("read():" + Thread.currentThread().getName()
- + "獲取了resourceA的鎖!");
- synchronized (resourceB) {
- System.out.println("read():" + Thread.currentThread().getName()
- + "獲取了resourceB的鎖!");
- return resourceB.value + resourceA.value;
- }
- }
- }
- public void write(int a, int b) {
- synchronized (resourceB) {
- System.out.println("write():" + Thread.currentThread().getName()
- + "獲取了resourceA的鎖!");
- synchronized (resourceA) {
- System.out.println("write():"
- + Thread.currentThread().getName() + "獲取了resourceB的鎖!");
- resourceA.value = a;
- resourceB.value = b;
- }
- }
- }
- }
執行結果:
[java] view plain copy
- read():Thread-1獲取了resourceA的鎖!
- read():Thread-1獲取了resourceB的鎖!
- write():Thread-1獲取了resourceA的鎖!
- write():Thread-1獲取了resourceB的鎖!
- read():Thread-3獲取了resourceA的鎖!
- read():Thread-3獲取了resourceB的鎖!
- write():Thread-3獲取了resourceA的鎖!
- write():Thread-3獲取了resourceB的鎖!
- read():Thread-2獲取了resourceA的鎖!
- read():Thread-2獲取了resourceB的鎖!
- write():Thread-2獲取了resourceA的鎖!
- write():Thread-2獲取了resourceB的鎖!
- read():Thread-0獲取了resourceA的鎖!
- read():Thread-0獲取了resourceB的鎖!
- write():Thread-0獲取了resourceA的鎖!
- write():Thread-0獲取了resourceB的鎖!
Java執行緒:volatile關鍵字
Java語言套件含兩種內在的同步機制 機製:同步塊(或方法)和 volatile變數。這兩種機制 機製的提出都是爲了實現程式碼執行緒的安全性。其中 Volatile變數的同步性較差(但有時它更簡單並且開銷更低),而且其使用也更容易出錯。
談及到volatile關鍵字,不得不提的一篇文章是:《Java理論與實踐:正確使用 Volatile 變數》,這篇文章對volatile關鍵字的用法做了相當精闢的闡述。
之所以要單獨提出volatile這個不常用的關鍵字原因是這個關鍵字在高效能的多執行緒程式中也有很重要的用途,只是這個關鍵字用不好會出很多問題。
首先考慮一個問題,爲什麼變數需要volatile來修飾呢?
要搞清楚這個問題,首先應該明白計算機內部都做什麼了。比如做了一個i++操作,計算機內部做了三次處理:讀取-修改-寫入。
同樣,對於一個long型數據,做了個賦值操作,在32系統下需要經過兩步才能 纔能完成,先修改低32位元,然後修改高32位元。
假想一下,當將以上的操作放到一個多執行緒環境下操作時候,有可能出現的問題,是這些步驟執行了一部分,而另外一個執行緒就已經參照了變數值,這樣就導致了讀取髒數據的問題。
通過這個設想,就不難理解volatile關鍵字了。
volatile可以用在任何變數前面,但不能用於final變數前面,因爲final型的變數是禁止修改的。也不存線上程安全的問題。
更多的內容,請參看:《Java理論與實踐:正確使用 Volatile 變數》一文,寫得很好。
Java執行緒:新特徵-執行緒池
Sun在Java5中,對Java執行緒的類庫做了大量的擴充套件,其中執行緒池就是Java5的新特徵之一,除了執行緒池之外,還有很多多執行緒相關的內容,爲多執行緒的程式設計帶來了極大便利。爲了編寫高效穩定可靠的多執行緒程式,執行緒部分的新增內容顯得尤爲重要。
有關Java5執行緒新特徵的內容全部在java.util.concurrent下面 下麪,裏面包含數目衆多的介面和類,熟悉這部分API特徵是一項艱難的學習過程。目前有關這方面的資料和書籍都少之又少,大所屬介紹執行緒方面書籍還停留在java5之前的知識層面上。
當然新特徵對做多執行緒程式沒有必須的關係,在java5之前通用可以寫出很優秀的多執行緒程式。只是代價不一樣而已。
執行緒池的基本思想還是一種物件池的思想,開闢一塊記憶體空間,裏面存放了衆多(未死亡)的執行緒,池中執行緒執行排程由池管理器來處理。當有執行緒任務時,從池中取一個,執行完成後執行緒物件歸池,這樣可以避免反覆 反復建立執行緒物件所帶來的效能開銷,節省了系統的資源。
在Java5之前,要實現一個執行緒池是相當有難度的,現在Java5爲我們做好了一切,我們只需要按照提供的API來使用,即可享受執行緒池帶來的極大便利。
Java5的執行緒池分好多種:固定尺寸的執行緒池、單任務執行緒池、可變尺寸連線池、延遲連線池、單任務延遲連線池、自定義執行緒池。
在使用執行緒池之前,必須知道如何去建立一個執行緒池,在Java5中,需要瞭解的是java.util.concurrent.Executors類的API,這個類提供大量建立連線池的靜態方法,是必須掌握的。
一、固定大小的執行緒池
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- /**
- * Java執行緒:執行緒池
- */
- public class Test {
- public static void main(String[] args) {
- //建立一個可重用固定執行緒數的執行緒池
- ExecutorService pool =Executors.newFixedThreadPool(2);
- //建立實現了Runnable介面物件,Thread物件當然也實現了Runnable介面
- Thread t1 = new MyThread();
- Thread t2 = new MyThread();
- Thread t3 = new MyThread();
- Thread t4 = new MyThread();
- Thread t5 = new MyThread();
- //將執行緒放入執行緒池中進行執行
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- class MyThread extends Thread{
- public void run(){
- System.out.println(Thread.currentThread().getName()+"正在執行...");
- }
- }
執行結果:
[java] view plain copy
- pool-1-thread-1正在執行...
- pool-1-thread-2正在執行...
- pool-1-thread-2正在執行...
- pool-1-thread-2正在執行...
- pool-1-thread-1正在執行...
二、單任務執行緒池
在上例的基礎上改一行建立pool物件的程式碼爲:
[java] view plain copy
- <span style="white-space:pre"> </span>//建立一個使用單個 worker執行緒的 Executor,以無界佇列方式來執行該執行緒。
- ExecutorService pool=Executors.newSingleThreadExecutor();
執行結果:
[java] view plain copy
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
對於以上兩種連線池,大小都是固定的,當要加入的池的執行緒(或者任務)超過池最大尺寸時候,則入此執行緒池需要排隊等待。
一旦池中有執行緒完畢,則排隊等待的某個執行緒會入池執行。
三、可變尺寸的執行緒池
與上面的類似,只是改動下pool的建立方式:
[java] view plain copy
- <span style="white-space:pre"> </span>//建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。
- ExecutorService pool=Executors.newCachedThreadPool();
執行結果:
[java] view plain copy
- pool-1-thread-2正在執行...
- pool-1-thread-4正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-3正在執行...
- pool-1-thread-5正在執行...
四、延遲連線池
[java] view plain copy
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.TimeUnit;
-
- /**
- * Java執行緒:執行緒池
- */
- public class Test {
- public static void main(String[] args) {
- //建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
- ScheduledExecutorServicepool=Executors.newScheduledThreadPool(2);
- //建立實現了Runnable介面物件,Thread物件當然也實現了Runnable介面
- Threadt1 = new MyThread();
- Threadt2 = new MyThread();
- Thread t3 = new MyThread();
- Thread t4 = new MyThread();
- Thread t5 = new MyThread();
- //將執行緒放入執行緒池中進行執行
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- //使用延遲執行風格的方法
- pool.schedule(t4, 10, TimeUnit.MILLISECONDS);
- pool.schedule(t5, 10, TimeUnit.MILLISECONDS);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- class MyThread extends Thread{
- public void run(){
- System.out.println(Thread.currentThread().getName()+"正在執行...");
- }
- }
執行結果:
[java] view plain copy
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-2正在執行...
五、單任務延遲連線池
在四程式碼基礎上,做改動
[java] view plain copy
- //建立一個單任務執行執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
- ScheduledExecutorServicepool=Executors.newSingleThreadScheduledExecutor();
執行結果:
[java] view plain copy
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-1正在執行...
六、自定義執行緒池
[java] view plain copy
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
-
- /**
- * Java執行緒:執行緒池-自定義執行緒池
- */
- public class Test {
- public static void main(String[] args) {
- //建立等待佇列
- BlockingQueue<Runnable> bqueue=newArrayBlockingQueue<Runnable>(20);
- //建立一個單執行緒執行任務,它可安排在給定延遲後執行命令或者定期地執行。
- ThreadPoolExecutor pool=newThreadPoolExecutor(2, 3, 2, TimeUnit.MILLISECONDS, bqueue);
- //建立實現了Runnable介面物件,Thread物件當然也實現了Runnable介面
- Thread t1 = new MyThread();
- Thread t2 = new MyThread();
- Thread t3 = new MyThread();
- Thread t4 = new MyThread();
- Thread t5 = new MyThread();
- Thread t6 = new MyThread();
- Thread t7 = new MyThread();
- //將執行緒放入執行緒池中進行執行
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- pool.execute(t6);
- pool.execute(t7);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- class MyThread extends Thread{
- public void run(){
- System.out.println(Thread.currentThread().getName()+"正在執行...");
- try {
- Thread.sleep(100L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
執行結果:
[java] view plain copy
- pool-1-thread-1正在執行...
- pool-1-thread-2正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-2正在執行...
- pool-1-thread-1正在執行...
- pool-1-thread-2正在執行...
- pool-1-thread-1正在執行...
建立自定義執行緒池的構造方法很多,本例中參數ThreadPoolExecutor的含義如下:
[java] view plain copy
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue)
用給定的初始參數和預設的執行緒工廠及處理程式建立新的ThreadPoolExecutor。使用Executors工廠方法之一比使用此通用構造方法方便得多。
參數:
[java] view plain copy
- corePoolSize -池中所儲存的執行緒數,包括空閒執行緒。
- maximumPoolSize -池中允許的最大執行緒數。
- keepAliveTime -當執行緒數大於核心時,此爲終止前多餘的空閒執行緒等待新任務的最長時間。
- unit -keepAliveTime參數的時間單位。
- workQueue -執行前用於保持任務的佇列。此佇列僅保持由execute方法提交的Runnable任務。
拋出:
[java] view plain copy
- IllegalArgumentException-如果 corePoolSize或 keepAliveTime小於零,或者 maximumPoolSize小於或等於零,或者 corePoolSize大於 maximumPoolSize。
- NullPointerException-如果workQueue爲 null
- 自定義連線池稍微麻煩些,不過通過建立的ThreadPoolExecutor執行緒池物件,可以獲取到當前執行緒池的尺寸、正在執行任務的執行緒數、工作佇列等等。
Java執行緒:新特徵-有返回值的執行緒
在Java5之前,執行緒是沒有返回值的,常常爲了「有」返回值,破費周折,而且程式碼很不好寫。或者乾脆繞過這道坎,走別的路了。現在Java終於有可返回值的任務(也可以叫做執行緒)了。
可返回值的任務必須實現Callable介面,類似的,無返回值的任務必須Runnable介面。
執行Callable任務後,可以獲取一個Future的物件,在該物件上呼叫get就可以獲取到Callable任務返回的Object了。
下面 下麪是個很簡單的例子:
[java] view plain copy
- import java.util.concurrent.Callable;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Future;
-
- /**
- * Java執行緒:執行緒池-有返回值的執行緒
- */
- public class Test {
- public static void main(String[] args)throwsExecutionException,InterruptedException {
- //建立一個執行緒池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- //建立兩個有返回值的任務
- Callable c1=new MyCallable("A");
- Callable c2=new MyCallable("B");
- //執行任務並獲取Future物件
- Future f1=pool.submit(c1);
- Future f2=pool.submit(c2);
- //從Future物件上獲取任務的返回值,並輸出到控制檯
- System.out.println(">>>"+f1.get().toString());
- System.out.println(">>>"+f2.get().toString());
- //關閉執行緒池
- pool.shutdown();
- }
- }
- class MyCallable implements Callable{
- private String oid;
- MyCallable(String oid) {
- this.oid = oid;
- }
- @Override
- public Object call() throws Exception {
- return oid+"任務返回的內容";
-
- }
- }
執行結果:
[java] view plain copy
- >>>A任務返回的內容
- >>>B任務返回的內容
Java執行緒:新特徵-鎖(上)
在Java5中,專門提供了鎖物件,利用鎖可以方便的實現資源的封鎖,用來控制對競爭資源併發存取的控制,這些內容主要集中在java.util.concurrent.locks包下面 下麪,裏面有三個重要的介面Condition、Lock、ReadWriteLock。
[java] view plain copy
- Condition將Object監視器方法(wait、notify和 notifyAll)分解成截然不同的物件,以便通過將這些物件與任意Lock實現組合使用,爲每個物件提供多個等待 set(wait-set)。
- Lock實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。
- ReadWriteLock維護了一對相關的鎖定,一個用於只讀操作,另一個用於寫入操作。
舉個例子:
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
- /**
- * Java執行緒:執行緒池-鎖
- */
- public class Test {
- public static void main(String[] args){
- //建立併發存取的賬戶
- MyCount myCount=new MyCount("6215580000000000000",10000);
- //建立一個鎖物件
- Lock lock=new ReentrantLock();
- //建立一個執行緒池
- ExecutorService pool = Executors.newCachedThreadPool();
- //建立一些併發存取使用者,一個信用卡,存的存,取的取
- User u1 = new User("張三", myCount, -4000, lock);
- User u2 = new User("張三他爹", myCount, 6000, lock);
- User u3 = new User("張三他弟", myCount, -8000, lock);
- User u4 = new User("張三", myCount, 800, lock);
- //線上程池中執行各個使用者的操作
- pool.execute(u1);
- pool.execute(u2);
- pool.execute(u3);
- pool.execute(u4);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- //信用卡使用者
- class User implements Runnable{
- private String name; //使用者名稱
- private MyCount myCount; //所要操作的賬戶
- private int iocash; //操作的金額,當然有正負之分了
- private Lock myLock; //執行操作所需的鎖物件
- User(String name, MyCount myCount, int iocash, LockmyLock) {
- this.name = name;
- this.myCount = myCount;
- this.iocash = iocash;
- this.myLock = myLock;
- }
- @Override
- public void run() {
- //獲取鎖
- myLock.lock();
- //執行現金業務
- System.out.println(name + "正在操作" + myCount +"賬戶,金額爲" + iocash +",當前金額爲" + myCount.getCash());
- myCount.setCash(myCount.getCash() + iocash);
- System.out.println(name + "操作" + myCount +"賬戶成功,金額爲" + iocash +",當前金額爲" + myCount.getCash());
- //釋放鎖,否則別的執行緒沒有機會執行了
- myLock.unlock();
- }
- }
- //信用卡賬戶,可隨意透支
- class MyCount {
- private String oid; //賬號
- private int cash; //賬戶餘額
- MyCount(String oid, int cash) {
- this.oid = oid;
- this.cash = cash;
- }
- public String getOid() {
- return oid;
- }
- public void setOid(String oid) {
- this.oid = oid;
- }
- public int getCash() {
- return cash;
- }
- public void setCash(int cash) {
- this.cash = cash;
- }
- @Override
- public String toString() {
- return"MyCount{" +
- "oid='" + oid + '\'' +
- ", cash=" + cash +
- '}';
- }
- }
執行結果:
[java] view plain copy
- 張三他爹正在操作MyCount{oid='6215580000000000000', cash=10000}賬戶,金額爲6000,當前金額爲10000
- 張三他爹操作MyCount{oid='6215580000000000000', cash=16000}賬戶成功,金額爲6000,當前金額爲16000
- 張三正在操作MyCount{oid='6215580000000000000', cash=16000}賬戶,金額爲800,當前金額爲16000
- 張三操作MyCount{oid='6215580000000000000',cash=16800}賬戶成功,金額爲800,當前金額爲16800
- 張三正在操作MyCount{oid='6215580000000000000', cash=16800}賬戶,金額爲-4000,當前金額爲16800
- 張三操作MyCount{oid='6215580000000000000',cash=12800}賬戶成功,金額爲-4000,當前金額爲12800
- 張三他弟正在操作MyCount{oid='6215580000000000000', cash=12800}賬戶,金額爲-8000,當前金額爲12800
- 張三他弟操作MyCount{oid='6215580000000000000', cash=4800}賬戶成功,金額爲-8000,當前金額爲4800
從上面的輸出可以看到,利用鎖物件太方便了,比直接在某個不知情的物件上用鎖清晰多了。
但一定要注意的是,在獲取了鎖物件後,用完後應該儘快釋放鎖,以便別的等待該鎖的執行緒有機會去執行
Java執行緒:新特徵-鎖(下)
在上文中提到了Lock介面以及物件,使用它,很優雅的控制了競爭資源的安全存取,但是這種鎖不區分讀寫,稱這種鎖爲普通鎖。爲了提高效能,Java提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,在一定程度上提高了程式的執行效率。
Java中讀寫鎖有個介面java.util.concurrent.locks.ReadWriteLock,也有具體的實現ReentrantReadWriteLock,詳細的API可以檢視JavaAPI文件。
下面 下麪這個例子是在文例子的基礎上,將普通鎖改爲讀寫鎖,並新增賬戶餘額查詢的功能,程式碼如下:
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.locks.ReadWriteLock;
- import java.util.concurrent.locks.ReentrantReadWriteLock;
-
- /**
- * Java執行緒:執行緒池-鎖
- */
- public class Test {
- public static void main(String[] args){
- //建立併發存取的賬戶
- MyCount myCount=new MyCount("6215580000000000000",10000);
- //建立一個鎖物件
- ReadWriteLock lock=new ReentrantReadWriteLock(false);
- //建立一個執行緒池
- ExecutorService pool = Executors.newCachedThreadPool();
- //建立一些併發存取使用者,一個信用卡,存的存,取的取
- User u1 = new User("張三", myCount, -4000, lock, false);
- User u2 = new User("張三他爹", myCount, 6000, lock, false);
- User u3 = new User("張三他弟", myCount, -8000, lock, false);
- User u4 = new User("張三", myCount, 800, lock,false);
- User u5 = new User("張三他爹", myCount, 0, lock,true);
- //線上程池中執行各個使用者的操作
- pool.execute(u1);
- pool.execute(u2);
- pool.execute(u3);
- pool.execute(u4);
- pool.execute(u5);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- //信用卡使用者
- class User implements Runnable{
- private String name; //使用者名稱
- private MyCount myCount; //所要操作的賬戶
- private int iocash; //操作的金額,當然有正負之分了
- private ReadWriteLock myLock; //執行操作所需的鎖物件
- private boolean ischeck; //是否查詢
- User(String name, MyCount myCount, int iocash,ReadWriteLock myLock,boolean ischeck) {
- this.name = name;
- this.myCount = myCount;
- this.iocash = iocash;
- this.myLock = myLock;
- this.ischeck = ischeck;
- }
- @Override
- public void run() {
- if (ischeck) {
- //獲取讀鎖
- myLock.readLock().lock();
- System.out.println("讀:" + name +"正在查詢" + myCount +"賬戶,當前金額爲" + myCount.getCash());
- //釋放讀鎖
- myLock.readLock().unlock();
- } else {
- //獲取寫鎖
- myLock.writeLock().lock();
- //執行現金業務
- System.out.println("寫:" + name +"正在操作" + myCount +"賬戶,金額爲" + iocash +",當前金額爲" + myCount.getCash());
- myCount.setCash(myCount.getCash() + iocash);
- System.out.println("寫:" + name +"操作" + myCount +"賬戶成功,金額爲" + iocash +",當前金額爲" + myCount.getCash());
- //釋放寫鎖
- myLock.writeLock().unlock();
- }
- }
- }
- //信用卡賬戶,可隨意透支
- class MyCount {
- private String oid; //賬號
- private int cash; //賬戶餘額
- MyCount(String oid, int cash) {
- this.oid = oid;
- this.cash = cash;
- }
- public String getOid() {
- return oid;
- }
- public void setOid(String oid) {
- this.oid = oid;
- }
- public int getCash() {
- return cash;
- }
- public void setCash(int cash) {
- this.cash = cash;
- }
- @Override
- public String toString() {
- return"MyCount{" +
- "oid='" + oid + '\'' +
- ",cash=" + cash +
- '}';
- }
- }
執行結果:
[java] view plain copy
- 寫:張三他爹正在操作MyCount{oid='6215580000000000000', cash=10000}賬戶,金額爲6000,當前金額爲10000
- 寫:張三他爹操作MyCount{oid='6215580000000000000', cash=16000}賬戶成功,金額爲6000,當前金額爲16000
- 寫:張三正在操作MyCount{oid='6215580000000000000', cash=16000}賬戶,金額爲-4000,當前金額爲16000
- 寫:張三操作MyCount{oid='6215580000000000000', cash=12000}賬戶成功,金額爲-4000,當前金額爲12000
- 寫:張三他弟正在操作MyCount{oid='6215580000000000000', cash=12000}賬戶,金額爲-8000,當前金額爲12000
- 寫:張三他弟操作MyCount{oid='6215580000000000000', cash=4000}賬戶成功,金額爲-8000,當前金額爲4000
- 讀:張三他爹正在查詢MyCount{oid='6215580000000000000', cash=4000}賬戶,當前金額爲4000
- 寫:張三正在操作MyCount{oid='6215580000000000000', cash=4000}賬戶,金額爲800,當前金額爲4000
- 寫:張三操作MyCount{oid='6215580000000000000', cash=4800}賬戶成功,金額爲800,當前金額爲4800
在實際開發中,最好在能用讀寫鎖的情況下使用讀寫鎖,而不要用普通鎖,以求更好的效能。
Java執行緒:新特徵-號志
Java的號志實際上是一個功能完畢的計數器,對控制一定資源的消費與回收有着很重要的意義,號志常常用於多執行緒的程式碼中,並能監控有多少數目的執行緒等待獲取資源,並且通過號志可以得知可用資源的數目等等,這裏總是在強調「數目」二字,但不能指出來有哪些在等待,哪些資源可用。
因此,本人認爲,這個號志類如果能返回數目,還能知道哪些物件在等待,哪些資源可使用,就非常完美了,僅僅拿到這些概括性的數位,對精確控制意義不是很大。目前還沒想到更好的用法。
下面 下麪是一個簡單例子:
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Semaphore;
-
- /**
- * Java執行緒:執行緒池-號志
- */
- public class Test {
- public static void main(String[] args){
- MyPool myPool = new MyPool(20);
- //建立執行緒池
- ExecutorService threadPool = Executors.newFixedThreadPool(2);
- MyThread t1 = new MyThread("任務A", myPool, 3);
- MyThread t2 = new MyThread("任務B", myPool, 12);
- MyThread t3 = new MyThread("任務C", myPool, 7);
- //線上程池中執行任務
- threadPool.execute(t1);
- threadPool.execute(t2);
- threadPool.execute(t3);
- //關閉池
- threadPool.shutdown();
- }
- }
- class MyPool {
- private Semaphore sp; //池相關的號志
-
- /**
- * 池的大小,這個大小會傳遞給號志
- * @param size 池的大小
- */
- MyPool(int size) {
- this.sp =new Semaphore(size);
- }
-
- public Semaphore getSp() {
- return sp;
- }
-
- public void setSp(Semaphore sp) {
- this.sp = sp;
- }
- }
- class MyThread extends Thread {
- private String threadname; //執行緒的名稱
- private MyPool pool; //自定義池
- private int x; //申請號志的大小
-
- MyThread(String threadname, MyPool pool, int x) {
- this.threadname = threadname;
- this.pool = pool;
- this.x = x;
- }
- public void run() {
- try {
- //從此號志獲取給定數目的許可
- pool.getSp().acquire(x);
- //todo:也許這裏可以做更復雜的業務
- System.out.println(threadname + "成功獲取了" + x +"個許可!");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- //釋放給定數目的許可,將其返回到號志。
- pool.getSp().release(x);
- System.out.println(threadname + "釋放了" + x +"個許可!");
- }
- }
- }
執行結果:
[java] view plain copy
- 任務A成功獲取了3個許可!
- 任務B成功獲取了12個許可!
- 任務B釋放了12個許可!
- 任務A釋放了3個許可!
- 任務C成功獲取了7個許可!
- 任務C釋放了7個許可!
從結果可以看出,號志僅僅是對池資源進行監控,但不保證執行緒的安全,因此,在使用時候,應該自己控制執行緒的安全存取池資源。
Java執行緒:新特徵-阻塞佇列
阻塞佇列是Java5執行緒新特徵中的內容,Java定義了阻塞佇列的介面java.util.concurrent.BlockingQueue,阻塞佇列的概念是,一個指定長度的佇列,如果佇列滿了,新增新元素的操作會被阻塞等待,直到有空位爲止。同樣,當佇列爲空時候,請求佇列元素的操作同樣會阻塞等待,直到有可用元素爲止。
有了這樣的功能,就爲多執行緒的排隊等候的模型實現開闢了便捷通道,非常有用。
java.util.concurrent.BlockingQueue繼承了java.util.Queue介面,可以參看API文件。
下面 下麪給出一個簡單應用的例子:
[java] view plain copy
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
-
- /**
- * Java執行緒:執行緒池-阻塞佇列
- */
- public class Test {
- public static void main(String[] args) throws InterruptedException{
- BlockingQueue bqueue = new ArrayBlockingQueue(20);
- for (int i = 0; i < 30; i++) {
- //將指定元素新增到此佇列中,如果沒有可用空間,將一直等待(如果有必要)。
- bqueue.put(i);
- System.out.println("向阻塞佇列中新增了元素:" + i);
- }
- System.out.println("程式到此執行結束,即將退出----");
- }
- }
執行結果:
[java] view plain copy
- 向阻塞佇列中新增了元素:0
- 向阻塞佇列中新增了元素:1
- 向阻塞佇列中新增了元素:2
- 向阻塞佇列中新增了元素:3
- 向阻塞佇列中新增了元素:4
- 向阻塞佇列中新增了元素:5
- 向阻塞佇列中新增了元素:6
- 向阻塞佇列中新增了元素:7
- 向阻塞佇列中新增了元素:8
- 向阻塞佇列中新增了元素:9
- 向阻塞佇列中新增了元素:10
- 向阻塞佇列中新增了元素:11
- 向阻塞佇列中新增了元素:12
- 向阻塞佇列中新增了元素:13
- 向阻塞佇列中新增了元素:14
- 向阻塞佇列中新增了元素:15
- 向阻塞佇列中新增了元素:16
- 向阻塞佇列中新增了元素:17
- 向阻塞佇列中新增了元素:18
- 向阻塞佇列中新增了元素:19
可以看出,輸出到元素19時候,就一直處於等待狀態,因爲佇列滿了,程式阻塞了。
這裏沒有用多執行緒來演示,沒有這個必要。
另外,阻塞佇列還有更多實現類,用來滿足各種複雜的需求:ArrayBlockingQueue, DelayQueue,LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue,具體的API差別也很小。
Java執行緒:新特徵-阻塞棧
對於阻塞棧,與阻塞佇列相似。不同點在於棧是「後入先出」的結構,每次操作的是棧頂,而佇列是「先進先出」的結構,每次操作的是佇列頭。
這裏要特別說明一點的是,阻塞棧是Java6的新特徵。
Java爲阻塞棧定義了介面:java.util.concurrent.BlockingDeque,其實現類也比較多,具體可以檢視JavaAPI文件。
下面 下麪看一個簡單例子:
[java] view plain copy
- import java.util.concurrent.BlockingDeque;
- import java.util.concurrent.LinkedBlockingDeque;
-
- /**
- * Java執行緒:執行緒池-阻塞棧
- */
- public class Test {
- public static void main(String[] args) throws InterruptedException{
- BlockingDeque bDeque = new LinkedBlockingDeque(20);
- for (int i = 0; i < 30; i++) {
- //將指定元素新增到此阻塞棧中,如果沒有可用空間,將一直等待(如果有必要)。
- bDeque.putFirst(i);
- System.out.println("向阻塞棧中新增了元素:" + i);
- }
- System.out.println("程式到此執行結束,即將退出----");
- }
- }
執行結果:
[java] view plain copy
- 向阻塞棧中新增了元素:0
- 向阻塞棧中新增了元素:1
- 向阻塞棧中新增了元素:2
- 向阻塞棧中新增了元素:3
- 向阻塞棧中新增了元素:4
- 向阻塞棧中新增了元素:5
- 向阻塞棧中新增了元素:6
- 向阻塞棧中新增了元素:7
- 向阻塞棧中新增了元素:8
- 向阻塞棧中新增了元素:9
- 向阻塞棧中新增了元素:10
- 向阻塞棧中新增了元素:11
- 向阻塞棧中新增了元素:12
- 向阻塞棧中新增了元素:13
- 向阻塞棧中新增了元素:14
- 向阻塞棧中新增了元素:15
- 向阻塞棧中新增了元素:16
- 向阻塞棧中新增了元素:17
- 向阻塞棧中新增了元素:18
- 向阻塞棧中新增了元素:19
從上面結果可以看到,程式並沒結束,而是阻塞住了,原因是棧已經滿了,後面追加元素的操作都被阻塞了。
Java執行緒:新特徵-條件變數
條件變數是Java5執行緒中很重要的一個概念,顧名思義,條件變數就是表示條件的一種變數。但是必須說明,這裏的條件是沒有實際含義的,僅僅是個標記而已,並且條件的含義往往通過程式碼來賦予其含義。
這裏的條件和普通意義上的條件表達式有着天壤之別。
條件變數都實現了java.util.concurrent.locks.Condition介面,條件變數的範例化是通過一個Lock物件上呼叫newCondition()方法來獲取的,這樣,條件就和一個鎖物件系結起來了。因此,Java中的條件變數只能和鎖配合使用,來控制併發程式存取競爭資源的安全。
條件變數的出現是爲了更精細控制執行緒等待與喚醒,在Java5之前,執行緒的等待與喚醒依靠的是Object物件的wait()和notify()/notifyAll()方法,這樣的處理不夠精細。
而在Java5中,一個鎖可以有多個條件,每個條件上可以有多個執行緒等待,通過呼叫await()方法,可以讓執行緒在該條件下等待。當呼叫signalAll()方法,又可以喚醒該條件下的等待的執行緒。有關Condition介面的API可以具體參考JavaAPI文件。
條件變數比較抽象,原因是他不是自然語言中的條件概念,而是程式控制的一種手段。
下面 下麪以一個銀行存取款的模擬程式爲例來揭蓋Java多執行緒條件變數的神祕面紗:
有一個賬戶,多個使用者(執行緒)在同時操作這個賬戶,有的存款有的取款,存款隨便存,取款有限制,不能透支,任何試圖透支的操作都將等待裏面有足夠存款才執行操作。
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.locks.Condition;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
- /**
- * Java執行緒:條件變數
- */
- public class Test {
- public static void main(String[] args){
- //建立併發存取的賬戶
- MyCount myCount=new MyCount("6215580000000000000",10000);
- //建立一個執行緒池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- Thread t1 = new SaveThread("張三", myCount, 2000);
- Thread t2 = new SaveThread("李四", myCount, 3600);
- Thread t3 = new DrawThread("王五", myCount, 2700);
- Thread t4 = new SaveThread("老張", myCount, 600);
- Thread t5 = new DrawThread("老牛", myCount, 1300);
- Thread t6 = new DrawThread("胖子", myCount, 800);
- //執行各個執行緒
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- pool.execute(t6);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- //存款執行緒類
- class SaveThread extends Thread {
- private String name; //操作人
- private MyCount myCount; //賬戶
- private int x; //存款金額
-
- SaveThread(String name, MyCount myCount, int x) {
- this.name = name;
- this.myCount = myCount;
- this.x = x;
- }
- public void run() {
- myCount.saving(x, name);
- }
- }
- //取款執行緒類
- class DrawThread extends Thread {
- private String name; //操作人
- private MyCount myCount; //賬戶
- private int x; //存款金額
-
- DrawThread(String name, MyCount myCount, int x) {
- this.name = name;
- this.myCount = myCount;
- this.x = x;
- }
- public void run() {
- myCount.drawing(x, name);
- }
- }
- //普通銀行賬戶,不可透支
- class MyCount {
- private String oid; //賬號
- private int cash; //賬戶餘額
- private Lock lock =new ReentrantLock(); //賬戶鎖
- private Condition _save = lock.newCondition(); //存款條件
- private Condition _draw = lock.newCondition(); //取款條件
- MyCount(String oid, int cash) {
- this.oid = oid;
- this.cash = cash;
- }
- /**
- * 存款
- * @param x 存款金額
- * @param name 存款人
- */
- public void saving(int x,String name){
- lock.lock(); //獲取鎖
- if (x > 0) {
- cash += x; // 存款
- System.out.println(name + "存款" + x + ",當前餘額爲" + cash);
- }
- _draw.signalAll(); //喚醒所有等待執行緒。
- lock.unlock(); //釋放鎖
- }
- //取款
- public void drawing(int x, String name) {
- lock.lock(); //獲取鎖
- try {
- if (cash - x < 0) {
- _draw.await(); // 阻塞取款操作
- } else {
- cash -= x; // 取款
- System.out.println(name + "取款" + x + ",當前餘額爲" + cash);
- }
- _save.signalAll(); // 喚醒所有存款操作
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock(); // 釋放鎖
- }
- }
- }
執行結果:
[java] view plain copy
- 張三存款2000,當前餘額爲12000
- 王五取款2700,當前餘額爲9300
- 老張存款600,當前餘額爲9900
- 老牛取款1300,當前餘額爲8600
- 胖子取款800,當前餘額爲7800
- 李四存款3600,當前餘額爲11400
假如我們不用鎖和條件變數,如何實現此功能呢?下面 下麪是實現程式碼:
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- /**
- * Java執行緒:不用條件變數
- */
- public class Test {
- public static void main(String[] args){
- //建立併發存取的賬戶
- MyCount myCount=new MyCount("6215580000000000000",10000);
- //建立一個執行緒池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- Thread t1 = new SaveThread("張三", myCount, 2000);
- Thread t2 = new SaveThread("李四", myCount, 3600);
- Thread t3 = new DrawThread("王五", myCount, 2700);
- Thread t4 = new SaveThread("老張", myCount, 600);
- Thread t5 = new DrawThread("老牛", myCount, 1300);
- Thread t6 = new DrawThread("胖子", myCount, 800);
- //執行各個執行緒
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- pool.execute(t6);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- //存款執行緒類
- class SaveThread extends Thread {
- private String name; //操作人
- private MyCount myCount; //賬戶
- private int x; //存款金額
-
- SaveThread(String name, MyCount myCount, int x) {
- this.name = name;
- this.myCount = myCount;
- this.x = x;
- }
- public void run() {
- myCount.saving(x, name);
- }
- }
- //取款執行緒類
- class DrawThread extends Thread {
- private String name; //操作人
- private MyCount myCount; //賬戶
- private int x; //存款金額
-
- DrawThread(String name, MyCount myCount, int x) {
- this.name = name;
- this.myCount = myCount;
- this.x = x;
- }
- public void run() {
- myCount.drawing(x, name);
- }
- }
- //普通銀行賬戶,不可透支
- class MyCount {
- private String oid; //賬號
- private int cash; //賬戶餘額
- MyCount(String oid, int cash) {
- this.oid = oid;
- this.cash = cash;
- }
- /**
- * 存款
- * @param x 存款金額
- * @param name 存款人
- */
- public synchronized void saving(int x,Stringname){
- if (x > 0) {
- cash += x; // 存款
- System.out.println(name + "存款" + x + ",當前餘額爲" + cash);
- }
- notifyAll(); //喚醒所有等待執行緒。
- }
- //取款
- public synchronized void drawing(int x, String name){
- if (cash - x < 0) {
- try {
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- } else {
- cash -= x; // 取款
- System.out.println(name + "取款" + x + ",當前餘額爲" + cash);
- }
- notifyAll(); // 喚醒所有存款操作
- }
- }
執行結果:
[java] view plain copy
- 張三存款2000,當前餘額爲12000
- 王五取款2700,當前餘額爲9300
- 李四存款3600,當前餘額爲12900
- 老牛取款1300,當前餘額爲11600
- 胖子取款800,當前餘額爲10800
- 老張存款600,當前餘額爲11400
結合先前同步程式碼知識,舉一反三,將此例改爲同步程式碼塊來實現,程式碼如下:
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- /**
- * Java執行緒:改用同步程式碼塊
- */
- public class Test {
- public static void main(String[] args){
- //建立併發存取的賬戶
- MyCount myCount=new MyCount("6215580000000000000",10000);
- //建立一個執行緒池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- Thread t1 = new SaveThread("張三", myCount, 2000);
- Thread t2 = new SaveThread("李四", myCount, 3600);
- Thread t3 = new DrawThread("王五", myCount, 2700);
- Thread t4 = new SaveThread("老張", myCount, 600);
- Thread t5 = new DrawThread("老牛", myCount, 1300);
- Thread t6 = new DrawThread("胖子", myCount, 800);
- //執行各個執行緒
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- pool.execute(t6);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- //存款執行緒類
- class SaveThread extends Thread {
- private String name; //操作人
- private MyCount myCount; //賬戶
- private int x; //存款金額
-
- SaveThread(String name, MyCount myCount, int x) {
- this.name = name;
- this.myCount = myCount;
- this.x = x;
- }
- public void run() {
- myCount.saving(x, name);
- }
- }
- //取款執行緒類
- class DrawThread extends Thread {
- private String name; //操作人
- private MyCount myCount; //賬戶
- private int x; //存款金額
-
- DrawThread(String name, MyCount myCount, int x) {
- this.name = name;
- this.myCount = myCount;
- this.x = x;
- }
- public void run() {
- myCount.drawing(x, name);
- }
- }
- //普通銀行賬戶,不可透支
- class MyCount {
- private String oid; //賬號
- private int cash; //賬戶餘額
- MyCount(String oid, int cash) {
- this.oid = oid;
- this.cash = cash;
- }
- /**
- * 存款
- * @param x 存款金額
- * @param name 存款人
- */
- public void saving(int x,String name){
- if (x > 0) {
- synchronized (this) {
- cash += x; // 存款
- System.out.println(name + "存款" + x + ",當前餘額爲" + cash);
- notifyAll(); //喚醒所有等待執行緒。
- }
- }
- }
- //取款
- public synchronized void drawing(int x, String name) {
- synchronized (this) {
- if (cash - x < 0) {
- try {
- wait();
- }catch (InterruptedException e) {
- e.printStackTrace();
- }
- } else {
- cash -= x; // 取款
- System.out.println(name + "取款" + x + ",當前餘額爲" + cash);
- }
- }
- notifyAll(); // 喚醒所有存款操作
- }
- }
執行結果:
[java] view plain copy
- 李四存款3600,當前餘額爲13600
- 王五取款2700,當前餘額爲10900
- 老張存款600,當前餘額爲11500
- 老牛取款1300,當前餘額爲10200
- 胖子取款800,當前餘額爲9400
- 張三存款2000,當前餘額爲11400
對比以上三種方式,從控制角度上講,第一種最靈活,第二種程式碼最簡單,第三種容易犯錯。
Java執行緒:新特徵-原子量
所謂的原子量即操作變數的操作是「原子的」,該操作不可再分,因此是執行緒安全的。
爲何要使用原子變數呢,原因是多個執行緒對單個變數操作也會引起一些問題。在Java5之前,可以通過volatile、synchronized關鍵字來解決併發存取的安全問題,但這樣太麻煩。
Java5之後,專門提供了用來進行單變數多執行緒併發安全存取的工具包java.util.concurrent.atomic,其中的類也很簡單。
下面 下麪給出一個反面例子(切勿模仿):
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.atomic.AtomicLong;
-
- /**
- * Java執行緒:新特徵-原子量
- */
- public class Test {
- public static void main(String[] args){
- //建立一個執行緒池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- Runnable t1 = new MyRunnable("張三", 2000);
- Runnable t2 = new MyRunnable("李四", 3600);
- Runnable t3 = new MyRunnable("王五", 2700);
- Runnable t4 = new MyRunnable("老張", 600);
- Runnable t5 = new MyRunnable("老牛", 1300);
- Runnable t6 = new MyRunnable("胖子", 800);
- //執行各個執行緒
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- pool.execute(t6);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- class MyRunnable implements Runnable{
- private static AtomicLong aLong =newAtomicLong(10000); //原子量,每個執行緒都可以自由操作
- private String name; //操作人
- private int x; //運算元額
-
- MyRunnable(String name, int x) {
- this.name = name;
- this.x = x;
- }
- public void run() {
- System.out.println(name + "執行了" + x +",當前餘額:" + aLong.addAndGet(x));
- }
- }
多次執行結果:
[java] view plain copy
- 李四執行了3600,當前餘額:13600
- 王五執行了2700,當前餘額:16300
- 老張執行了600,當前餘額:16900
- 老牛執行了1300,當前餘額:18200
- 胖子執行了800,當前餘額:19000
- 張三執行了2000,當前餘額:21000
[java] view plain copy
- 張三執行了2000,當前餘額:12000
- 王五執行了2700,當前餘額:14700
- 老張執行了600,當前餘額:15300
- 老牛執行了1300,當前餘額:20200
- 李四執行了3600,當前餘額:18900
- 胖子執行了800,當前餘額:21000
[java] view plain copy
- 張三執行了2000,當前餘額:12000
- 王五執行了2700,當前餘額:14700
- 李四執行了3600,當前餘額:18300
- 老牛執行了1300,當前餘額:20200
- 胖子執行了800,當前餘額:21000
- 老張執行了600,當前餘額:18900
從執行結果可以看出,雖然使用了原子量,但是程式併發存取還是有問題,那究竟問題出在哪裏了?
這裏要注意的一點是,原子量雖然可以保證單個變數在某一個操作過程的安全,但無法保證你整個程式碼塊,或者整個程式的安全性。因此,通常還應該使用鎖等同步機制 機製來控制整個程式的安全性。
下面 下麪是對這個錯誤修正:
[java] view plain copy
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.atomic.AtomicLong;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
- /**
- * Java執行緒:新特徵-原子量
- */
- public class Test {
- public static void main(String[] args){
- //建立一個執行緒池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- Lock lock=new ReentrantLock(false);
- Runnable t1 = new MyRunnable("張三", 2000,lock);
- Runnable t2 = new MyRunnable("李四", 3600,lock);
- Runnable t3 = new MyRunnable("王五", 2700,lock);
- Runnable t4 = new MyRunnable("老張", 600,lock);
- Runnable t5 = new MyRunnable("老牛", 1300,lock);
- Runnable t6 = new MyRunnable("胖子", 800,lock);
- //執行各個執行緒
- pool.execute(t1);
- pool.execute(t2);
- pool.execute(t3);
- pool.execute(t4);
- pool.execute(t5);
- pool.execute(t6);
- //關閉執行緒池
- pool.shutdown();
- }
- }
- class MyRunnable implements Runnable {
- private static AtomicLong aLong =newAtomicLong(10000); //原子量,每個執行緒都可以自由操作
- private String name; //操作人
- private int x; //運算元額
- private Lock lock;
- MyRunnable(String name, int x,Lock lock) {
- this.name = name;
- this.x = x;
- this.lock=lock;
- }
- public void run() {
- lock.lock();
- System.out.println(name + "執行了" + x +",當前餘額:" + aLong.addAndGet(x));
- lock.unlock();
- }
- }
執行結果:
[java] view plain copy
- 張三執行了2000,當前餘額:12000
- 王五執行了2700,當前餘額:14700
- 老張執行了600,當前餘額:15300
- 老牛執行了1300,當前餘額:16600
- 胖子執行了800,當前餘額:17400
- 李四執行了3600,當前餘額:21000
這裏使用了一個物件鎖,來控制對併發程式碼的存取。不管執行多少次,執行次序如何,最終餘額均爲21000,這個結果是正確的。
有關原子量的用法很簡單,關鍵是對原子量的認識,原子僅僅是保證變數操作的原子性,但整個程式還需要考慮執行緒安全的。
Java執行緒:新特徵-障礙器
Java5中,新增了障礙器類,爲了適應一種新的設計需求,比如一個大型的任務,常常需要分配好多子任務去執行,只有當所有子任務都執行完成時候,才能 纔能執行主任務,這時候,就可以選擇障礙器了。
障礙器是多執行緒併發控制的一種手段,用法很簡單。下面 下麪給個例子:
[java] view plain copy
- import java.util.concurrent.BrokenBarrierException;
- import java.util.concurrent.CyclicBarrier;
-
- /**
- * Java執行緒:新特徵-障礙器
- */
- public class Test {
- public static void main(String[] args){
- //建立障礙器,並設定MainTask爲所有定數量的執行緒都達到障礙點時候所要執行的任務(Runnable)
- CyclicBarrier cb = new CyclicBarrier(7,new MainTask());
- new SubTask("A", cb).start();
- new SubTask("B", cb).start();
- new SubTask("C", cb).start();
- new SubTask("D", cb).start();
- new SubTask("E", cb).start();
- new SubTask("F", cb).start();
- new SubTask("G", cb).start();
- }
- }
- //主任務
- class MainTask implements Runnable {
- public void run() {
- System.out.println(">>>>主任務執行了!<<<<");
- }
- }
- //子任務
- class SubTask extends Thread {
- private String name;
- private CyclicBarrier cb;
- SubTask(String name, CyclicBarrier cb) {
- this.name = name;
- this.cb = cb;
- }
- public void run(){
- System.out.println("[子任務"+name+"]開始執行了!");
- for(int i=0;i<999999;i++);//模擬耗時的任務
- System.out.println("[子任務" + name +"]開始執行完成了,並通知障礙器已經完成!");
- try {
- //通知障礙器已經完成
- cb.await();
- } catch(InterruptedException e) {
- e.printStackTrace();
- } catch(BrokenBarrierException e) {
- e.printStackTrace();
- }
- }
- }
執行結果:
[java] view plain copy
- [子任務B]開始執行了!
- [子任務A]開始執行了!
- [子任務D]開始執行了!
- [子任務A]開始執行完成了,並通知障礙器已經完成!
- [子任務D]開始執行完成了,並通知障礙器已經完成!
- [子任務C]開始執行了!
- [子任務C]開始執行完成了,並通知障礙器已經完成!
- [子任務F]開始執行了!
- [子任務B]開始執行完成了,並通知障礙器已經完成!
- [子任務E]開始執行了!
- [子任務F]開始執行完成了,並通知障礙器已經完成!
- [子任務G]開始執行了!
- [子任務E]開始執行完成了,並通知障礙器已經完成!
- [子任務G]開始執行完成了,並通知障礙器已經完成!
- >>>>主任務執行了!<<<<
從執行結果可以看出,所有子任務完成的時候,主任務執行了,達到了控制的目標。