Java多線程基礎學習

寫在前面的話:

這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小時對其中每一行的代碼都有認真的練習,對其中的一些小錯誤進行調整,并且重新排版,希望通過本篇博客可以讓自己對 Java 多線程有更好的理解,同時也希望能夠幫助正在學習多線程的你。

此文只能說是 Java 多線程的一個入門,其實Java里頭線程完全可以寫一本書了,但是如果最基本的你都沒掌握好,又怎么能更上一個臺階呢?如果你覺得此文很簡單,那推薦你看看Java并發包的的線程池(Java 并發編程與技術內幕:線程池深入理解),或者看這個專欄:Java 并發編程與技術內幕。你將會對 Java 里頭的高并發場景下的線程有更加深刻的理解

本文主要講了 Java 中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。在這之前,首先讓我們來了解下在操作系統中進程和線程的區別:

進程:每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。(進程是資源分配的最小單位)

線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。(線程是 cpu 調度的最小單位)

  • 線程和進程一樣分為五個階段:創建、就緒、運行、阻塞、終止。
  • 多進程是指操作系統能同時運行多個任務(程序)。
  • 多線程是指在同一程序中有多個順序流在執行。

在 Java 中要想實現多線程,有兩種手段:

  • 一種是繼承 Thread 類;
  • 一種是實現 Runnable 接口.

其實準確來講,應該有三種,還有一種是實現 Callable 接口,并與 Future、線程池結合使用,此文不講這個,有興趣看這里 Java 并發編程與技術內幕:Callable、Future、FutureTask、CompletionService

一、擴展java.lang.Thread類

這里繼承 Thread 類的方法是比較常用的一種,如果說你只是想重新開啟一條線程。沒有什么其它特殊的要求,那么可以使用 Thread ,(筆者推薦使用 Runnable ,后頭會說明為什么)。下面來看一個簡單的實例:

/**
 * Created by Sean on 2017/5/9.
 */
class Thread1 extends Thread {
    private String threadName; // 用于標示不同的線程

    public Thread1(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + "運行,此時的 i = " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {

    public static void main(String[] args) {
        System.out.println("Hello World!");
        Thread1 thread1 = new Thread1("我是A線程");
        Thread1 thread2 = new Thread1("我是B線程");
        thread1.start();
        thread2.start();
    }
}

運行結果:

運行結果

說明:

程序啟動運行 main 時候, java 虛擬機啟動一個進程,主線程 main 在
main() 調用時候被創建。隨著調用 Thread1 的兩個對象的 start 方法,另外兩個線程也啟動了,這樣,整個應用就在多線程下運行。

注意:

start() 方法的調用后并不是立即執行多線程代碼,而是使得該線程變為可運行態(Runnable),什么時候運行是由操作系統決定的。

從程序運行的結果可以發現,多線程程序是亂序執行。因此,只有亂序執行的代碼才有必要設計為多線程。

Thread.sleep() 方法調用目的是不讓當前線程獨自霸占該進程所獲取的
CPU 資源,以留出一定時間給其他線程執行的機會。

實際上所有的多線程代碼執行順序都是不確定的,每次執行的結果都是隨機的。

此外 start() 方法重復調用的話,會出現java.lang.IllegalThreadStateException異常。

比如把 Main 類代碼改成下面:

public class Main {

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1("我是A線程");
        thread1.start();
        thread1.start();
    }
}

結果如下:

重復調用 start() 異常

二、實現 java.lang.Runnable 接口

采用 Runnable 也是非常常見的一種,我們只需要重寫 run() 即可。下面也來看個實例。

使用繼承 Thread 實現共享的錯誤示范

/**
 * Created by Sean on 2017/5/9.
 */


class Thread2 implements Runnable {

    private String threadName;

