Java執行緒詳解(深度好文)
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的構造方法:
- 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介面的多執行緒例子
- /**
- * 實現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();
- }
- }
執行結果:
- 屈原:0
- 李白:0
- 屈原:1
- 李白:1
- 屈原:2
- 李白:2
- 李白:3
- 屈原:3
- 李白:4
- 屈原:4
2、擴充套件Thread類實現的多執行緒例子
- /**
- * 測試擴充套件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();
- }
- }
執行結果:
- 屈原: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規範不保證合理的輪換。
睡眠的實現:呼叫靜態方法。
- try {
- Thread.sleep(123);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
睡眠的位置:為了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣才能保證該執行緒執行過程中會睡眠。
例如,在前面的例子中,將一個耗時的操作改為睡眠,以減慢執行緒的執行。可以這麼寫:
- 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);
- }
執行結果:
- 李白:0
- 李白:1
- 屈原:0
- 李白:2
- 屈原:1
- 李白:3
- 屈原:2
- 李白:4
- 屈原:3
- 屈原:4
這樣,執行緒在每次執行過程中,總會睡眠3毫秒,睡眠了,其他的執行緒就有機會執行了。
注意:
1、執行緒睡眠是幫助所有執行緒獲得執行機會的最好方法。
2、執行緒睡眠到期自動甦醒,並返回到可執行狀態,不是執行狀態。sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就開始執行。
3、sleep()是靜態方法,只能控制當前正在執行的執行緒。
下面給個例子:
- /**
- * 一個計數器,計數到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();
- }
- }
執行結果:
- --------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)更改執行緒的優先順序。例如:
- Thread t = new MyThread();
- t.setPriority(8);
- t.start();
執行緒優先順序為1~10之間的正整數,JVM從不會改變一個執行緒的優先順序。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先順序進行每兩個或多個合併,變成少於10個的優先順序,則兩個或多個優先順序的執行緒可能被對映為一個優先順序。
執行緒預設優先順序是5,Thread類中有三個常量,定義執行緒優先順序範圍:
- 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不能工作。例如:
- 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物件上的資料。
- 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);
- }
- }
執行結果:
- 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)在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:
- 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 staticsynchronized int setName(String name){
- Xxx.name = name;
- }
等價於
- public static intsetName(String name){
- synchronized(Xxx.class){
- Xxx.name = name;
- }
- }
四、如果執行緒不能獲得鎖會怎麼樣
如果執行緒試圖進入同步方法,而其鎖已經被佔用,則執行緒在該物件上被阻塞。實質上,執行緒進入該物件的一種池中,必須在那裡等待,直到其鎖被釋放,該執行緒再次變為可執行或執行為止。
當考慮阻塞時,一定要注意哪個物件正被用於鎖定:
1、呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同物件,則每個執行緒有自己的物件的鎖,執行緒間彼此互不干預。
2、呼叫同一個類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的Class物件上。
3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class物件上,非靜態方法鎖定在該類的物件上。
4、對於同步程式碼塊,要看清楚什麼物件已經用於鎖定(synchronized後面括號的內容)。在同一個物件上進行同步的執行緒將彼此阻塞,在不同物件上鎖定的執行緒將永遠不會彼此阻塞。
五、何時需要同步
在多個執行緒同時訪問互斥(可交換)資料時,應該同步以保護資料,確保兩個執行緒不會同時修改更改它。
對於非靜態欄位中可更改的資料,通常使用非靜態方法訪問。
對於靜態欄位中可更改的資料,通常使用靜態方法訪問。
如果需要在非靜態方法中使用靜態欄位,或者在靜態欄位中呼叫非靜態方法,問題將變得非常複雜。
六、執行緒安全類
當一個類已經很好的同步以保護它的資料時,這個類就稱為“執行緒安全的”。
即使是執行緒安全類,也應該特別小心,因為操作的執行緒之間仍然不一定安全。
舉個形象的例子,比如一個集合是執行緒安全的,有兩個執行緒在操作同一個集合物件,當第一個執行緒查詢集合非空後,刪除集合中所有元素的時候。第二個執行緒也來執行與第一個執行緒相同的操作,也許在第一個執行緒查詢後,第二個執行緒也查詢出集合非空,但是當第一個執行清除後,第二個再執行刪除顯然是不對的,因為此時集合已經為空了。
舉個例子:
- 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();
- }
- }
執行結果:
- 蘇東坡
- null
雖然集合物件
- private List nameList =Collections.synchronizedList(new LinkedList());
是同步的,但是程式還不是執行緒安全的。
出現這種事件的原因是,上例中一個執行緒操作列表過程中無法阻止另外一個執行緒對列表的其他操作。
解決上面問題的辦法是,在操作集合物件的NameList上面做一個同步。改寫後的程式碼如下:
- 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程式來說,是很複雜的,也很難發現問題。當兩個執行緒被阻塞,每個執行緒在等待另一個執行緒時就發生死鎖。
還是看一個比較直觀的死鎖例子:
- 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的類的三個方法來學習:
- void notify()——喚醒在此物件監視器上等待的單個執行緒。
- void notifyAll()——喚醒在此物件監視器上等待的所有執行緒。
- void wait()——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法。
當然,wait()還有另外兩個過載方法:
- void wait(longtimeout)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
- void wait(longtimeout, int nanos)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
以上這些方法是幫助執行緒傳遞執行緒關心的時間狀態。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內呼叫wait()、notify()、notifyAll()方法。執行緒不能呼叫物件上等待或通知的方法,除非它擁有那個物件的鎖。
wait()、notify()、notifyAll()都是Object的例項方法。與每個物件具有鎖一樣,每個物件可以有一個執行緒列表,他們等待來自該訊號(通知)。執行緒通過執行物件上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到呼叫物件的notify()方法為止。如果多個執行緒在同一個物件上等待,則將只選擇一個執行緒(不保證以何種順序)繼續執行。如果沒有執行緒等待,則不採取任何特殊操作。
下面看個例子就明白了:
- /**
- * 計算輸出其他執行緒鎖計算的資料
- */
- 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();
- }
- }
- }
執行結果:
- 等待物件b完成計算......
- b物件計算的總和是:5050
千萬注意:
當在物件上呼叫wait()方法時,執行該程式碼的執行緒立即放棄它在物件上的鎖。然而呼叫notify()時,並不意味著這時執行緒會放棄其鎖。如果執行緒榮然在完成同步程式碼,則執行緒在移出之前不會放棄鎖。因此,只要呼叫notify()並不意味著這時該鎖變得可用。
二、多個執行緒在等待一個物件鎖時候使用notifyAll()
在多數情況下,最好通知等待某個物件的所有執行緒。如果這樣做,可以在物件上使用notifyAll()讓所有在此物件上等待的執行緒衝出等待區,返回到可執行狀態。
舉個例子:
- /**
- * 計算執行緒
- */
- 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();
- }
- }
執行結果:
- 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執行緒:執行緒的排程-休眠
- */
- 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();
- }
- }
- }
- }
執行結果:
- 執行緒1第0次執行!
- 執行緒2第0次執行!
- 執行緒2第1次執行!
- 執行緒1第1次執行!
- 執行緒2第2次執行!
- 執行緒1第2次執行!
從上面的結果輸出可以看出,無法精準保證執行緒執行次序。
Java執行緒:執行緒的排程-優先順序
與執行緒休眠類似,執行緒的優先順序仍然無法保障執行緒的執行次序。只不過,優先順序高的執行緒獲取CPU資源的概率較大,優先順序低的並非沒機會執行。
執行緒的優先順序用1-10之間的整數表示,數值越大優先順序越高,預設的優先順序為5。
在一個執行緒中開啟另外一個新執行緒,則新開執行緒稱為該執行緒的子執行緒,子執行緒初始優先順序與父執行緒相同。
- /**
- * 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();
- }
- }
- }
- }
執行結果:
- 執行緒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執行緒:執行緒的排程-讓步
- */
- 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();
- }
- }
- }
執行結果:
- 執行緒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次執行!
- 執行