Java 併發編程基礎 ① - 線程

原文地址: Java 併發編程基礎 ① - 線程
轉載請註明出處!

1、什麼是線程

進程是代碼在數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,線程則是進程的一個執行路徑,一個進程中至少有一個線程,進程中的多個線程共享進程的資源java

操做系統在分配資源時是把資源分配給進程的,可是CPU資源比較特殊,它是被分配到線程的,由於真正要佔用CPU運行的是線程,因此也說線程是CPU分配的基本單位編程

以Java 爲例,咱們啓動一個main函數時,實際上就是啓動了一個JVM 的進程main函數所在的線程就是這個進程的一個線程,也稱爲主線程。一個JVM進程中有多個線程,多個線程共享進程的堆和方法區資源,可是每一個線程有本身的程序計數器和棧區域tomcat

2、線程建立和運行

Java 線程建立有3種方式:多線程

  1. 繼承 Thread 類而且重寫 run 方法
  2. 實現 Runnable接口的 run 方法
  3. 使用FutureTask方式
具體代碼示例不詳述,太基礎,會感受在水。

說下 FutureTask 的方式,這種方式的本事也是實現了Runnable 接口的 run 方法,看它的繼承結構就能夠知道。併發

前兩種方式都沒辦法拿到任務的返回結果,可是 Futuretask 方式能夠。ide

3、線程通知與等待

3.1 wait() 方法

wait() 方法的效果就是該調用線程被阻塞掛起,直到發生如下幾種狀況纔會調起:函數

  • 其餘線程調用了該共享對象的 notify() 方法或者 notifyAll() 方法(繼續往下走)
  • 其餘線程調用了該線程的 interrupt() 方法,該線程會 InterruptedException 異常返回

若是調用 wait() 方法的線程沒有事先獲取該對象的監視器鎖,調用線程會拋出IllegalMonitorStateException 異常。當線程調用wait() 以後,就已經釋放了該對象的監視器鎖測試

那麼,一個線程如何才能獲取一個共享變量的監視器鎖?網站

  1. 執行synchronized 同步代碼塊,使用該共享變量做爲參數。this

    synchronized(共享變量) {
        // TODO
    }
  2. 調用該共享變量的同步方法(synchronized 修飾)

    synchronized void sum(int a, int b) {
        // TODO
    }

3.2 notify() / notifyAll()

一個線程調用共享對象的 notify() 方法後,會喚醒一個在該共享變量上調用 wait(...) 系列方法後被掛起的線程。

值得注意的是:

  • 一個共享變量上可能會有多個線程在等待,notify() 具體喚醒哪一個等待的線程是隨機的
  • 被喚醒的線程不能立刻從wait方法返回並繼續執行,它必須在獲取了共享對象的監視器鎖後才能夠返回,也就是喚醒它的線程釋放了共享變量上的監視器鎖後,被喚醒的線程也不必定會獲取到共享對象的監視器鎖,這是由於該線程還須要和其餘線程一塊兒競爭該鎖,只有該線程競爭到了共享變量的監視器鎖後才能夠繼續執行

notifyAll() 方法則會喚醒全部在該共享變量上因爲調用wait系列方法而被掛起的線程。

3.3 實例

比較經典的就是生產者和消費者的例子

public class NotifyWaitDemo {

    public static final int MAX_SIZE = 1024;
    // 共享變量
    public static Queue queue = new Queue();

    public static void main(String[] args) {
        // 生產者
        Thread producer = new Thread(() -> {
            synchronized (queue) {
                while (true) {
                    // 掛起當前線程(生產者線程)
                    // 而且,釋放經過queue的監視器鎖,讓消費者對象獲取到鎖,執行消費邏輯
                    if (queue.size() == MAX_SIZE) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 空閒則生成元素,而且通知消費線程
                    queue.add();
                    queue.notifyAll();
                }
            }
        });
        // 消費者
        Thread consumer = new Thread(() -> {
            synchronized (queue) {
                while (true) {
                    // 掛起當前線程(消費者線程)
                    // 而且,釋放經過queue的監視器鎖,讓生產者對象獲取到鎖,執行生產邏輯
                    if (queue.size() == 0) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 空閒則消費元素,而且通知生產線程
                    queue.take();
                    queue.notifyAll();
                }
            }
        });
        producer.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        consumer.start();
    }

    static class Queue {

        private int size = 0;

        public int size() {
            return this.size;
        }

        public void add() {
            // TODO
            size++;
            System.out.println("執行add 操做,current size: " +  size);
        }

        public void take() {
            // TODO
            size--;
            System.out.println("執行take 操做,current size: " +  size);
        }
    }
}

3.4 wait()/notify()/notifyAll() 爲何定義在 Object 類中?

因爲Thread類繼承了Object類,因此Thread也能夠調用者三個方法,等待和喚醒必須是同一個鎖。而鎖能夠是任意對象,因此能夠被任意對象調用的方法是定義在object類中。