    public Thread2(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + "運行,此時的 i = " + i);
            try {
                Thread.sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main2 {
    public static void main(String[] args) {
        new Thread(new Thread2("我是在A線程中")).start();
        new Thread(new Thread2("我是在B線程中")).start();
    }
}

結果:

運行結果

說明:

Thread2 類是通過實現 Runnable 接口,使該類有了多線程類的特征, run() 方法是多線程程序的一個約定,所有的多線程代碼都在 run() 方法里面, 事實上, Thread 類也是實現了 Runnable 接口的類。

在啟動實現了 Runnable 接口的類的多線程的時候,需要先通過 Thread 類的構造方法 Thread(Runanable target) 構造出 Thread 對象,然后調用 Thread 對象的 start() 方法來開啟線程,運行 run() 方法里面的多線程代碼(這個run() 方法不需要開發者手動調用,會在操作系統分給該線程時間片的時候自動運行

實際上所有的多線程代碼都是通過運行 Thread 的 start() 方法來運行的。因此,不管是擴展 Thread 類還是實現 Runnable 接口來實現多線程,最終還是通過 Thread 的對象的 API 來控制線程的,熟悉 Thread 類的 API 是進行多線程編程的基礎。

三、Thread和Runnable的區別

如果一個類繼承 Thread,則不適合資源共享。但是如果實現了 Runable 接口的話,則很容易的實現資源共享。

上面這句話是原博客里面給出的,我認為是有瑕疵的。

以賣票程序為例,下面來說明為什么這樣說:

使用繼承 Thread 的方式共享的錯誤示例

/**
 * Created by Sean on 2017/5/9.
 */
class Thread3 extends Thread {
    private String threadName;
    private int ticket = 5;

    public Thread3(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            synchronized (this) {
                if (ticket > 0){
                    System.out.println(threadName + "運行,此時的 i = " + i+" 剩余票數" + this.ticket--);
                }
            }
            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main3 {
    public static void main(String[] args){
        Thread3 thread1 = new Thread3("我是在A線程中");
        Thread3 thread2 = new Thread3("我是在B線程中");
        Thread3 thread3 = new Thread3("我是在C線程中");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

結果:

運行結果

從上面的結果可以看到,開啟了三個線程,每個線程都賣了5張票,這明顯是不合理的,接下來看看用 Runnable 來實現共享 5 張票的例子

使用實現 Runnable 實現共享票數

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

/**
 * Created by Sean on 2017/5/9.
 */
class Thread4 implements Runnable {

    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(currentThread().getName() + "運行,此時的 i = " + i + " 剩余票數" + this.ticket--);
                }
            }
            try {
                sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main4 {
    public static void main(String[] args) {
        Thread4 thread1 = new Thread4();
        new Thread(thread1, "我是在A線程中").start();
        new Thread(thread1, "我是在B線程中").start();
        new Thread(thread1, "我是在C線程中").start();
    }
}

運行結果:

運行結果

可以看到,我們用實現 Runnable 接口的方式實現了資源的共享。

那么我們使用繼承 Thread 的方式就真的沒法實現資源共享嗎?

答案是 NO!

往下看。

使用繼承 Thread 的方式共享的正確示例

我們先看下面的代碼:

/**
 * Created by Sean on 2017/5/9.
 */
class Thread5 extends Thread {

    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(currentThread().getName() + "運行,此時的 i = " + i + " 剩余票數" + this.ticket--);
                }
            }
            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main5 {
    public static void main(String[] args) {
        Thread5 thread1 = new Thread5();
        new Thread(thread1, "我是在A線程中").start();
        new Thread(thread1, "我是在B線程中").start();
        new Thread(thread1, "我是在C線程中").start();
    }
}

運行結果:

運行結果

可以看到,雖然我們使用了繼承 Thread 的方式來實現線程類,最后我們也同樣實現了多線程中資源的共享。

從而可以判斷,原博客的話是有一定錯誤的。

下面看一下總結:

實現 Runnable 接口比繼承 Thread 類所具有的優勢:

  1. 可以避免java中的單繼承的限制
  2. 線程池只能放入實現 Runable 或 callable 類線程,不能直接放入繼承
    Thread 的類

兩者都有的:

  1. 適合多個相同的程序代碼的線程去處理同一個資源
  2. 增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立

提醒一下大家: main() 方法其實也是一個線程,在 java 中所有的線程都是同時啟動的,至于什么時候啟動,哪個線程先執行,完全是看哪個線程先從 cpu 哪里獲取時間片資源。

此外:在 java 中,每次程序運行至少啟動兩個線程, 一個是 main 線程, 一個是垃圾回收線程。因為每當使用 java 命令執行一個類的時候,實際上都會啟動一個 JVM ,每一個 JVM 實際就是在操作系統中啟動了一個進程。

四、線程狀態轉換

下面的這個圖非常重要!你如果看懂了這個圖,那么對于多線程的理解將會更加深刻!

線程狀態轉換圖

學過操作系統的同學應該看起來很容易的,畢竟當初考試的時候這一塊是個重點,沒少復習這一塊。

  • 新建狀態(New):新創建了一個線程對象
  • 就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的 start() 方法,該狀態的線程位于可運行的線程池中,變為可運行狀態,這個時候,只要獲取了 cpu 的執行權,就可以運行,進入運行狀態。
  • 運行狀態(Running): 就緒狀態的線程從 cpu 獲得了執行權之后,便可進入此狀態,執行 run() 方法里面的代碼。
  • 阻塞狀態(Blocked):阻塞狀態是線程因為某種原因失去了 cpu 的使用權,暫時停止運行,一直等到線程進入就緒狀態,才有機會轉到運行狀態,阻塞一般分為下面三種:
    • 等待阻塞 :運行的線程執行了 wait() 方法, JVM 會把該線程放入線程等待池中,(wait() 會釋放持有的鎖 )
    • 同步阻塞:運行的線程在獲取對象的同步鎖時,如果該同步鎖被其他線程占用,這時此線程是無法運行的,那么 JVM 就會把該線程放入鎖池中,導致阻塞
    • 其他阻塞:運行的線程執行 sleep() 或者 join() 方法,或者發出了 I/O 請求,JVM 會把該線程置為阻塞狀態,當 sleep() 狀態超時、join() 等待線程終止或者超時、或者 I/O 處理完畢時,線程會重新進入就緒狀態,(注意:sleep() 是不會釋放本身持有的鎖的)
  • 死亡狀態(Dead):線程執行完了之后或者因為程序異常退出了 run() 方法,結束該線程的生命周期。

五、線程調度

1. 調整線程優先級

Java 線程有優先級,優先級高的線程會獲得較多的運行機會,Java 線程的優先級用整數表示,取值范圍是 1~10 ,Thread 類有以下三個靜態常量:

static int MAX_PRIORITY      線程可以具有的最高優先級,取值為10。  
static int MIN_PRIORITY       線程可以具有的最低優先級,取值為1。  
static int NORM_PRIORITY   分配給線程的默認優先級,取值為5。 

Thread 類的 setPriority() 和 getPriority() 分別用于設置和獲取線程的優先級。

  • 每個線程都有默認的優先級。主線程的默認優先級為Thread.NORM_PRIORITY。
  • 線程的優先級有繼承關系,比如A線程中創建了B線程,那么B將和A具有相同的優先級。
  • JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先級,這樣能保證同樣的優先級采用了同樣的調度方式。

2. 線程睡眠

Thread.sleep(long millis) 方法,使線程轉到阻塞狀態。millis 參數設定睡眠的時間,以毫秒為單位。當睡眠結束后,就轉為就緒(Runnable)狀態。sleep() 平臺移植性好。

3. 線程等待

Object 類中的 wait() 方法,導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是 Object
類中的方法,行為等價于調用 wait(0) 一樣。

4. 線程讓步

Thread.yield()方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。

5. 線程加入

join() 方法,等待其他線程終止,在當前線程中調用一個線程的 join() 方法,則當前線程轉為阻塞狀態,回到另一個線程結束,當前線程再由阻塞狀態變為就緒狀態,等待 cpu 的寵幸。

6. 線程喚醒

Object 類中的 notify() 方法,喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,并在對實現做出決定時發生,線程通過調用其中一個 wait() 方法,在對象的監視器上等待,直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程,被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。

例如:喚醒的線程在作為鎖定此對象的下一個線程方面沒有可靠的特權或者劣勢,

類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有線程,

注意: Thread 中 suspend() 和 resume() 兩個方法已經在 JDK 1.5 中廢除,此處不做介紹,因為有死鎖傾向。

六、常用函數說明

1. sleep(long millis): 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行)

2. join() 指等待t線程終止。

使用方式

Thread6 thread1 = new Thread6();
thread1.start();
thread1.join(); 

為什么要用join()方法

很多情況下,主線程生成并啟動了子線程,如果子線程需要大量的耗時運算,主線程往往將于子線程結束之前結束,但是如果主線程處理完了其他事務后,需要用到子線程返回的結果,也就是需要主線程需要在子線程結束后再結束,這時候就要用到 join() 方法。

先看下不加 join() 的代碼:

/**
 * Created by Sean on 2017/5/9.
 */
class Thread6 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + "運行開始!");
        for (int i = 0; i < 5; i++) {
            System.out.println(currentThread().getName() + "======>" + i);
            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "運行結束!");
    }
}

public class Main6 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "線程運行開始!");
        Thread6 thread1 = new Thread6();
        Thread6 thread2 = new Thread6();
        thread1.setName("線程A");
        thread2.setName("線程B");
        thread1.start();
        thread2.start();
        System.out.println("這時thread1 和 thread2 都執行完畢之后才能執行主線程打印此句話因為兩個子線程都被主線程調用了join() 方法");
        System.out.println(Thread.currentThread().getName() + "線程運行結束!");
    }
}

