JAVA併發不會?怎麼辦,看這裏(一)

思惟導圖

本次我將經過兩篇文章進行分享。第二篇傳送門JAVA併發不會?怎麼辦,看這裏(二)html

使用線程

有三種使用線程的方法:java

  • 實現 Runnable 接口;
  • 實現 Callable 接口;
  • 繼承 Thread 類。
  • 實現 Runnable 和 Callable

接口的類只能當作一個能夠在線程中運行的任務,不是真正意義上的線程,所以最後還須要經過 Thread 來調用。能夠理解爲任務是經過線程驅動從而執行的。git

實現 Runnable 接口

須要實現接口中的 run() 方法。程序員

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // ...
    }
}

複製代碼

使用 Runnable 實例再建立一個 Thread 實例,而後調用 Thread 實例的 start() 方法來啓動線程。github

public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

複製代碼

實現 Callable 接口

與 Runnable 相比,Callable 能夠有返回值,返回值經過 FutureTask 進行封裝。bash

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}

複製代碼
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

複製代碼

繼承 Thread 類

一樣也是須要實現 run() 方法,由於 Thread 類也實現了 Runable 接口。併發

當調用 start() 方法啓動一個線程時,虛擬機會將該線程放入就緒隊列中等待被調度,當一個線程被調度時會執行該線程的 run() 方法。oracle

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}

複製代碼
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

複製代碼

實現接口 VS 繼承 Thread

實現接口會更好一些,由於:框架

  • Java 不支持多重繼承,所以繼承了 Thread 類就沒法繼承其它類,可是能夠實現多個接口;
  • 類可能只要求可執行就行,繼承整個 Thread 類開銷過大。

基礎線程機制

Executor

Executor 管理多個異步任務的執行,而無需程序員顯式地管理線程的生命週期。這裏的異步是指多個任務的執行互不干擾,不須要進行同步操做。異步

主要有三種 Executor:

  • CachedThreadPool:一個任務建立一個線程;
  • FixedThreadPool:全部任務只能使用固定大小的線程;
  • SingleThreadExecutor:至關於大小爲 1 的 FixedThreadPool。
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}

複製代碼

Daemon

守護線程是程序運行時在後臺提供服務的線程,不屬於程序中不可或缺的部分。

當全部非守護線程結束時,程序也就終止,同時會殺死全部守護線程。

main() 屬於非守護線程。

在線程啓動以前使用 setDaemon() 方法能夠將一個線程設置爲守護線程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}

複製代碼

sleep()

Thread.sleep(millisec) 方法會休眠當前正在執行的線程,millisec 單位爲毫秒。

sleep() 可能會拋出 InterruptedException,由於異常不能跨線程傳播回 main() 中,所以必須在本地進行處理。線程中拋出的其它異常也一樣須要在本地進行處理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

複製代碼

yield()

對靜態方法 Thread.yield() 的調用聲明瞭當前線程已經完成了生命週期中最重要的部分,能夠切換給其它線程來執行。該方法只是對線程調度器的一個建議,並且也只是建議具備相同優先級的其它線程能夠運行。

public void run() {
    Thread.yield();
}

``
`
## 中斷
一個線程執行完畢以後會自動結束,若是在運行過程當中發生異常也會提早結束。
### InterruptedException
經過調用一個線程的 interrupt() 來中斷該線程,若是該線程處於阻塞、限期等待或者無限期等待狀態,那麼就會拋出 InterruptedException,從而提早結束該線程。可是不能中斷 I/O 阻塞和 synchronized 鎖阻塞。

對於如下代碼,在 main() 中啓動一個線程以後再中斷它,因爲線程中調用了 Thread.sleep() 方法,所以會拋出一個 InterruptedException,從而提早結束線程,不執行以後的語句。

```java
public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

複製代碼
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}

複製代碼
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

複製代碼

interrupted()

若是一個線程的 run() 方法執行一個無限循環,而且沒有執行 sleep() 等會拋出 InterruptedException 的操做,那麼調用線程的 interrupt() 方法就沒法使線程提早結束。

可是調用 interrupt() 方法會設置線程的中斷標記,此時調用 interrupted() 方法會返回 true。所以能夠在循環體中使用 interrupted() 方法來判斷線程是否處於中斷狀態,從而提早結束線程。

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
}

複製代碼
public static void main(String[] args) throws InterruptedException {
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}

複製代碼

Executor 的中斷操做

調用 Executor 的 shutdown() 方法會等待線程都執行完畢以後再關閉,可是若是調用的是 shutdownNow() 方法,則至關於調用每一個線程的 interrupt() 方法。

如下使用 Lambda 建立線程,至關於建立了一個匿名內部線程。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    executorService.shutdownNow();
    System.out.println("Main run");
}

複製代碼
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
    at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

複製代碼

若是隻想中斷 Executor 中的一個線程,能夠經過使用 submit() 方法來提交一個線程,它會返回一個 Future<?> 對象,經過調用該對象的 cancel(true) 方法就能夠中斷線程。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);

複製代碼

互斥同步

Java 提供了兩種鎖機制來控制多個線程對共享資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另外一個是 JDK 實現的 ReentrantLock。

synchronized

同步一個代碼塊

public void func() {
    synchronized (this) {
        // ...
    }
}

複製代碼

它只做用於同一個對象,若是調用兩個對象上的同步代碼塊,就不會進行同步。

對於如下代碼,使用 ExecutorService 執行了兩個線程,因爲調用的是同一個對象的同步代碼塊,所以這兩個線程會進行同步,當一個線程進入同步語句塊時,另外一個線程就必須等待。

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}