4、join() - 等待線程執行終止

適用場景:須要等待某幾件事情完成後才能繼續往下執行,好比多個線程加載資源,須要等待多個線程所有加載完畢再彙總處理。

public static void main(String[] args){
    ...
    thread1.join();
    thread2.join();
    System.out.println("all child thread over!");
}

主線程首先會在調用thread1.join() 後被阻塞,等待thread1執行完畢後,調用thread2.join(),等待thread2 執行完畢(有可能),以此類推,最終會等全部子線程都結束後main函數纔會返回。若是其餘線程調用了被阻塞線程的 interrupt() 方法,被阻塞線程會拋出 InterruptedException 異常而返回。

4.1 實例

給出一個實例幫助理解。

public class JoinExample {

    private static final int TIMES = 100;

    private class JoinThread extends Thread {

        JoinThread(String name){
           super(name);
        }

        @Override
        public void run() {
            for (int i = 0; i < TIMES; i++) {
                System.out.println(getName() + " " + i);
            }
        }
    }

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

    private void test() {
       for (int i = 0; i < TIMES; i++) {
           if (i == 20) {
               Thread jt1 = new JoinThread("子線程1");
               Thread jt2 = new JoinThread("子線程2");
               jt1.start();
               jt2.start();
               // main 線程調用了jt線程的join()方法
               // main 線程必須等到 jt 執行完以後纔會向下執行
               try {
                   jt1.join();
                   jt2.join();
                   // join(long mills) - 等待時間內 被join的線程還沒執行,再也不等待
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

5、線程睡眠

sleep()會使線程暫時讓出指定時間的執行權,也就是在這期間不參與CPU的調度但不會釋放鎖

指定的睡眠時間到了後該函數會正常返回,線程就處於就緒狀態,而後參與CPU的調度,獲取到CPU資源後就能夠繼續運行了。

若是在睡眠期間其餘線程調用了該線程的 interrupt() 方法中斷了該線程,則該線程會在調用sleep方法的地方拋出 InterruptedException 異常而返回。

/**
 * 幫助理解 sleep 不會讓出監視器資源
 * 
 * 在線程A睡眠的這10s內那個獨佔鎖lock仍是線程A本身持有
 * 線程B會一直阻塞直到線程A醒來後執行unlock釋放鎖。
 */
public class ThreadSleepDemo {

    // 獨佔鎖
    private static final Lock LOCK = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            // 獲取獨佔鎖
            LOCK.lock();
            try {
                System.out.println("child thread A is in sleep");
                Thread.sleep(10000);
                System.out.println("child thread A is awake");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // release lock
                LOCK.unlock();
            }
        });

        Thread threadB = new Thread(() -> {
            // 獲取獨佔鎖
            LOCK.lock();
            try {
                System.out.println("child thread B is in sleep");
                Thread.sleep(10000);
                System.out.println("child thread B is awake");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // release lock
                LOCK.unlock();
            }
        });

        threadA.start();
        threadB.start();
    }
}

6、讓出CPU執行權 - yield()

線程調用yield 方法時,其實是暗示線程調度器當前線程請求讓出本身的CPU使用(告訴線程調度器能夠進行下一輪的線程調度),但線程調度器能夠無條件忽略這個暗示

咱們知道操做系統是爲每一個線程分配一個時間片來佔有CPU的,正常狀況下當一個線程把分配給本身的時間片使用完後,線程調度器纔會進行下一輪的線程調度,而當一個線程調用了Thread類的靜態方法 yield 時,是在告訴線程調度器本身佔有的時間片中尚未使用完的部分本身不想使用了,這暗示線程調度器如今就能夠進行下一輪的線程調度。

通常不多使用這個方法,在調試或者測試時這個方法或許能夠幫助復現因爲併發競爭條件致使的問題,其在設計併發控制時或許會有用途,在java.util.concurrent.locks包裏面的鎖時會看到該方法的使用

sleep與yield方法的區別在於:當線程調用sleep方法時調用線程會被阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程。而調用yield 方法時,線程只是讓出本身剩餘的時間片,並無被阻塞掛起,而是處於就緒狀態,線程調度器下一次調度時就有可能調度到當前線程執行。

7、線程中斷

不少人看到 interrupt() 方法,認爲「中斷」線程不就是讓線程中止嘛。實際上, interrupt() 方法實現的根本就不是這個效果, interrupt()方法更像是發出一個信號,這個信號會改變線程的一個標識位屬性(中斷標識),對於這個信號如何進行響應則是沒法肯定的(能夠有不一樣的處理邏輯)。不少時候調用 interrupt() 方法非但不是爲了中止線程,反而是爲了讓線程繼續運行下去

官方一點的表述:

Java中的線程中斷是一種線程間的協做模式, 經過設置線程的中斷標誌並不能直接終止該線程的執行,而是被中斷的線程根據中斷狀態自行處理

7.1 void interrupt()

設置線程的中斷標誌爲true並當即返回,但線程實際上並無被中斷而會繼續向下執行;若是線程由於調用了wait系列函數、join方法或者sleep方法而被阻塞掛起,其餘線程調用該線程的interrupt()方法會使該線程拋出InterruptedException異常而返回。

7.2 boolean isInterrupted()

檢測線程是否被中斷,是則返回true,不然返回false。

public boolean isInterrupted() {
        // 傳遞 false 說明不清除中斷標誌
        return isInterrupted(false);
    }

7.3 boolean interrupted()

檢測當前線程是否被中斷,返回值同上 isInterrupted() ,不一樣的是,若是發現當前線程被中斷,會清除中斷標誌;該方法是static方法,內部是獲取當前調用線程的中斷標誌而不是調用interrupted()方法的實例對象的中斷標誌

public static boolean interrupted() {
        // static 方法
        // true - 清除終端標誌
        // currentThread
        return currentThread().isInterrupted(true);
    }

8、線程上下文切換

咱們都知道,在多線程編程中,線程個數通常都大於CPU 個數,而每一個CPU同一時刻只能被一個線程使用。

爲了讓用戶感受多個線程是在同時執行的,CPU資源的分配採用了時間片輪轉的策略,也就是每一個線程分配一個時間片,線程在時間片內佔用CPU執行任務。當前線程使用完時間片後,就會處於就緒狀態而且讓出CPU讓其餘線程佔用,這就是線程的上下文切換

9、線程死鎖

死鎖是指兩個或兩個以上的線程在執行過程當中,因爭奪資源而形成的互相等待的現象,在無外力做用的狀況下,這些線程會一直相互等待而沒法繼續運行下去。

就如上圖,線程A持有資源2,同時想申請資源1,線程B持有資源1,同時想申請資源2,兩個線程相互等待就造成了死鎖狀態。

死鎖的產生有四個條件:

  • 互斥條件:指線程對已經獲取到的資源進行排他性使用,即該資源同時只由一個線程佔用。若是此時還有其餘線程請求獲取該資源,則請求者只能等待,直至佔有資源的線程釋放該資源。
  • 請求並持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新資源已被其餘線程佔有,因此當前線程會被阻塞,但阻塞的同時並不釋放本身已經獲取的資源。
  • 不可剝奪條件:指線程獲取到的資源在本身使用完以前不能被其餘線程搶佔,只有在本身使用完畢後才由本身釋放該資源。
  • 環路等待條件:指在發生死鎖時,必然存在一個線程—資源的環形鏈,即線程集合{T0, T1, T2, …, Tn}中的T0正在等待一個T1佔用的資源,T1正在等待T2佔用的資源,……Tn正在等待已被T0佔用的資源。

10、守護線程與用戶線程

Java 線程分爲兩類,

  • daemon 線程(即守護線程)
  • user 線程 (用戶線程)

在JVM 啓動時會調用 main 函數,main 函數所在的線程就是一個用戶線程,同時在JVM 內部也啓動了不少守護線程,好比 GC 線程。

守護線程和用戶線程的區別在於,守護線程不會影響JVM 的退出,當最後一個用戶線程結束時,JVM 會正常退出。

因此,若是你但願在主線程結束後JVM進程立刻結束,那麼在建立線程時能夠將其設置爲守護線程,若是你但願在主線程結束後子線程繼續工做,等子線程結束後再讓JVM進程結束,那麼就將子線程設置爲用戶線程

舉例:好比在Tomcat 的NIO 實現NioEndpoint 類中,會開啓一組接受線程來接受用戶的鏈接請求,以及一組處理線程負責具體處理用戶請求。

/**
     * Start the NIO endpoint, creating acceptor, poller threads.
     */
    @Override
    public void startInternal() throws Exception {

        if (!running) {
            // ... 省略

            // Start poller threads 處理線程
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; i<pollers.length; i++) {
                pollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true); // 
                pollerThread.start();
            }
            // 啓動接收線程
            startAcceptorThreads();
        }
    }

    protected final void startAcceptorThreads() {
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];

        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon()); // 默認值是true 
            t.start();
        }
    }

在如上代碼中,在默認狀況下,接受線程和處理線程都是守護線程,這意味着當tomcat收到shutdown命令後而且沒有其餘用戶線程存在的狀況下tomcat進程會立刻消亡,而不會等待處理線程處理完當前的請求。

小結

本篇講了有關Java 線程的一些基礎知識。下一篇,我計劃是寫一下 Java 線程的生命週期、線程的各個狀態和以及各個狀態的流轉。由於關於這部分我感受國內網站上大部分的文章都沒有講清楚,我會嘗試用圖片、文字和具體的代碼結合的方式寫一篇。有興趣的小夥伴能夠關注一下。

若是本文有幫助到你,但願能點個贊,這是對個人最大動力🤝🤝🤗🤗。

參考

  • 《Java 併發編程之美》
相關文章
相關標籤/搜索