結果:

運行結果

從結果中可以看到我們打印的 main 線程運行結束之后,兩個子線程才開始執行,這和上面說的是對照的,

下面演示下等待兩個子線程結束之后再結束主線程:

/**
 * Created by Sean on 2017/5/9.
 */
class Thread6 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + "運行開始!");
        for (int i = 0; i < 5; i++) {
            System.out.println(currentThread().getName() + "======>" + i);
            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "運行結束!");
    }
}

public class Main6 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "線程運行開始!");
        Thread6 thread1 = new Thread6();
        Thread6 thread2 = new Thread6();
        thread1.setName("線程A");
        thread2.setName("線程B");
        thread1.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
        try {
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("這時thread1 和 thread2 都執行完畢之后才能執行主線程打印此句話因為兩個子線程都被主線程調用了join() 方法");
        System.out.println(Thread.currentThread().getName() + "線程運行結束!");
    }
}

運行結果:

運行結果

這個時候不論執行多少遍,都是主線程等待子線程結束后才結束。

如果主線程的執行需要依賴于子線程中的完整數據的時候,這種方法就可以很好的確保兩個線程的同步性。

3. yield():暫停當前正在執行的線程對象,并執行其他線程。

注意:yield() 應該做的是讓當前運行線程回到可運行狀態(就緒狀態),以允許具有相同優先級的其他線程獲得運行機會。因此,使用 yield() 的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證 yield() 達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。

