Java 多線程使用

1. Thread

1.1 創建線程

繼承Thread,重寫run方法

@Test
public void helloThreadTest() {
    Thread thread = new HelloThread();
    thread.start();
}

public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("HelloThread");
    }
}

實現Runnable接口

@Test
public void helloRunnableTest() {
    Thread thread = new Thread(new HelloRunnable());
    thread.start();
}

public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("HelloRunnable");
    }
}

Lambda語法簡寫

@Test
public void lambdaTest() {
    Thread thread = new Thread(() -> {
        System.out.println("lambdaTest");
    });
    thread.start();
}

1.2 join

join()方法使主線程會等待線程完成後繼續執行。

沒有join()方法,只能保證主線程先打印main start,然後main end;線程先打印thread start,然後thread end
main end具體打印什麼位置未知,由操作系統調度

但是添加join,就可以保證main end在線程執行結束後打印

@Test
public void mainThreadTest() throws InterruptedException {
    System.out.println("main start");

    Thread thread = new Thread(() -> {
        System.out.println("thread start");
        System.out.println("thread end");
    });
    thread.start();
    //thread.join();

    System.out.println("main end");
}

1.3 interrupt

Thread.sleep(1);需要使用main方法,單元測試會不可用。

線程t中斷,主線程等待線程t中斷後,繼續執行

public static void main(String[] args) throws InterruptedException {
    Thread t = new InterruptThread();
    t.start();
    // 暫停1毫秒
    Thread.sleep(1);
    // 中斷t線程
    t.interrupt();
    // 等待t線程結束
    t.join();
    System.out.println("end");
}

public class InterruptThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

線程t啓動後,線程t內部會啓動線程2,並等待線程2結束
當線程t中斷後,主線程等待線程t結束
如果不把線程2中斷,則線程2會繼續執行

public static void main(String[] args) throws InterruptedException {
    Thread t = new InterruptThread1();
    t.start();
    Thread.sleep(1000);
    // 中斷t線程
    t.interrupt();
    // 等待t線程結束
    t.join();
    System.out.println("end");
}

public class InterruptThread1 extends Thread {
    public void run() {
        Thread thread2 = new InterruptThread2();
        // 啓動hello線程
        thread2.start();
        try {
            // 等待hello線程結束,如果中斷會拋異常
            thread2.join();
        } catch (InterruptedException e) {
            System.out.println("interrupted!" + new Date());
        }

        // 不將thread2中斷,還會繼續
        thread2.interrupt();
    }
}

public class InterruptThread2 extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " InterruptThread2!" + new Date());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

中斷也可通過設置標誌位來實現

通過關鍵詞volatile將其標記爲線程間共享的變量

public static void main(String[] args) throws InterruptedException {
    InterruptThread3 thread = new InterruptThread3();
    thread.start();
    Thread.sleep(1);
    thread.running = false;
}

public class InterruptThread3 extends Thread {
    public volatile boolean running = true;

    @Override
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

2. wait notify

wait和notify用於多線程協調運行。

在synchronized內部可以調用wait()使線程進入等待狀態,調用notify()或notifyAll()喚醒其他等待線程;
必須在已獲得的鎖對象上調用wait()方法,調用notify()或notifyAll()方法;
已喚醒的線程還需要重新獲得鎖後才能繼續執行。

啓動兩個線程,一個輸出 1,3,5,7…99,另一個輸出 2,4,6,8…100,最後按序輸出 1,2,3,4,5…100

private static final Object lock = new Object();
private volatile int i = 1;

@Test
public void test1() {
    Thread thread1 = new Thread(() -> {
        while (i <= 100) {
            synchronized (lock) {
                // 偶數則等待
                if (i % 2 == 0) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 奇數打印並判斷
                } else {
                    System.out.print(i + " ");
                    i++;
                    lock.notifyAll();
                }
            }
        }
    });

