Java併發程式設計之多執行緒
詳細請看我部落格:Java併發程式設計之多執行緒 - 小簡部落格 (ideaopen.cn)
我們首先,先要了解什麼是程序,什麼是執行緒。
首先,我們看看程序。我們如果允許一個程式,它卡死了,我們通常會去工作管理員裡面將程序結束。
所以,這裡所看見的,就是程序。
那麼,何為執行緒呢?
首先,看看來自知乎的解釋:
執行緒是程序中執行運算的最小單位,是程序中的一個實體,是被系統獨立排程和分派的基本單位,執行緒自己不擁有系統資源,只擁有一點在執行中必不可少的資源,但它可與同屬一個程序的其它執行緒共享程序所擁有的全部資源。一個執行緒可以建立和撤消另一個執行緒,同一程序中的多個執行緒之間可以併發執行。
啥意思呢?通俗的說。QQ
和 Chrome
瀏覽器是兩個程序,Chrome
程序裡面有很多執行緒,例如 HTTP
請求執行緒、事件響應執行緒、渲染執行緒等等,執行緒的併發執行使得在瀏覽器中點選一個新連結從而發起 HTTP 請求時,瀏覽器還可以響應使用者的其它事件,同時你開多個視窗瀏覽網頁也沒問題。
想了解得更詳細點,可以看看知乎這一篇——>
然後,騰訊雲還有一篇,連結中文,不好分享,我直接截圖。
多執行緒優勢
多執行緒是多工的一種特別的形式,但多執行緒使用了更小的資源開銷。
這裡定義和執行緒相關的另一個術語 - 程序:一個程序包括由作業系統分配的記憶體空間,包含一個或多個執行緒。一個執行緒不能獨立的存在,它必須是程序的一部分。一個程序一直執行,直到所有的非守護執行緒都結束執行後才能結束。
多執行緒能滿足程式設計師編寫高效率的程式來達到充分利用 CPU 的目的。
優勢很多,我就直接去網上搬運來了,能達到學會的目的就可以,不在意是不是自己描述出來。
一、多執行緒優勢
採用多執行緒技術的應用程式可以更好地利用系統資源。主要優勢在於充分利用了CPU的空閒時間片,用盡可能少的時間來對使用者的要求做出響應,使得程序的整體執行效率得到較大提高,同時增強了應用程式的靈活性。由於同一程序的所有執行緒是共享同一記憶體,所以不需要特殊的資料傳送機制,不需要建立共享儲存區或共享檔案,從而使得不同任務之間的協調操作與執行、資料的互動、資源的分配等問題更加易於解決。
執行緒同步,在多執行緒應用中,考慮不同執行緒之間的資料同步和防止死鎖。當兩個或多個執行緒之間同時等待對方釋放資源的時候就會形成執行緒之間的死鎖。為了防止死鎖的發生,需要通過同步來實現執行緒安全。在Visual Basic中提供了三種方法來完成執行緒的同步。在Java中可用
synchronized
關鍵字。二、程式碼域同步
使用
Monitor
類可以同步靜態/例項化的方法的全部程式碼或者部分程式碼段。三、手工同步
可以使用不同的同步類建立自己的同步機制。這種同步方式要求你自己手動的為不同的域和方法同步,這種同步方式也可以用於程序間的同步和解除由於對共享資源的等待而造成的死鎖。
四、上下文同步
使用
SynchronizationAttribute
為ContextBoundObject
物件建立簡單的,自動同步。這種同步方式僅用於例項化的方法和域的同步。所有在同一個上下文域的物件共享同一個鎖。總結多執行緒的好處,使用執行緒可以把佔據時間長的程式中的任務放到後臺去處理;使用者介面更加吸引人,這樣比如使用者點選了一個按鈕去觸發某件事件的處理,可以彈出一個進度條來顯示處理的進度;程式的執行效率可能會提高;在一些等待的任務實現上如使用者輸入,檔案讀取和網路收發資料等,執行緒就比較有用了。
以上很多部分,如果看不懂,可能是許多東西還未涉及到。
實現多執行緒
我們實現Java的多執行緒呢,有4中方法。
1.繼承Thread類建立執行緒
2.實現Runnable介面建立執行緒
3.實現Callable介面通過FutureTask
包裝器來建立Thread執行緒
4.使用ExecutorService
、Callable
、Future
實現有返回結果的執行緒(執行緒池方式)
多執行緒如何執行?
菜鳥教程其他的不行,圖還是好用,我們看看這張圖。
如果正常執行的話,路徑就是這樣的:
新建執行緒——>就緒狀態——>執行狀態——>死亡狀態
Thread類建立執行緒
使用Thread
類建立執行緒,我們首先需要繼承它,並且重寫run
方法。
滿足這兩個條件就可以。
我這裡,為了體現多執行緒的併發,我使用了Time
下的LocalTime
類,來體現時間的變化。
sleep()
方法可以設定延遲,也就是說,執行一次後,我這裡需要1000
毫秒在執行下一次,我加個迴圈執行一下。
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println(this.getName()+"多執行緒輸出"+LocalTime.now());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
在建立一個入口類,執行起來。
package Thread;
public class ThreadTest {
public static void main(String[] args) {
ThreadDemo t = new ThreadDemo();
t.start();
ThreadDemo t1 = new ThreadDemo();
t1.start();
ThreadDemo t2 = new ThreadDemo();
t2.start();
}
}
這裡建立了三個執行緒,使用start()
方法可以執行起來,呼叫run
方法。
注意!!!不是.run()
,是.start()
。
執行下了,結果如圖。
Thread-2多執行緒輸出18:29:22.345
Thread-1多執行緒輸出18:29:22.345
Thread-0多執行緒輸出18:29:22.345
//sleep(1000),延遲1s後繼續執行
Thread-1多執行緒輸出18:29:23.356
Thread-2多執行緒輸出18:29:23.356
Thread-0多執行緒輸出18:29:23.356
//sleep(1000),延遲1s後繼續執行
Thread-1多執行緒輸出18:29:24.357
Thread-2多執行緒輸出18:29:24.357
Thread-0多執行緒輸出18:29:24.357
看看後面的時間,T
、T1
、T2
都是同時執行的,比如第一次,都是18:29:22.345
這個時間。
至於為啥三個物件順序不一樣,這就相當於擠公交,鬼知道誰先擠進去呢?嘿嘿。
Thread 方法
下表列出了Thread類的一些重要方法:
序號 | 方法描述 |
---|---|
1 | public void start() 使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的 run 方法。 |
2 | public void run() 如果該執行緒是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。 |
3 | public final void setName(String name) 改變執行緒名稱,使之與引數 name 相同。 |
4 | public final void setPriority(int priority) 更改執行緒的優先順序。 |
5 | public final void setDaemon(boolean on) 將該執行緒標記為守護執行緒或使用者執行緒。 |
6 | public final void join(long millisec) 等待該執行緒終止的時間最長為 millis 毫秒。 |
7 | public void interrupt() 中斷執行緒。 |
8 | public final boolean isAlive() 測試執行緒是否處於活動狀態。 |
上述方法是被 Thread 物件呼叫的,下面表格的方法是 Thread 類的靜態方法。
序號 | 方法描述 |
---|---|
1 | public static void yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒。 |
2 | public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。 |
3 | public static boolean holdsLock(Object x) 當且僅當當前執行緒在指定的物件上保持監視器鎖時,才返回 true。 |
4 | public static Thread currentThread() 返回對當前正在執行的執行緒物件的引用。 |
5 | public static void dumpStack() 將當前執行緒的堆疊跟蹤列印至標準錯誤流。 |
方法用法都一樣,請自行斟酌。
Runnable介面建立執行緒
因為和Thread
建立執行緒類似,我就直接放程式碼了。
package Runnable;
import java.time.LocalTime;
public class RunnableDemo implements Runnable {
@Override
public void run() {
//設定延遲
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"多執行緒輸出"+ LocalTime.now());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這裡也是兩個條件。
implements Runnable
和@Override run
。繼承介面,重寫run
方法。
Thread.currentThread().getName()
這個是返回當前呼叫的主執行緒的名字。
執行類就有點不一樣了。
package Runnable;
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
t.start();
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
t1.start();
RunnableDemo r2 = new RunnableDemo();
Thread t2 = new Thread(r2);
t2.start();
}
}
//建立執行緒物件
RunnableDemo r = new RunnableDemo();
//將執行緒物件放在Thread類物件重
Thread t = new Thread(r);
//呼叫start方法
t.start();
執行結果也一樣。
為何要用Runnable
那有人就說了,為啥這個東西多了一步,還麻煩,我怎麼不直接用Thread
呢?
我們首先要明白,Java語言不可以多繼承。
兩者實現方式帶來最明顯的區別就是,由於Java不允許多繼承,因此實現了Runnable介面可以再繼承其他類,但是Thread明顯不可以。
Runnable可以實現多個相同的程式程式碼的執行緒去共享同一個資源,而Thread並不是不可以,而是相比於Runnable來說,不太適合。
執行緒的排程
執行緒的排程的操作,有如下常用方法。
方法 | 作用 |
---|---|
int getPriority() | 返回執行緒的優先順序 |
void setPrority(int newPrority) | 更改執行緒的優先順序 |
boolean isAlive() | 判斷執行緒是否處於活動狀態 |
void join() | 使程序中其他執行緒等待該執行緒終止後再執行 |
void yield() | 暫停當前正在執行的執行緒物件並允許其他執行緒 |
執行緒優先順序
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
//設定優先順序
t.setPriority(Thread.MAX_PRIORITY);
t.start();
這樣就可以設定執行緒物件優先順序,優先順序有三個常量。
MIN_PRIORITY //值為1 最低
NORM_PRIORITY //值為5 普通級別
MAX_PRIORITY //值為10 最高
執行緒強制
package Runnable;
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
//執行緒強制執行
//join強制
try {
t.start();
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
t1.start();
RunnableDemo r2 = new RunnableDemo();
Thread t2 = new Thread(r2);
t2.start();
}
}
join
還有兩個過載方法,可以去自己瞭解。
執行緒禮讓
yield
是靜態方法,直接用類呼叫就可以。
注意!!!
上面的設定優先順序,是不能完完全全一個不漏的把控住的,只是優先順序越高,先執行的機率越高。
yield的禮讓也是如此,不是一定,是提高概率,不是絕對禮讓。
而我們的
join
是絕對的
執行緒的同步
首先,我們需要了解,為什麼同步。
為什麼需要同步
執行緒的安全問題
- 多個執行緒執行的不確定性硬氣執行結果的不穩定性
- 多個執行緒對賬本的共享, 會造成操作的不完整性, 會破壞資料.
- 多個執行緒訪問共享的資料時可能存在安全性問題
比如:
賣票過程中出現了重票和錯票的情況 (以下多視窗售票demo存在多執行緒安全問題)。
當票數為1的時候,三個執行緒中有執行緒被阻塞沒有執行票數-1的操作,這是其它執行緒就會通過if語句的判斷,這樣一來就會造成多賣了一張票,出現錯票的情況。
極端情況為,當票數為1時,三個執行緒同時判斷通過,進入阻塞,然後多執行兩側賣票操作。
所以,執行緒的同步是為了防止多個執行緒訪問一個數據物件時,對資料造成的破壞。
那麼,執行緒同步的原因,就解釋清楚了,我得準備一些程式碼來寫關於同步鎖的文章。
所以這篇文章先到這吧!