結論:yield() 從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield() 將導致線程從運行狀態轉到可運行狀態(就緒狀態),但有可能沒有效果。可看上面的圖。

看下面的例子:

/**
 * yield()的用法
 * Created by Sean on 2017/5/9.
 */
class Thread7 extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 當i為30時,該線程就會把CPU時間讓掉,讓其他或者自己的線程執行(也就是誰先搶到誰執行)
            if (i == 30) {
                this.yield();
            }
        }
    }
}

public class Main7 {
    public static void main(String[] args) {
        Thread7 thread1 = new Thread7();
        Thread7 thread2 = new Thread7();
        thread1.setName("A線程");
        thread2.setName("B線程");
        thread1.start();
        thread2.start();
    }
}

運行結果:

第一種情況:A線程當執行到30時會CPU時間讓掉,這時A線程搶到 CPU 的時間片執行。
第二種情況:B線程當執行到30時會CPU時間讓掉,這時A線程搶到 CPU 的時間片執行。
第二種情況:從一開始就交替執行,當到30的時候進行一次讓步。

sleep()和yield()的區別

  • sleep() 使當前線程進入停滯狀態,所以執行 sleep() 的線程在指定的時間內肯定不會被執行
  • yield() 只是使當前線程重新回到可執行狀態,所以執行 yield() 的線程有可能在進入到可執行狀態后馬上又被執行。

sleep 方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 占有權,但讓出的時間是不可設定的。實際上,yield() 方法對應了如下操作:先檢測當前是否有相同優先級的線程處于同可運行狀態,如有,則把 CPU 的占有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱為“退讓”,它把運行機會讓給了同等優先級的其他線程

另外,sleep 方法允許較低優先級的線程獲得運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 占有權。在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那么,較低優先級線程只能等待所有較高優先級的線程運行結束,才有機會運行。

4. setPriority(): 更改線程的優先級。

MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10
用法:
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

5. interrupt()

不要以為它是中斷某個線程!它只是線線程發送一個中斷信號,讓線程在無限等待時(如死鎖時)能拋出拋出,從而結束線程,但是如果你吃掉了這個異常,那么這個線程還是不會中斷的!