    Thread thread2 = new Thread(() -> {
        while (i <= 100) {
            synchronized (lock) {
                // 奇數則等待
                if (i % 2 != 0) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 偶數打印並判斷
                } else {
                    System.out.print(i + " ");
                    i++;
                    lock.notifyAll();
                }
            }
        }
    });

    thread1.start();
    thread2.start();
}

也可以這樣:

@Test
public void test2() {
    Thread t1 = new Thread(() -> {
        while(i <= 100) {
            synchronized (lock) {
                if (i % 2 == 0) {
                    System.out.print(i++);
                    System.out.print(" ");
                } else {
                    // 奇數,就wait,釋放鎖
                    try{
                        lock.wait();
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    });

    Thread t2 = new Thread(() -> {
        while(i <= 100) {
            synchronized (lock) {
                if (i % 2 != 0) {
                    System.out.print(i++);
                    System.out.print(" ");
                } else {
                    // 偶數,就notify,會喚醒上方鎖等待的線程
                    lock.notifyAll();
                }
            }
        }
    });

    t1.start();
    t2.start();
}

兩個線程,一個線程輸出奇數,一個線程輸出偶數,保證輸出順序是:2、1、4、3、……、50、49、52、51、54、53、……、100、99

private static final Object lock = new Object();
private volatile int count = 2;

@Test
public void test3() {
    Thread t1 = new Thread(() -> {
        while(count <= 100) {
            synchronized (lock){
                // 偶數則打印,並減1
                if (count % 2 == 0) {
                    System.out.print(count + " ");
                    count--;

                    // 需要notify,否則兩個線程執行一次後都進行等待
                    lock.notifyAll();

                    if (count <= 100) {
                        try{
                            lock.wait();
                        }catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    });

    Thread t2 = new Thread(() -> {
        while(count <= 100) {
            synchronized (lock){
                // 奇數則打印,並加3
                if (count % 2 != 0) {
                    System.out.print(count + " ");
                    count += 3;

                    // 需要notify,否則兩個線程執行一次後都進行等待
                    lock.notifyAll();

                    if (count <= 100) {
                        try{
                            lock.wait();
                        }catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    });

    t1.start();
    t2.start();
}

3. ReentrantLock Condition

ReentrantLock可以替代synchronized進行同步,並且獲取鎖更安全;必須先獲取到鎖,再進入try {…}代碼塊,最後使用finally保證釋放鎖;可以使用tryLock()嘗試獲取鎖。

用ReentrantLock,可以使用Condition對象來實現,等同於synchronized,wait和notify的功能

啓動兩個線程,一個輸出 1,3,5,7…99,另一個輸出 2,4,6,8…100,最後按序輸出 1,2,3,4,5…100

使用ReentrantLock和Condition:

private static final Lock reentrantLock = new ReentrantLock();
private static final Condition condition = reentrantLock.newCondition();
private volatile int i = 1;

@Test
public void reentrantLockTest() {
    Thread thread1 = new Thread(() -> {
        while (i <= 100) {
            reentrantLock.lock();
            try {
                // 偶數則等待
                if (i % 2 == 0) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 奇數打印並判斷
                } else {
                    System.out.print(i + " ");
                    i++;
                    condition.signalAll();
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    });

    Thread thread2 = new Thread(() -> {
        while (i <= 100) {
            reentrantLock.lock();
            try {
                // 齊數則等待
                if (i % 2 != 0) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 偶數打印並判斷
                } else {
                    System.out.print(i + " ");
                    i++;
                    condition.signalAll();
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    });

    thread1.start();
    thread2.start();
}

4. ReadWriteLock

使用ReadWriteLock可以保證:只允許一個線程寫入(其他線程既不能寫入也不能讀取);沒有寫入時,多個線程允許同時讀(提高性能)。

如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。

private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static final Lock readLock = readWriteLock.readLock();
private static final Lock writeLock = readWriteLock.writeLock();

/**
 * 獲取寫鎖
 * @param i
 */
private void setI (Integer i) {
    writeLock.lock();
    try {
        this.i = i;
    } finally {
        writeLock.unlock();
    }
}

/**
 * 獲取讀鎖
 * @return
 */
private Integer getI () {
    readLock.lock();
    try {
        return i;
    } finally {
        readLock.unlock();
    }
}

5. StampedLock

StampedLock和ReadWriteLock相比,改進之處在於:讀的過程中也允許獲取寫鎖後寫入!這樣一來,我們讀的數據就可能不一致,所以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。

樂觀鎖的意思就是樂觀地估計讀的過程中大概率不會有寫入,因此被稱爲樂觀鎖。反過來,悲觀鎖則是讀的過程中拒絕有寫入,也就是寫入必須等待。顯然樂觀鎖的併發效率更高,但一旦有小概率的寫入導致讀取的數據不一致,需要能檢測出來,再讀一遍就行。

StampedLock提供了樂觀讀鎖,可取代ReadWriteLock以進一步提升併發性能;StampedLock是不可重入鎖。

private static final StampedLock stampedLock = new StampedLock();
private Integer x;
private Integer y;

private void set(Integer x , Integer y) {
    long stamp = stampedLock.writeLock();
    try {
        x += x;
        y += y;
    } finally {
        stampedLock.unlockWrite(stamp);
    }
}

private Integer get() {
    // 先獲取樂觀讀鎖
    long stamp = stampedLock.tryOptimisticRead();
    Integer newX = x;
    Integer newY = y;

    // 判斷獲取樂觀讀鎖後是否有獲取其他寫鎖
    if (!stampedLock.validate(stamp)) {
        // 有獲取其他寫鎖,則獲取悲觀讀鎖。寫鎖需要等悲觀讀鎖釋放後,才能寫
        stamp = stampedLock.readLock();
        try {
            // 重新賦值
            newX = x;
            newY = y;
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }

    return newX * newY;
}

4. ExecutorService

ExecutorService接口表示線程池,Java標準庫提供的幾個常用實現類有:
FixedThreadPool:線程數固定的線程池,任務超過則等待;
CachedThreadPool:線程數根據任務動態調整的線程池;
SingleThreadExecutor:僅單線程執行的線程池,定期反覆執行。

@Test
public void executorServiceTest() {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    for (int i = 1; i <= 3; i++) {
        executorService.submit(new Task(i + ""));
    }

    executorService.shutdown();
}

public class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(new Date() + " start " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(new Date() + " end " + name);
    }
}

5. Future

一個Future接口表示一個未來可能會返回的結果,可以用來獲取線程返回值。

當我們提交一個Callable任務後,我們會同時獲得一個Future對象,然後,我們在主線程某個時刻調用Future對象的get()方法,就可以獲得異步執行的結果。

在調用get()時,如果異步任務已經完成,我們就直接獲得結果。如果異步任務還沒有完成,那麼get()會阻塞,直到任務完成後才返回結果。

@Test
public void callableTest() {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    Callable<String> callable = new TaskCallable();
    Future<String> future = executorService.submit(callable);
    String returnStr = "";
    try {
        returnStr = future.get();
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(returnStr);
}

public class TaskCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "TaskCallable";
    }
}

6. CompletableFuture

CompletableFuture針對Future做了改進,可以傳入回調對象,當異步任務完成或者發生異常時,自動調用回調對象的回調方法。

CompletableFuture可多個串行或並行執行

@Test
public void CompletableFutureTest() {
    while (true) {
        CompletableFuture<Double> completableFuture = CompletableFuture.supplyAsync(ThreadTest::fetchPrice);
        // 成功
        completableFuture.thenAccept((result) -> {
            System.out.println(result);
        });
        // 失敗
        completableFuture.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
    }
}

static Double fetchPrice() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    if (Math.random() < 0.3) {
        throw new RuntimeException("fetch price failed!");
    }
    return 5 + Math.random() * 20;
}

參考:
Java 多線程
編程題:兩個線程,一個線程輸出奇數,一個線程輸出偶數,保證輸出順序是:2、1、4、3、……、50、49、52、51、54、53、……、100、99
你真的懂wait、notify和notifyAll嗎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章