複製代碼
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}

複製代碼
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

複製代碼

對於如下代碼,兩個線程調用了不一樣對象的同步代碼塊,所以這兩個線程就不須要同步。從輸出結果能夠看出,兩個線程交叉執行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}

複製代碼
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

複製代碼

同步一個方法

public synchronized void func () {
    // ...
}

複製代碼

它和同步代碼塊同樣,做用於同一個對象。

同步一個類

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

複製代碼

做用於整個類,也就是說兩個線程調用同一個類的不一樣對象上的這種同步語句,也會進行同步。

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}

複製代碼
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}

複製代碼
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

複製代碼

同步一個靜態方法

public synchronized static void fun() {
    // ...
}

複製代碼

做用於整個類。

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖。

public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。
        }
    }
}

複製代碼
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}

複製代碼
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

複製代碼

比較

鎖的實現

synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。

性能

新版本 Java 對 synchronized 進行了不少優化,例如自旋鎖等,synchronized 與 ReentrantLock 大體相同。

等待可中斷

當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情。

ReentrantLock 可中斷,而 synchronized 不行。

公平鎖

公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。

synchronized 中的鎖是非公平的,ReentrantLock 默認狀況下也是非公平的,可是也能夠是公平的

鎖綁定多個條件

一個 ReentrantLock 能夠同時綁定多個 Condition 對象。

使用選擇

除非須要使用 ReentrantLock 的高級功能,不然優先使用 synchronized。這是由於 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是全部的 JDK 版本都支持。而且使用 synchronized 不用擔憂沒有釋放鎖而致使死鎖問題,由於 JVM 會確保鎖的釋放。

線程之間的協做

當多個線程能夠一塊兒工做去解決某個問題時,若是某些部分必須在其它部分以前完成,那麼就須要對線程進行協調。

join()

在線程中調用另外一個線程的 join() 方法,會將當前線程掛起,而不是忙等待,直到目標線程結束。

對於如下代碼,雖然 b 線程先啓動,可是由於在 b 線程中調用了 a 線程的 join() 方法,b 線程會等待 a 線程結束才繼續執行,所以最後可以保證 a 線程的輸出先於 b 線程的輸出。

public class JoinExample {

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

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}

複製代碼
public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}

複製代碼
A
B

複製代碼

wait() notify() notifyAll()

調用 wait() 使得線程等待某個條件知足,線程在等待時會被掛起,當其餘線程的運行使得這個條件知足時,其它線程會調用 notify() 或者 notifyAll() 來喚醒掛起的線程。

它們都屬於 Object 的一部分,而不屬於 Thread。

只能用在同步方法或者同步控制塊中使用,不然會在運行時拋出 IllegalMonitorStateException。

使用 wait() 掛起期間,線程會釋放鎖。這是由於,若是沒有釋放鎖,那麼其它線程就沒法進入對象的同步方法或者同步控制塊中,那麼就沒法執行 notify() 或者 notifyAll() 來喚醒掛起的線程,形成死鎖。

public class WaitNotifyExample {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}

複製代碼
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}

複製代碼
before
after

複製代碼

wait() 和 sleep() 的區別

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的靜態方法;
  • wait() 會釋放鎖,sleep() 不會。

await() signal() signalAll()

java.util.concurrent 類庫中提供了 Condition 類來實現線程之間的協調,能夠在 Condition 上調用 await() 方法使線程等待,其它線程調用 signal() 或 signalAll() 方法喚醒等待的線程。

相比於 wait() 這種等待方式,await() 能夠指定等待的條件,所以更加靈活。

使用 Lock 來獲取一個 Condition 對象。

public class AwaitSignalExample {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

複製代碼
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}

複製代碼
before
after

複製代碼

線程狀態

一個線程只能處於一種狀態,而且這裏的線程狀態特指 Java 虛擬機的線程狀態,不能反映線程在特定操做系統下的狀態。

新建(NEW)

建立後還沒有啓動。

可運行(RUNABLE)

正在 Java 虛擬機中運行。可是在操做系統層面,它可能處於運行狀態,也可能等待資源調度(例如處理器資源),資源調度完成就進入運行狀態。因此該狀態的可運行是指能夠被運行,具體有沒有運行要看底層操做系統的資源調度。

阻塞(BLOCKED)

請求獲取 monitor lock 從而進入 synchronized 函數或者代碼塊,可是其它線程已經佔用了該 monitor lock,因此出於阻塞狀態。要結束該狀態進入從而 RUNABLE 須要其餘線程釋放 monitor lock。

無限期等待(WAITING)

等待其它線程顯式地喚醒。

阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取 monitor lock。而等待是主動的,經過調用 Object.wait() 等方法進入。

限期等待(TIMED_WAITING)

無需等待其它線程顯式地喚醒,在必定時間以後會被系統自動喚醒。

調用 Thread.sleep() 方法使線程進入限期等待狀態時,經常用「使一個線程睡眠」進行描述。調用 Object.wait() 方法使線程進入限期等待或者無限期等待時,經常用「掛起一個線程」進行描述。睡眠和掛起是用來描述行爲,而阻塞和等待用來描述狀態。

死亡(TERMINATED)

能夠是線程結束任務以後本身結束,或者產生了異常而結束。

本文參考聊聊併發(八)——Fork/Join 框架介紹線程通訊Threads and LocksThreads and LocksCS-NotesJava內存模型 之三個特性輕量級鎖thread state java

第二篇傳送門JAVA併發不會?怎麼辦,看這裏(二)

相關文章
相關標籤/搜索