Java線程大總結 原文章地址:一篇很老的專欄,但是現在看起來也感覺深受啟發,知識點很多,很多線程特點我沒有看,尷尬。但是還是整理了一下排版,轉載一下。 操作系統中線程和進程的概念 在現代操作系統中,進程支持多線程。進程是資源管理的最小單元;線程是程式執行的最小單元。 為了實現程式的併發執行引入了進 ...
Java線程大總結 原文章地址:一篇很老的專欄,但是現在看起來也感覺深受啟發,知識點很多,很多線程特點我沒有看,尷尬。但是還是整理了一下排版,轉載一下。
操作系統中線程和進程的概念
在現代操作系統中,進程支持多線程。進程是資源管理的最小單元;線程是程式執行的最小單元。
為了實現程式的併發執行引入了進程的概念(程式段、數據段、PCB三部分)。每個進程都有自己獨立的一塊記憶體空間,進程是程式的一個執行過程,進程之間可以併發執行。
線程是指進程中的一個執行流程,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
Java中的線程
在Java中,“線程”指兩件不同的事情:
1、java.lang.Thread類的一個實例;
2、線程的執行。
使用java.lang.Thread類或者java.lang.Runnable介面編寫代碼來定義、實例化和啟動新線程。
一個Thread類實例只是一個對象,像Java中的任何其他對象一樣,具有變數和方法,生死於堆上。
Java中,每個線程都有一個調用棧,即使不在程式中創建任何新的線程,線程也在後臺運行著。
一個Java應用總是從main()方法開始運行,mian()方法運行在一個線程內,它被稱為主線程。
一旦創建一個新的線程,就產生一個新的調用棧。
線程總體分兩類:用戶線程和守候線程。
當所有用戶線程執行完畢的時候,JVM自動關閉。但是守候線程卻不獨立於JVM,守候線程一般是由操作系統或者用戶自己創建的
定義線程
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的構造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
啟動線程
線上程的Thread對象上調用start()方法,而不是run()或者別的方法。
在調用start()方法之前:線程處於新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。
在調用start()方法之後:發生了一系列複雜的事情。
啟動新的執行線程(具有新的調用棧);
該線程從新狀態轉移到可運行狀態;
當該線程獲得機會執行時,其目標run()方法將運行。
註意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的。但並不啟動新的線程。
例子
1、實現Runnable介面的多線程例子
/**
* 實現Runnable介面的類
*
* @author leizhimin 2008-9-13 18:12:10
*/
publicclass DoSomething implements Runnable {
private String name;
public DoSomething(String name) {
this.name = name;
}
publicvoid run() {
for (int i = 0; i < 5; i++) {
for (long k = 0; k < 100000000; k++) ;
System.out.println(name + ": " + i);
}
}
}
/**
* 測試Runnable類實現的多線程程式
*
* @author leizhimin 2008-9-13 18:15:02
*/
public class TestRunnable {
public staticvoid main(String[] args) {
DoSomething ds1 = new DoSomething("阿三");
DoSomething ds2 = new DoSomething("李四");
Thread t1 = new Thread(ds1);
Thread t2 = new Thread(ds2);
t1.start();
t2.start();
}
}
執行結果:
李四: 0
阿三: 0
李四: 1
阿三: 1
李四: 2
李四: 3
阿三: 2
李四: 4
阿三: 3
阿三: 4
Process finished with exit code 0
2、擴展Thread類實現的多線程例子
/**
* 測試擴展Thread類實現的多線程程式
*
* @author leizhimin 2008-9-13 18:22:13
*/
publicclass TestThreadextends Thread{
public TestThread(String name) {
super(name);
}
publicvoid run() {
for(int i = 0;i<5;i++){
for(long k= 0; k <100000000;k++);
System.out.println(this.getName()+" :"+i);
}
}
publicstaticvoid main(String[] args) {
Thread t1 = new TestThread("阿三");
Thread t2 = new TestThread("李四");
t1.start();
t2.start();
}
}
執行結果:
阿三 :0
李四 :0
阿三 :1
李四 :1
阿三 :2
李四 :2
阿三 :3
阿三 :4
李四 :3
李四 :4
Process finished with exit code 0
對於上面的多線程程式代碼來說,輸出的結果是不確定的。其中的一條語句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(long millis)和Thread.sleep(long millis, int nanos)靜態方法強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。當線程睡眠時,它入睡在某個地方,在蘇醒之前不會返回到可運行狀態。當睡眠時間到期,則返回到可運行狀態。
線程睡眠的原因:線程執行太快,或者需要強制進入下一輪,因為Java規範不保證合理的輪換。
睡眠的實現:調用靜態方法。
try {
Thread.sleep(123);
} catch (InterruptedException e) {
e.printStackTrace();
}
睡眠的位置:為了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程中會睡眠。
註意:
1、線程睡眠是幫助所有線程獲得運行機會的最好方法。
2、線程睡眠到期自動蘇醒,並返回到可運行狀態,不是運行狀態。sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始執行。
3、sleep()是靜態方法,只能控制當前正在運行的線程。
下麵給個例子:
/**
* 一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字元串
*
* @author leizhimin 2008-9-14 9:53:49
*/
publicclass MyThreadextends Thread {
publicvoid 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();
}
}
}
publicstaticvoid main(String[] args) {
new MyThread().start();
}
}
-------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毫秒!
Process finished with exit code 0
2、線程的優先順序和線程讓步yield()
線程的讓步是通過Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的線程對象,並執行其他線程。
要理解yield(),必須瞭解線程的優先順序的概念。線程總是存在優先順序,優先順序範圍在1~10之間。JVM線程調度程式是基於優先順序的搶先調度機制。在大多數情況下,當前運行的線程優先順序將大於或等於線程池中任何線程的優先順序。但這僅僅是大多數情況。
註意:當設計多線程應用程式的時候,一定不要依賴於線程的優先順序。因為線程調度優先順序操作是沒有保障的,只能把線程優先順序作用作為一種提高程式效率的方法,但是要保證程式不依賴這種操作。
當線程池中線程都具有相同的優先順序,調度程式的JVM實現自由選擇它喜歡的線程。這時候調度程式的操作有兩種可能:一是選擇一個線程運行,直到它阻塞或者運行完成為止。二是時間分片,為池內的每個線程提供均等的運行機會。
設置線程的優先順序:線程預設的優先順序是創建它的執行線程的優先順序。可以通過setPriority(int newPriority)更改線程的優先順序。例如:
Thread t = new MyThread();
t.setPriority(8);
t.start();
線程優先順序為1~10之間的正整數,JVM從不會改變一個線程的優先順序。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先順序進行每兩個或多個合併,變成少於10個的優先順序,則兩個或多個優先順序的線程可能被映射為一個優先順序。
線程預設優先順序是5,Thread類中有三個常量,定義線程優先順序範圍:
static int MAX_PRIORITY
線程可以具有的最高優先順序。
static int MIN_PRIORITY
線程可以具有的最低優先順序。
static int NORM_PRIORITY
分配給線程的預設優先順序。
3、Thread.yield()方法
Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程。
yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先順序的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先順序的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程式再次選中。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
4、join()方法
Thread的非靜態方法join()讓一個線程B“加入”到另外一個線程A的尾部。在A執行完畢之前,B不能工作。例如:
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對象上的數據。
publicclass Foo {
privateint x = 100;
publicint getX() {
return x;
}
publicint fix(int y) {
x = x - y;
return x;
}
}
publicclass MyRunnableimplements Runnable {
private Foo foo =new Foo();
publicstaticvoid main(String[] args) {
MyRunnable r = new MyRunnable();
Thread ta = new Thread(r,"Thread-A");
Thread tb = new Thread(r,"Thread-B");
ta.start();
tb.start();
}
publicvoid 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());
}
}
publicint fix(int y) {
return foo.fix(y);
}
}
運行結果:
Thread-A : 當前foo對象的x值= 40
Thread-B : 當前foo對象的x值= 40
Thread-B : 當前foo對象的x值= -20
Thread-A : 當前foo對象的x值= -50
Thread-A : 當前foo對象的x值= -80
Thread-B : 當前foo對象的x值= -80
Process finished with exit code 0
從結果發現,這樣的輸出值明顯是不合理的。原因是兩個線程不加控制的訪問Foo對象並修改其數據所致。
如果要保持結果的合理性,只需要達到一個目的,就是將對Foo的訪問加以限制,每次只能有一個線程在訪問。這樣就能保證Foo對象中數據的合理性了。
在具體的Java代碼中需要完成一下兩個操作:
把競爭訪問的資源類Foo變數x標識為private;
同步哪些修改變數的代碼,使用synchronized關鍵字同步方法或代碼。
同步和鎖定
1、鎖的原理
Java中每個對象都有一個內置鎖
當程式運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
當程式運行到synchronized同步方法或代碼塊時,該對象鎖才起作用。
一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
關於鎖和同步,有一下幾個要點:
- 只能同步方法,而不能同步變數和類;
- 每個對象只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個對象上同步?
- 不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
- 如果兩個線程要執行一個類中的synchronized方法,如果一個線程在對象上獲得一個鎖,就沒有任何其他線程可以進入(該對象的)類中的任何一個同步方法。
- 如果線程擁有同步和非同步方法,則非同步方法可以被多個線程自由訪問而不受鎖的限制。
- 線程睡眠時,它所持的任何鎖都不會釋放。
- 線程可以獲得多個鎖。比如,在一個對象的同步方法裡面調用另外一個對象的同步方法,則獲取了兩個對象的同步鎖。
- 同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分代碼塊。
- 在使用同步代碼塊時候,應該指定在哪個對象上同步,也就是說要獲取哪個對象的鎖。例如:
public int fix(int y) {
synchronized (this) {
x = x - y;
}
return x;
}
當然,同步方法也可以改寫為非同步方法,但功能完全一樣的,例如:
public synchronized int getX() {
return x++;
}
與
public int getX() {
synchronized (this) {
return x;
}
}
效果是完全一樣的。
靜態方法同步
要同步靜態方法,需要一個用於整個類對象的鎖,這個對象是就是這個類(XXX.class)。
例如:
public static synchronized int setName(String name){
Xxx.name = name;
}
等價於
public static int setName(String name){
synchronized(Xxx.class){
Xxx.name = name;
}
}
如果線程不能獲得鎖會怎麼樣
如果線程試圖進入同步方法,而其鎖已經被占用,則線程在該對象上被阻塞。實質上,線程進入該對象的的一種池中,必須在哪裡等待,直到其鎖被釋放,該線程再次變為可運行或運行為止。
當考慮阻塞時,一定要註意哪個對象正被用於鎖定:
1、調用同一個對象中非靜態同步方法的線程將彼此阻塞。如果是不同對象,則每個線程有自己的對象的鎖,線程間彼此互不幹預。
2、調用同一個類中的靜態同步方法的線程將彼此阻塞,它們都是鎖定在相同的Class對象上。
3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class對象上,非靜態方法鎖定在該類的對象上。
4、對於同步代碼塊,要看清楚什麼對象已經用於鎖定(synchronized後面括弧的內容)。在同一個對象上進行同步的線程將彼此阻塞,在不同對象上鎖定的線程將永遠不會彼此阻塞。
何時需要同步
在多個線程同時訪問互斥(可交換)數據時,應該同步以保護數據,確保兩個線程不會同時修改更改它。
對於非靜態欄位中可更改的數據,通常使用非靜態方法訪問。
對於靜態欄位中可更改的數據,通常使用靜態方法訪問。
如果需要在非靜態方法中使用靜態欄位,或者在靜態欄位中調用非靜態方法,問題將變得非常複雜。已經超出SJCP考試範圍了。
線程安全類
當一個類已經很好的同步以保護它的數據時,這個類就稱為“線程安全的”。
即使是線程安全類,也應該特別小心,因為操作的線程之間仍然不一定安全。
舉個形象的例子,比如一個集合是線程安全的,有兩個線程在操作同一個集合對象,當第一個線程查詢集合非空後,刪除集合中所有元素的時候。第二個線程也來執行與第一個線程相同的操作,也許在第一個線程查詢後,第二個線程也查詢出集合非空,但是當第一個執行清除後,第二個再執行刪除顯然是不對的,因為此時集合已經為空了。
看個代碼:
publicclass NameList {
private List nameList = Collections.synchronizedList(new LinkedList());
publicvoid add(String name) {
nameList.add(name);
}
public String removeFirst() {
if (nameList.size() > 0) {
return (String) nameList.remove(0);
} else {
returnnull;
}
}
}
publicclass Test {
publicstaticvoid main(String[] args) {
final NameList nl =new NameList();
nl.add("aaa");
class NameDropper extends Thread{
publicvoid run(){
String name = nl.removeFirst();
System.out.println(name);
}
}
Thread t1 = new NameDropper();
Thread t2 = new NameDropper();
t1.start();
t2.start();
}
}
雖然集合對象 private List nameList = Collections.synchronizedList(new LinkedList()); 是同步的,但是程式還不是線程安全的。
出現這種事件的原因是,上例中一個線程操作列表過程中無法阻止另外一個線程對列表的其他操作。
解決上面問題的辦法是,在操作集合對象的NameList上面做一個同步。改寫後的代碼如下:
publicclass NameList {
private List nameList = Collections.synchronizedList(new LinkedList());
publicsynchronizedvoid add(String name) {
nameList.add(name);
}
publicsynchronized String removeFirst() {
if (nameList.size() > 0) {
return (String) nameList.remove(0);
} else {
return null;
}
}
}
這樣,當一個線程訪問其中一個同步方法時,其他線程只有等待。
線程死鎖
死鎖對Java程式來說,是很複雜的,也很難發現問題。當兩個線程被阻塞,每個線程在等待另一個線程時就發生死鎖。
還是看一個比較直觀的死鎖例子:
publicclass DeadlockRisk {
privatestaticclass Resource {
publicint value;
}
private Resource resourceA =new Resource();
private Resource resourceB =new Resource();
publicint read() {
synchronized (resourceA) {
synchronized (resourceB) {
return resourceB.value + resourceA.value;
}
}
}
publicvoid 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的類的三個方法來學習:
void notify()
喚醒在此對象監視器上等待的單個線程。
void notifyAll()
喚醒在此對象監視器上等待的所有線程。
void wait()
導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法。
當然,wait()還有另外兩個重載方法:
void wait(long timeout)
導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
void wait(long timeout, int nanos)
導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量。
以上這些方法是幫助線程傳遞線程關心的時間狀態。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。
wait()、notify()、notifyAll()都是Object的實例方法。與每個對象具有鎖一樣,每個對象可以有一個線程列表,他們等待來自該信號(通知)。線程通過執行對象上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到調用對象的notify()方法為止。如果多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續執行。如果沒有線程等待,則不採取任何特殊操作。
下麵看個例子就明白了:
/**
* 計算輸出其他線程鎖計算的數據
*
* @author leizhimin 2008-9-15 13:20:38
*/
publicclass ThreadA {
public static void main(String[] args) {
ThreadB b = new ThreadB();
//啟動計算線程
b.start();
//線程A擁有b對象上的鎖。線程為了調用wait()或notify()方法,該線程必須是那個對象鎖的擁有者
synchronized (b) {
try {
System.out.println("等待對象b完成計算。。。");
//當前線程A等待
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b對象計算的總和是:" + b.total);
}
}
}
/**
* 計算1+2+3 ... +100的和
*
* @author leizhimin 2008-9-15 13:20:49
*/
public class ThreadB extends Thread {
int total;
publicvoid run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
//(完成計算了)喚醒在此對象監視器上等待的單個線程,在本例中線程A被喚醒
notify();
}
}
}
等待對象b完成計算。。。
b對象計算的總和是:5050
Process finished with exit code 0
千萬註意:
當在對象上調用wait()方法時,執行該代碼的線程立即放棄它在對象上的鎖。然而調用notify()時,並不意味著這時線程會放棄其鎖。如果線程榮然在完成同步代碼,則線程在移出之前不會放棄鎖。因此,只要調用notify()並不意味著這時該鎖變得可用。
二、多個線程在等待一個對象鎖時候使用notifyAll()
在多數情況下,最好通知等待某個對象的所有線程。如果這樣做,可以在對象上使用notifyAll()讓所有在此對象上等待的線程衝出等待區,返回到可運行狀態。
下麵給個例子:
/**
* 計算線程
*
* @author leizhimin 2008-9-20 11:15:46
*/
publicclass Calculator extends Thread {
int total;
publicvoid run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
}
//通知所有在此對象上等待的線程
notifyAll();
}
}
/**
* 獲取計算結果並輸出
* @author leizhimin 2008-9-20 11:15:22
*/
publicclass ReaderResultextends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
publicvoid run() {
synchronized (c) {
try {
System.out.println(Thread.currentThread() + "等待計算結果。。。");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
}
}
publicstaticvoid main(String[] args) {
Calculator calculator = new Calculator();
//啟動三個線程,分別獲取計算結果
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
//啟動計算線程
calculator.start();
}
}
運行結果:
Thread[Thread-1,5,main]等待計算結果。。。
Thread[Thread-2,5,main]等待計算結果。。。
Thread[Thread-3,5,main]等待計算結果。。。
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner
at java.lang.Object.notifyAll(Native Method)
at threadtest.Calculator.run(Calculator.java:18)
Thread[Thread-1,5,main]計算結果為:5050
Thread[Thread-2,5,main]計算結果為:5050
Thread[Thread-3,5,main]計算結果為:5050
Process finished with exit code 0
運行結果表明,程式中有異常,並且多次運行結果可能有多種輸出結果。這就是說明,這個多線程的交互程式還存在問題。究竟是出了什麼問題,需要深入的分析和思考,下麵將做具體分析。
實際上,上面這個代碼中,我們期望的是讀取結果的線程在計算線程調用notifyAll()之前等待即可。但是,如果計算線程先執行,併在讀取結果線程等待之前調用了notify()方法,那麼又會發生什麼呢?這種情況是可能發生的。因為無法保證線程的不同部分將按照什麼順序來執行。幸運的是當讀取線程運行時,它只能馬上進入等待狀態----它沒有做任何事情來檢查等待的事件是否已經發生。 ----因此,如果計算線程已經調用了notifyAll()方法,那麼它就不會再次調用notifyAll(),----並且等待的讀取線程將永遠保持等待。這當然是開發者所不願意看到的問題。
因此,當等待的事件發生時,需要能夠檢查notifyAll()通知事件是否已經發生。
Java線程:線程的調度-休眠
Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提高程式的執行效率。
這裡要明確的一點,不管程式員怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能做到精準控制。
線程休眠的目的是使線程讓出CPU的最簡單的做法之一,線程休眠時候,會將CPU資源交給其他線程,以便能輪換執行,當休眠一定時間後,線程會蘇醒,進入準備狀態等待執行。
線程休眠的方法是Thread.sleep(long millis)和Thread.sleep(long millis, int nanos),均為靜態方法,那調用sleep休眠的哪個線程呢?簡單說,哪個線程調用sleep,就休眠哪個線程。
/**
* Java線程:線程的調度-休眠
*
* @author leizhimin 2009-11-4 9:02:40
*/
publicclass Test {
publicstaticvoid main(String[] args) {
Thread t1 = new MyThread1();
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
publicvoid run() {
for (int i = 0; i < 3; i++) {
System.out.println("線程1第" + i + "次執行!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnableimplements Runnable {
publicvoid run() {
for (int i = 0; i < 3; i++) {
System.out.println("線程2第" + i + "次執行!");
try {
Thread.sleep(50);
} catch (InterruptedExceptio