6. wait() 暫停線程,釋放 cpu 控制權,同時釋放對象鎖的控制

Obj.wait() 與 Obj.notify() 必須要與 synchronized(Obj) 一起使用,也就是
wait 與 notify 是針對已經獲取了 Obj 鎖進行操作,從語法角度來說就是
Obj.wait()、Obj.notify 必須在 synchronized(Obj){...} 語句塊內。從功能上來說 wait 就是說線程在獲取對象鎖后,主動釋放對象鎖,同時本線程休眠。直到有其它線程調用對象的notify()喚醒該線程,才能繼續獲取對象鎖,并繼續執行。相應的 notify() 就是對對象鎖的喚醒操作。但有一點需要注意的是 notify() 調用后,并不是馬上就釋放對象鎖的,而是在相應的 synchronized(){} 語句塊執行結束,自動釋放鎖后, JVM會在wait() 對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。 Thread.sleep() 與 Object.wait() 二者都可以暫停當前線程,釋放 CPU 控制權,主要的區別在于 Object.wait() 在釋放 CPU 同時,釋放了對象鎖的控制。
單單在概念上理解清楚了還不夠,需要在實際的例子中進行測試才能更好的理解。對 Object.wait() 、Object.notify() 的應用最經典的例子,應該是三線程打印 ABC 的問題了吧,這是一道比較經典的面試題,題目要求如下:
建立三個線程,A線程打印 10 次 A、B 線程打印 10 次 B、C 線程打印
10 次 C,要求線程同時運行,交替打印 10 次 ABC 。這個問題用 Object
的 wait() , notify() 就可以很方便的解決。代碼如下:

/**
 * wait() 練習
 * Created by Sean on 2017/5/9.
 */
class Thread8 implements Runnable {

    private String name;
    private Object prev;
    private Object self;

    private Thread8(String name, Object prev, Object self) {
        this.name = name;
        this.prev = prev;
        this.self = self;
    }

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (prev) {   //上一個對象鎖,先申請上一個對象的鎖,如果上個線程釋放對象鎖,則獲取該對象鎖
                synchronized (self) {   // 當前對象鎖
                    System.out.print(name + ((count == 1 && name.equals("C")) ? "" : "->"));
                    count--;
                    self.notify(); // 喚醒下一個等待線程
                }
                try {
                    prev.wait();// 釋放當前線程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        Thread8 threadA = new Thread8("A", c, a);//c是上個對象,a是當前對象
        Thread8 threadB = new Thread8("B", a, b);//a是上個對象,b是當前對象
        Thread8 threadC = new Thread8("C", b, c);//b是上個對象,c是當前對象
        new Thread(threadA).start();
        Thread.sleep(100);  //確保按順序A、B、C執行
        new Thread(threadB).start();
        Thread.sleep(100);  //確保按順序A、B、C執行
        new Thread(threadC).start();
        Thread.sleep(100);  //確保按順序A、B、C執行
    }
}

輸出結果:

輸出結果

先來解釋一下其整體思路,從大的方向上來講,該問題為三線程間的同步喚醒操作,主要的目的就是 ThreadA -> ThreadB -> ThreadC -> ThreadA 循環執行三個線程。為了控制線程執行的順序,那么就必須要確定喚醒、等待的順序,所以每一個線程必須同時持有兩個對象鎖,才能繼續執行。一個對象鎖是 prev ,就是前一個線程所持有的對象鎖。還有一個就是自身對象鎖。主要的思想就是,為了控制執行的順序,必須要先持有 prev 鎖,也就前一個線程要釋放自身對象鎖,再去申請自身對象鎖,兩者兼備時打印,之后首先調用 self.notify() 釋放自身對象鎖,喚醒下一個等待線程,再調用 prev.wait() 釋放 prev 對象鎖,終止當前線程,等待循環結束后再次被喚醒。

通過上面代碼可以看到, A、B、C 都被順序打印了十次,過程是這樣的:

