Java 自學筆記 06 - Multi Thread

Java Thread

There are two ways to new a thread in Java:

Java Thread

Implemented from Runnable Interface

Runnable 是一個 interface,run() 裡面放的就是 thread 要執行的內容,一定要 override 且一定要設為 public

1
2
3
4
5
6
7
8
9
class TestRunnable implements Runnable{
@Override
public void run(){
// ...
}
}

// to new
Thread thread = new Thread(new TestRunnable());

Extended from Thread Class

Thread 是 Runnable 的實現,用法一樣是把要執行的程式放進 run() 裡面,概念一樣是 override + public

1
2
3
4
5
6
7
8
9
class TestThread extends Thread{
@Override
public void run(){
// ...
}
}

// to new
Thread thread = new TestThread()

我們也可以直接用 lambda 來 new thread。

1
2
3
Thread thread = new Thread(()->{
// ...
});

Start and Join

我們可以用 start() 來開始執行 thread,並用 join() 來等待 thread 完成並回歸 main thread。

1
2
3
4
5
6
7
8
9
10
11
12
Thread thread = new Thread(()->{
System.out.println("Run in thread");
});

System.out.println("Run in main");
thread.start();
try{
thread.join();
}catch(InterruptedException e){ // 一定要處理 Exception
// ...
}
System.out.println("Back to main");

執行結果如下:

1
2
3
Run in main
Run in thread
Back to main

Shared Resources

同時有多個 thread 對同一個資源進行操作時,可能會出現 race condition,導致結果不如預期,舉例來說:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SyncThread extends Thread{
static int sum = 0;

@Override
public void run(){
for (int i=0; i<10000; i++){
addOne();
}
}
void addOne(){
sum += 1;
}
}

public class SharedResources {
public static void main(String[] args){
Thread thread1 = new SyncThread();
Thread thread2 = new SyncThread();
thread1.start();
thread2.start();
try{
thread1.join();
thread2.join();
}catch(InterruptedException e){
// ...
}
System.out.println(SyncThread.sum);
}
}

每一個 thread 各別對 sum 進行 addOne() 10000 次,理論上最後的結果應該是sum = 20000,但最後結果卻小於 20000 (每次跑會不太一樣),原因大致上是這樣:

race_condition

Synchronized

我們可以使用 synchronized 關鍵字來實現同步,確保在同一時間只有一個 thread 在執行指定的代碼,使用方法有:

1
2
3
4
5
6
7
8
9
10
11
// 1. 修飾方法
synchronized void addOne(){
sum += 1;
}

// 2. 修飾代碼塊
void addOne(){
synchronized (this){
sum += 1;
}
}

不過 synchronized 是比較古老的用法了,在高併發的情況下性能可能會不太好,所以可以考慮用後面的方法。

Atomic

Atomic 表示不可被切分的動作,以我們的例子來說就是,我們可以將 sum 指定為 AtomicInteger,確保對 sum 的操作不會被中斷。

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.concurrent.atomic.AtomicInteger;

class SyncThread extends Thread {
static AtomicInteger sum = new AtomicInteger(0);

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
sum.addAndGet(1);
}
}
}

Concurrent Lock

java.util.concurrent 是 Java 中用來取代 synchronized 的一個 package,我們可以使用裡面的 Lock 機制,透過 Lock 的取得以及釋放來控制 thread 的動作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SyncThread extends Thread {
static int sum = 0;
private static final Lock lock = new ReentrantLock();

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
addOne();
}
}

void addOne() {
lock.lock();
try {
sum += 1;
} finally {
lock.unlock();
}
}
}

Concurrent Collection

前兩篇在介紹 Collections 的時候有稍微提到,Vector、Hashtable 等容器透過 synchronized 來實現同步,在高併發時效率不太好,因此 Java 後來又提供了 ConcurrentHashMapConcurrentLinkedQueue 等基於 concurrent 實現的容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.ConcurrentHashMap;

class SyncThread extends Thread {
static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(){
{
put("sum", 0);
}
};

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
map.compute("sum", (key, value) -> value + 1);
}
}
}

參考資料

Java多執行緒的基本知識