本文將比較Program、Process與Thread的定義,說明基本概念,再以Java的不同方式展示多執行緒的特點。
兩岸名詞比較
關於執行緒,網路上有很多學習資源是用大陸慣用語撰寫的,所以畫張對照表當作筆記。遇到中文描述「並行」,需仔細根據上下文判斷是指Concorrent還是Parallel。這篇文章將使用台灣地區的中文名稱撰寫
英文 | 台灣用語 | 大陸用語 |
---|---|---|
Process | 程序 | 進程 |
Thread | 執行緒 | 線程 |
Concorrent | 並行 | 並發 |
Parallel | 平行 | 並行 |
Program、Process 與 Thread
- Program 程式:一整組程式代碼,因為尚未被執行,所以保留在次級儲存裝置。
- Process 程序:已在記憶體中執行的程式,也是OS分配資源的最小單位,它的獨立ID就是在cmd裡要手動關閉程式的pid。不同程序不會共享資源,所以不需要對資源管理做特殊處理。
- Thread 執行緒:一個程序中至少含有一個執行緒,是OS能進行運算排程的最小單位。不同執行緒會共享資源,如果多個執行緒同時存取資源,可能會發生同步(synchronized)問題,例如:A執行緒更新的值被B執行緒覆蓋掉。
所以多程序(multiprocessing)的優點是讓電腦在同一單位時間內可以完成多個不同的程序;多執行緒(multithreading)的優點則是讓同一個程序可以在較短的時間內完成。
多執行緒的簡易範例
假設我們現在想要製作10萬個執行緒,每個執行緒都在初始值為0的計數器上 + 1,最後將計數器的最終值打印出來,可以寫出以下這段程式:
public class ThreadTest {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100000];
for(int i = 0; i < 100000; i++){
threads[i] = new Thread(()->{
counter++;
});
threads[i].start();
}
for(Thread thread:threads){
thread.join();
}
System.out.println(counter);
}
}
類別 Thread 可以幫我們產生一個執行緒,start 方法則表示執行這個執行緒預先寫入的業務邏輯;join 方法則是會封鎖主執行緒,讓Main方法的這個執行緒,在其他Thread物件執行完畢後,再繼續執行。
問題
理想上,執行打印出的結果是100000,但實際上卻並非如此,有時是99988、有時是99998,並不固定。這是因為 counter++
實際上是 counter = counter + 1;
,如果有2個執行緒同時執行就可能發生計算後的數值被覆蓋的情況:
Thread A | Thread B |
---|---|
取得counter = 100 | - |
counter加1 | 取得counter = 100 |
counter被指回成101 | counter加1 |
- | counter被指回成101,覆蓋目前的counter |
解決方法
ReentrantLock
這個類別產生的物件,可以鎖住其本身,只讓取得鎖的那個執行緒進入臨界區,也就是工程師要進行業務邏輯操作的部分。類別的建構子可以依參數決定實作公平鎖或非公平鎖。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平鎖依照執行緒的請求順序排序執行先後,適用於銀行櫃檯抽號機;非公平鎖則無限制,因此有可能發生插隊,但因為省去了排隊及排程成本,效能較高、吞吐量大(系統在某段時間內成功處理的請求數量),適用高併發任務。
由於只讓取得鎖的執行緒進入臨界區執行業務邏輯,所以可以避免兩個執行緒同時修改計數器。
public class ThreadTest {
private static int counter = 0;
// 非公平鎖
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100000];
for(int i = 0; i < 100000; i++){
threads[i] = new Thread(()->{
lock.lock();
try {
//臨界區
counter++;
} finally {
//釋放鎖
lock.unlock();
}
});
threads[i].start();
}
for(Thread thread:threads){
thread.join();
}
System.out.println(counter);
}
}
synchronized
synchronized 字符是針對某個物件上鎖,寫成 synchronized (Object obj)
,讓依據這個物件執行的業務邏輯一次只能被一個執行緒操作。
public class ThreadTest {
private static int counter = 0;
private final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100000];
for(int i = 0; i < 100000; i++){
threads[i] = new Thread(()->{
synchronized (obj){
counter++;
}
});
threads[i].start();
}
for(Thread thread:threads){
thread.join();
}
System.out.println(counter);
}
但要特別小心 synchronized 包裹的物件類型,像是以下寫法就沒有上鎖的意義。
public class ThreadTest {
private static Integer counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100000];
for(int i = 0; i < 100000; i++){
threads[i] = new Thread(()->{
synchronized (counter){
counter++;
}
});
threads[i].start();
}
for(Thread thread:threads){
thread.join();
}
System.out.println(counter);
}
雖然Integer是物件類別,但是每次計算counter++,其實都會另建立一個不同記憶體位置的物件,所以上鎖毫無意義,計算結果仍然不會等於100000。
補充:AtomicInteger
如果是操作整數型別的邏輯,可以使用原子級別的AtomicInteger,效能更高,但這就不是鎖機制的實作了。
public class ThreadTest {
static final AtomicInteger atomicCounter = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100000];
for(int i = 0; i < 100000; i++){
threads[i] = new Thread(()->{
atomicCounter.incrementAndGet();
});
threads[i].start();
}
for(Thread thread:threads){
thread.join();
}
System.out.println(atomicCounter);
}