  1. 打印A:A 線程先運行,A 線程持有 C、A對像鎖,因為C對象鎖對應上一個打印的線程,A 對象鎖對應自己打印的線程。然后在自身對象鎖中synchronized (self) { }執行完之后喚醒下一個打印線程,然后在上一個對象鎖synchronized (prev) { }中暫停線程、釋放 CPU 的控制權,同時釋放 C 對象鎖的控制權
  2. 打印B:拿到線程 A 釋放的 A 對像鎖,然后獲取自身的 B 對象鎖,重復上面“打印A”的步驟
  3. 打印C:拿到線程 B 釋放的 B 對像鎖,然后獲取自身的 C 對象鎖,重復上面“打印A”的步驟
  4. 打印A:拿到線程 C 釋放的 C 對像鎖,然后獲取自身的 A 對象鎖,重復上面“打印A”的步驟
    .
    .
    .
    .
    .
    這樣一直執行到程序結束全部打印完畢。

wait() 和 sleep() 區別

  1. 共同點:
  • 他們都是在多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,并返回。
  • wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。

如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。
需要注意的是,InterruptedException是線程自己從內部拋出的,并不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那么該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()后,就會立刻拋出InterruptedException 。

  1. 不同點:
  • Thread類的方法:sleep(),yield()等
    Object的方法:wait()和notify()等
  • 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。
    sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
  • wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用

所以sleep()和wait()方法的最大區別是:
    sleep()睡眠時,保持對象鎖,仍然占有該鎖;
    而wait()睡眠時,釋放對象鎖。
但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態,從而使線程立刻拋出InterruptedException(但不建議使用該方法)

sleep() 方法

sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸占該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;

sleep()是Thread類的Static(靜態)的方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,但是對象的機鎖并木有被釋放,其他線程無法訪問這個對象(即使睡著也持有對象鎖)。

在sleep()休眠時間期滿后,該線程不一定會立即執行,這是因為其它線程可能正在運行而且沒有被調度為放棄執行,除非此線程具有更高的優先級。

wait() 方法

wait()方法是Object類里的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到后還需要返還對象鎖);其他線程可以訪問;

wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程。

wiat()必須放在synchronized block中,否則會在program runtime時扔出”java.lang.IllegalMonitorStateException“異常。

七、常見線程名詞解釋以及常用方法

1. 名詞解釋

  • 主線程:JVM 調用程序 main() 所產生的線程。
  • 當前線程:這個是容易混淆的概念。一般指通過 Thread.currentThread() 來獲取的進程。
  • 后臺線程:指為其他線程提供服務的線程,也稱為守護線程。JVM 的垃圾回收線程就是一個后臺線程。用戶線程和守護線程的區別在于,是否等待主線程依賴于主線程結束而結束
  • 前臺線程:是指接受后臺線程服務的線程,其實前臺后臺線程是聯系在一起,就像傀儡和幕后操縱者一樣的關系。傀儡是前臺線程、幕后操縱者是后臺線程。由前臺線程創建的線程默認也是前臺線程。可以通過
    isDaemon() 和 setDaemon() 方法來判斷和設置一個線程是否為后臺線程。

2. 線程類的一些常用方法:

  • sleep():強迫一個線程睡眠N毫秒。
  • isAlive(): 判斷一個線程是否存活。
  • join(): 等待線程終止。
  • activeCount(): 程序中活躍的線程數。
  • enumerate(): 枚舉程序中的線程。
  • currentThread(): 得到當前線程。
  • isDaemon(): 一個線程是否為守護線程。
  • setDaemon(): 設置一個線程為守護線程。(用戶線程和守護線程的區別在于,是否等待主線程依賴于主線程結束而結束)
  • setName(): 為線程設置一個名稱。
  • wait(): 強迫一個線程等待。
  • notify(): 通知一個線程繼續運行。
  • setPriority(): 設置一個線程的優先級。
  • getPriority()::獲得一個線程的優先級。

八、線程同步

1、synchronized關鍵字的作用域

  1. 是某個對象實例內,synchronized aMethod(){} 可以防止多個線程同時訪問這個對象的 synchronized 方法(如果一個對象有多個 synchronized 方法,只要一個線程訪問了其中的一個 synchronized 方法,其它線程不能同時訪問這個對象中任何一個 synchronized 方法)。這時,不同的對象實例的 synchronized 方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的 synchronized 方法;
  2. 是某個類的范圍,synchronized static aStaticMethod{} 防止多個線程同時訪問這個類中的 synchronized static 方法。它可以對類的所有對象實例起作用。

2、synchronized關鍵字實現互斥訪問

除了方法前用 synchronized 關鍵字, synchronized 關鍵字還可以用于方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是: synchronized(this){/*區塊*/},它的作用域是當前對象;

3. 不能繼承

synchronized 關鍵字是不能繼承的,也就是說,基類的方法 synchronized f(){}在繼承類中并不自動是synchronized f(){},而是變成了 f(){} 。繼承類需要你顯式的指定它的某個方法為 synchronized 方法;

4. 用法

Java對多線程的支持與同步機制深受大家的喜愛,似乎看起來使用了synchronized關鍵字就可以輕松地解決多線程共享數據同步問題。到底如何?――還得對synchronized關鍵字的作用進行深入了解才可定論。

總的說來,synchronized關鍵字可以作為函數的修飾符,也可作為函數內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用于instance變量、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。

在進一步闡述之前,我們需要明確幾點:

  • 無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。
  • 每個對象只有一個鎖(lock)與之相關聯。
  • 實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。

接著來討論synchronized用到不同地方對代碼產生的影響:

假設P1、P2是同一個類的不同對象,這個類中定義了以下幾種情況的同步塊或同步方法,P1、P2就都可以調用它們。

把synchronized當作函數修飾符時

示例代碼如下:

Public synchronized void methodAAA()  
{  
      //….  
}  

這也就是同步方法,那這時 synchronized 鎖定的是哪個對象呢?它鎖定的是調用這個同步方法對象。也就是說,當一個對象 P1 在不同的線程中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個對象所屬的 Class 所產生的另一對象 P2 卻可以任意調用這個被加了
synchronized 關鍵字的方法。
上邊的示例代碼等同于如下代碼:

public void methodAAA()  
{  
synchronized (this)      //  (1)  
{  
       //…..  
}  
}  

(1)處的this指的是什么呢?它指的就是調用這個方法的對象,如P1。可見同步方法實質是將synchronized作用于object reference。――那個拿到了P1對象鎖的線程,才可以調用P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程序也可能在這種情形下擺脫同步機制的控制,造成數據混亂.

2. 同步塊

示例代碼如下:

    public void method3(SomeObject so) {
        synchronized (so) {
            //…..  
        }
    }

這時,鎖就是so這個對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。當有一個明確的對象作為鎖時,就可以這樣寫程序,但當沒有明確的對象作為鎖,只是想讓一段代碼同步時,可以創建一個特殊的 instance 變量(它得是一個對象)來充當鎖:

class Foo implements Runnable {
    private byte[] lock = new byte[0];  // 特殊的instance變量  

    public void methodA() {
        synchronized (lock) { //… }  
        }
//…..  
    }
}

注:零長度的 byte 數組對象創建起來將比任何對象都經濟――查看編譯后的字節碼:生成零長度的 byte[] 對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

3. 將synchronized作用于static 函數

示例代碼如下:

class Foo {
    public synchronized static void methodAAA()   // 同步的static 函數
    {
//….
    }

    public void methodBBB() {
        synchronized (Foo.class)   //  class literal(類名稱字面常量)
    }
}  

代碼中的 methodBBB() 方法是把 class literal 作為鎖的情況,它和同步的
static 函數產生的效果是一樣的,取得的鎖很特別,是當前調用這個方法的對象所屬的類(Class,而不再是由這個 Class 產生的某個具體對象了)。
記得在《Effective Java》一書中看到過將 Foo.class 和 P1.getClass() 用于作同步鎖還不一樣,不能用 P1.getClass() 來達到鎖這個 Class 的目的。 P1 指的是由 Foo 類產生的對象。
可以推斷:如果一個類中定義了一個 synchronized 的 static 函數A,也定義了一個 synchronized 的 instance 函數B,那么這個類的同一對象 Obj 在多線程中分別訪問 A 和 B 兩個方法時,不會構成同步,因為它們的鎖都不一樣。 A 方法的鎖是 Obj 這個對象,而 B 的鎖是 Obj 所屬的那個
Class 。

總結

  1. 線程同步的目的是為了保護多個線程反問一個資源時對資源的破壞。
  2. 線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他非同步方法
  3. 對于靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程獲得鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。
  4. 對于同步,要時刻清醒在哪個對象上同步,這是關鍵。
  5. 編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,并保證原子操作期間別的線程無法訪問競爭資源。
  6. 當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。
  7. 死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,不一定好使,呵呵。但是,一旦程序發生死鎖,程序將死掉。

九、線程數據傳遞

在傳統的同步開發模式下,當我們調用一個函數時,通過這個函數的參數將數據傳入,并通過這個函數的返回值來返回最終的計算結果。但在多線程的異步開發模式下,數據的傳遞和返回和同步開發模式有很大的區別。由于線程的運行和結束是不可預料的,因此,在傳遞和返回數據時就無法象函數一樣通過函數參數和 return 語句來返回數據。

1. 通過構造方法傳遞數據

在創建線程時,必須要建立一個 Thread 類的或其子類的實例。因此,我們不難想到在調用 start 方法之前通過線程類的構造方法將數據傳入線程。并將傳入的數據使用類變量保存起來,以便線程使用(其實就是在 run 方法中使用)。下面的代碼演示了如何通過構造方法來傳遞數據:

/**
 * Created by Sean on 2017/5/9.
 */
class Thread9 extends Thread {
    private String threadName;

    public Thread9(String threadName) {
        this.threadName = threadName;
    }

    public void run() {
        System.out.println("hello " + threadName);
    }
}

public class Main9 {
    public static void main(String[] args) {
        Thread9 thread = new Thread9("world");
        thread.start();
    }
}

由于這種方法是在創建線程對象的同時傳遞數據的,因此,在線程運行之前這些數據就就已經到位了,這樣就不會造成數據在線程運行后才傳入的現象。如果要傳遞更復雜的數據,可以使用集合、類等數據結構。使用構造方法來傳遞數據雖然比較安全,但如果要傳遞的數據比較多時,就會造成很多不便。由于 Java 沒有默認參數,要想實現類似默認參數的效果,就得使用重載,這樣不但使構造方法本身過于復雜,又會使構造方法在數量上大增。因此,要想避免這種情況,就得通過類方法或類變量來傳遞數據。

2. 通過變量和方法傳遞數據

向對象中傳入數據一般有兩次機會:

  • 第一次機會是在建立對象時通過構造方法將數據傳入;
  • 另外一次機會就是在類中定義一系列的 public 的方法或變量(也可稱之為字段)。然后在建立完對象后,通過對象實例逐個賦值。下面的代碼是對Thread9 類的改版,使用了一個 setThreadName 方法來設置 threadName 變量:
/**
 * Created by Sean on 2017/5/9.
 */
class Thread9 extends Thread {
    private String threadName;

    public Thread9(String threadName) {
        this.threadName = threadName;
    }

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }

    public void run() {
        System.out.println("hello " + threadName);
    }
}

public class Main9 {
    public static void main(String[] args) {
        Thread9 thread = new Thread9("world");
        thread.start();
    }
}

3. 通過回調函數傳遞數據

面討論的兩種向線程中傳遞數據的方法是最常用的。但這兩種方法都是
main 方法中主動將數據傳入線程類的。這對于線程來說,是被動接收這些數據的。然而,在有些應用中需要在線程運行的過程中動態地獲取數據,如在下面代碼的 run 方法中產生了 3 個隨機數,然后通過 Work 類的
process 方法求這三個隨機數的和,并通過 Data 類的 value 將結果返回。從這個例子可以看出,在返回 value 之前,必須要得到三個隨機數。也就是說,這個 value 是無法事先就傳入線程類的。

/**
 * 回調實現多線程傳遞數據
 * Created by Sean on 2017/5/9.
 */
class Data {
    public int value = 0;
}

class Work {
    public void process(Data data, Integer[] numbers) {
        for (int n : numbers) {
            data.value += n;
        }
    }
}


class Thread10 extends Thread {
    private Work work;

    public Thread10(Work work) {
        this.work = work;
    }

    @Override
    public void run() {
        super.run();
        Integer[] numbers = new Integer[3];
        java.util.Random random = new java.util.Random();
        Data data = new Data();
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = random.nextInt(100);
        }
        work.process(data,numbers);
        System.out.println(String.valueOf(numbers[0]) + "+" + String.valueOf(numbers[1]) + "+"
                + String.valueOf(numbers[2]) + "=" + data.value);
    }

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

多線程就寫到這里了,基本都是按照這篇博客敲的,每個貼的代碼都是親自重寫、驗證,都是可執行的,雖然花了很多時間,但是自己對多線程有了更深層次的認識,希望這篇文章可以幫到大家。

林炳文Evankaka原創作品。出處http://blog.csdn.net/evankaka

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容