溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

線程的實(shí)現(xiàn)方式有哪些

發(fā)布時(shí)間:2021-10-23 15:47:02 來源:億速云 閱讀:162 作者:iii 欄目:編程語言

這篇文章主要講解了“線程的實(shí)現(xiàn)方式有哪些”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“線程的實(shí)現(xiàn)方式有哪些”吧!

方式一:繼承Thread類

package com.thread;
//通過繼承Thread類實(shí)現(xiàn)自定義線程類
public class MyThread extends Thread {
	//線程體
    @Override
    public void run() {
        System.out.println("Hello, I am the defined thread created by extends Thread");
    }
    public static void main(String[] args){
        //實(shí)例化自定義線程類實(shí)例
        Thread thread = new MyThread();
        //調(diào)用start()實(shí)例方法啟動(dòng)線程
        thread.start();
    }
}
123456789101112131415

優(yōu)點(diǎn):實(shí)現(xiàn)簡單,只需實(shí)例化繼承類的實(shí)例,即可使用線程 缺點(diǎn):擴(kuò)展性不足,Java是單繼承的語言,如果一個(gè)類已經(jīng)繼承了其他類,就無法通過這種方式實(shí)現(xiàn)自定義線程

方式二:實(shí)現(xiàn)Runnable接口

package com.thread;
public class MyRunnable implements Runnable {
    //線程體
    @Override
    public void run() {
        System.out.println("Hello, I am the defined thread created by implements Runnable");
    }
    public static void main(String[] args){
    	//線程的執(zhí)行目標(biāo)對象
        MyRunnable myRunnable = new MyRunnable();
        //實(shí)際的線程對象
        Thread thread = new Thread(myRunnable);
        //啟動(dòng)線程
        thread.start();
    }
}

優(yōu)點(diǎn):

  • 擴(kuò)展性好,可以在此基礎(chǔ)上繼承其他類,實(shí)現(xiàn)其他必需的功能

  • 對于多線程共享資源的場景,具有天然的支持,適用于多線程處理一份資源的場景

缺點(diǎn):構(gòu)造線程實(shí)例的過程相對繁瑣一點(diǎn)

方式三:實(shí)現(xiàn)Callable接口

package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Hello, I am the defined thread created by implements Callable";
    }
    public static void main(String[] args){
        //線程執(zhí)行目標(biāo)
        MyCallable myCallable = new MyCallable();
        //包裝線程執(zhí)行目標(biāo),因?yàn)門hread的構(gòu)造函數(shù)只能接受Runnable接口的實(shí)現(xiàn)類,而FutureTask類實(shí)現(xiàn)了Runnable接口
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        //傳入線程執(zhí)行目標(biāo),實(shí)例化線程對象
        Thread thread = new Thread(futureTask);
        //啟動(dòng)線程
        thread.start();
        String result = null;
        try {
            //獲取線程執(zhí)行結(jié)果
            result = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(result);
    }
}

優(yōu)點(diǎn):

  • 擴(kuò)展性好

  • 支持多線程處理同一份資源

  • 具備返回值以及可以拋出受檢查異常

缺點(diǎn):

  • 相較于實(shí)現(xiàn)Runnable接口的方式,較為繁瑣

小結(jié)

我們對這三種方式進(jìn)行分析,可以發(fā)現(xiàn):方式一和方式二本質(zhì)上都是通過實(shí)現(xiàn)Runnable接口并重寫run()方法,將接口實(shí)現(xiàn)類的實(shí)例傳遞給Thread線程類來執(zhí)行線程體(run()方法中的實(shí)現(xiàn)),這里將Runnable接口實(shí)現(xiàn)類的實(shí)例作為線程執(zhí)行目標(biāo),供線程Thread實(shí)例執(zhí)行;對于方式三,其實(shí)也是這樣的,由于Thread類只能執(zhí)行Runnable接口實(shí)現(xiàn)類的執(zhí)行目標(biāo),所以需要對Callable接口的實(shí)現(xiàn)類進(jìn)行包裝,包裝成Runnable接口的實(shí)現(xiàn)類(通過實(shí)現(xiàn)了Runnable接口的FutureTask類進(jìn)行包裝),從而使得Thread類能夠接收Callable接口實(shí)現(xiàn)類的實(shí)例,可見這里使用了適配器模式!

綜上所述,三種實(shí)現(xiàn)方式都存在著一個(gè)使用范式,即首先實(shí)現(xiàn)線程執(zhí)行目標(biāo)對象(包含線程所要執(zhí)行的任務(wù)),然后將目標(biāo)對象作為構(gòu)造參數(shù)以實(shí)例化Thread實(shí)例,來獲得線程!本質(zhì)上都是實(shí)現(xiàn)一個(gè)線程體,由Thread來執(zhí)行線程體,達(dá)到開啟線程執(zhí)行任務(wù)的效果!但是,三種實(shí)現(xiàn)方式各有優(yōu)缺點(diǎn),使用時(shí),應(yīng)該結(jié)合具體需求來選用合適的實(shí)現(xiàn)方式進(jìn)行開發(fā)!


線程的生命周期

經(jīng)過上面的代碼演示,我們知道了線程如何實(shí)現(xiàn),但是如果我們想要更好地使用線程,還需要對程序運(yùn)行中線程的狀態(tài)以及狀態(tài)之間的轉(zhuǎn)換(即線程的生命周期)有所了解,這樣才能在多線程程序運(yùn)行出現(xiàn)問題時(shí),分析問題產(chǎn)生的原因,從而快速準(zhǔn)確地定位并解決問題!

首先,看一下Thread類中給出的關(guān)于線程狀態(tài)的說明:

	/**
     * 線程生命周期中的的六種狀態(tài)
     * NEW:還沒有調(diào)用start()的線程實(shí)例所處的狀態(tài)
     * RUNNABLE:正在虛擬機(jī)中執(zhí)行的線程所處的狀態(tài)
     * BLOCKED:等待在監(jiān)視器鎖上的線程所處的狀態(tài)
     * WAITING:等待其它線程執(zhí)行特定操作的線程所處的狀態(tài)
     * TIMED_WAITING:等待其它線程執(zhí)行超時(shí)操作的線程所處的狀態(tài)
     * TERMINATED:退出的線程所處的狀態(tài)
     * 給定時(shí)間點(diǎn),一個(gè)線程只會(huì)處于以下狀態(tài)中的一個(gè),這些狀態(tài)僅僅是虛擬機(jī)層面的線程狀態(tài),并不能反映任何操作系統(tǒng)中線程的狀態(tài)
     */
    public enum State {
        //還沒有調(diào)用start()開啟的線程實(shí)例所處的狀態(tài)
        NEW, 
        //正在虛擬機(jī)中執(zhí)行或者等待被執(zhí)行的線程所處的狀態(tài),但是這種狀態(tài)也包含線程正在等待處理器資源這種情況
        RUNNABLE,
        // 等待在監(jiān)視器鎖上的線程所處的狀態(tài),比如進(jìn)入synchronized同步代碼塊或同步方法失敗
        BLOCKED,
        // 等待其它線程執(zhí)行特定操作的線程所處的狀態(tài);比如線程執(zhí)行了以下方法: Object.wait with no timeout、Thread.join with no timeout、 LockSupport.park
        WAITING,
       // 等待其它線程執(zhí)行超時(shí)操作的線程所處的狀態(tài);比如線程執(zhí)行了以下方法: Thread.sleep、Object.wait with timeout
       //Thread.join with timeout、LockSupport.parkNanos、LockSupport.parkUntil
        TIMED_WAITING,
        //退出的線程所處的狀態(tài)
        TERMINATED;
    }
  • 新建(New):當(dāng)線程實(shí)例被new出來之后,調(diào)用start()方法之前,線程實(shí)例處于新建狀態(tài)

  • 可運(yùn)行(Runnable):當(dāng)線程實(shí)例調(diào)用start()方法之后,線程調(diào)度器分配處理器資源之前,線程實(shí)例處于可運(yùn)行狀態(tài)或者線程調(diào)度器分配處理器資源給線程之后,線程實(shí)例處于運(yùn)行中狀態(tài),這兩種情況都屬于可運(yùn)行狀態(tài)

  • 等待(Waitting):當(dāng)線程處于運(yùn)行狀態(tài)時(shí),線程執(zhí)行了obj.wait()或Thread.join()方法、Thread.join、LockSupport.park以及Thread.sleep()時(shí),線程處于等待狀態(tài)

  • 超時(shí)等待(Timed Waitting):當(dāng)線程處于運(yùn)行狀態(tài)時(shí),線程執(zhí)行了obj.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil以及Thread.sleep(long)方法時(shí),線程處于超時(shí)等待狀態(tài)

  • 阻塞(Blocked):當(dāng)線程處于運(yùn)行狀態(tài)時(shí),獲取鎖失敗,線程實(shí)例進(jìn)入等待隊(duì)列,同時(shí)狀態(tài)變?yōu)樽枞?/p>

  • 終止(Terminated):當(dāng)線程執(zhí)行完畢或出現(xiàn)異常提前結(jié)束時(shí),線程進(jìn)入終止?fàn)顟B(tài)

線程的狀態(tài)轉(zhuǎn)換

上面也提到了,某一時(shí)間點(diǎn)線程的狀態(tài)只能是上述6個(gè)狀態(tài)中的其中一個(gè);但是,線程在程序運(yùn)行過程中的狀態(tài)是會(huì)發(fā)生變化的,由一個(gè)狀態(tài)轉(zhuǎn)變?yōu)榱硪粋€(gè)狀態(tài),那么下面給出線程狀態(tài)轉(zhuǎn)換圖幫助我們清晰地理解線程的狀態(tài)轉(zhuǎn)變過程: 線程的實(shí)現(xiàn)方式有哪些


上面我們已經(jīng)對線程的實(shí)現(xiàn)以及線程的狀態(tài)有了較為清晰的認(rèn)識(shí),那么通過上述內(nèi)容,我們也可以發(fā)現(xiàn)其實(shí)有很多方法,我們并沒有詳細(xì)地介紹,比如start()、yield()、wait()、notify()、notifyAll()、sleep()、join()等等,這些方法大多來源于JDK中Thread類這一關(guān)鍵的線程類中,下面結(jié)合Thread類的源碼看一下,多線程編程中經(jīng)常遇到的方法有哪些,以及這些方法的用途;

線程類Thread源碼

實(shí)例同步方法:join()

    /**
     * 等待調(diào)用此方法的線程執(zhí)行結(jié)束
     * @throws  InterruptedException 如果任何線程中斷了當(dāng)前線程,將會(huì)拋出此異常,同時(shí)將中斷標(biāo)志位清除
     */
    public final void join() throws InterruptedException {
        join(0);
    }
    /**
     * 最多等待millis毫秒,時(shí)間一到無論是否執(zhí)行完畢,都會(huì)返回
     * 如果millis為0,那么意味著一直等到線程執(zhí)行完畢才會(huì)返回
     * 此方法的實(shí)現(xiàn)是基于循環(huán)檢測當(dāng)前線程是否存活來判斷是否調(diào)用當(dāng)前實(shí)例的wait方法來實(shí)現(xiàn)的
     * @param  millis 等待時(shí)間
     * @throws  IllegalArgumentException 非法參數(shù)異常
     * @throws  InterruptedException 如果任何線程中斷了當(dāng)前線程,將會(huì)拋出此異常,同時(shí)將中斷標(biāo)志位清除
     */
    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    /**
     * 線程執(zhí)行結(jié)束之前最多等待millis毫秒nanos納秒
     * 此方法基于循環(huán)判斷isAlive返回值來決定是否調(diào)用wait方法來實(shí)現(xiàn)
     * 隨著一個(gè)線程終止,將會(huì)調(diào)用notifyAll方法 
     * 所以建議不要在當(dāng)前實(shí)例上調(diào)用 wait、 notify、 notifyAll
     */
    public final synchronized void join(long millis, int nanos)
            throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                    "nanosecond timeout value out of range");
        }
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
        join(millis);
    }

中斷方法以及檢測中斷方法和判活方法:

    /**
     * 中斷當(dāng)前線程
     * 如果當(dāng)前線程阻塞在Object的wait()、wait(long)、wait(long, int),或者
     * join()、join(long)、join(long, int)以及sleep(long)、sleep(long, int)等方法
     * 那么將會(huì)清除中斷標(biāo)志位并受到一個(gè)中斷異常
     * 非靜態(tài)方法
     */
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
    /**
     * 檢測當(dāng)前線程是否已經(jīng)被中斷,此方法會(huì)清除當(dāng)前線程的中斷標(biāo)志
     * 也就是說,如果這個(gè)方法被連續(xù)調(diào)用兩次,并且第一次調(diào)用之前,線程被中斷過,那么第一次調(diào)用返回true,第二次返回false
     * @return  <code>true</code> 如果當(dāng)前線程已經(jīng)被中斷,返回true,否則返回false     
     * 靜態(tài)方法
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    /**
     * 檢測當(dāng)前線程是否已經(jīng)被中斷,此方法不會(huì)清除當(dāng)前線程的中斷標(biāo)志
     * 非靜態(tài)方法
     */
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
    /**
     * 根據(jù)參數(shù)值決定是否在判斷中斷標(biāo)志位之后清除標(biāo)志位
     * 實(shí)例方法
     */
    private native boolean isInterrupted(boolean ClearInterrupted);
    /**
     * 檢測一個(gè)線程是否還存活,存活指的是已經(jīng)啟動(dòng)但還沒有終止
     * 實(shí)例方法
     */
    public final native boolean isAlive();

線程調(diào)度

Java線程的實(shí)現(xiàn):Java線程模型是基于操作系統(tǒng)原生線程模型來實(shí)現(xiàn)的; 線程模型只對線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響,對Java程序的編寫和運(yùn)行過程來說,并沒有什么不同;

線程優(yōu)先級(jí)

時(shí)分形式是現(xiàn)代操作系統(tǒng)采用的基本線程調(diào)度形式,操作系統(tǒng)將CPU資源分為一個(gè)個(gè)的時(shí)間片,并分配給線程,線程使用獲取的時(shí)間片執(zhí)行任務(wù),時(shí)間片使用完之后,操作系統(tǒng)進(jìn)行線程調(diào)度,其他獲得時(shí)間片的線程開始執(zhí)行;那么,一個(gè)線程能夠分配得到的時(shí)間片的多少?zèng)Q定了線程使用多少的處理器資源,線程優(yōu)先級(jí)則是決定線程可以獲得多或少的處理器資源的線程屬性;

可以通過設(shè)置線程的優(yōu)先級(jí),使得線程獲得處理器執(zhí)行時(shí)間的長短有所不同,但采用這種方式來實(shí)現(xiàn)線程獲取處理器執(zhí)行時(shí)間的長短并不可靠(因?yàn)橄到y(tǒng)的優(yōu)先級(jí)和Java中的優(yōu)先級(jí)不是一一對應(yīng)的,有可能Java中多個(gè)線程優(yōu)先級(jí)對應(yīng)于系統(tǒng)中同一個(gè)優(yōu)先級(jí));Java中有10個(gè)線程優(yōu)先級(jí),從1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默認(rèn)優(yōu)先級(jí)為5; 因此,程序的正確性不能夠依賴線程優(yōu)先級(jí)的高低來判斷;

## 線程調(diào)度

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程;主要調(diào)度方式有:搶占式線程調(diào)度、協(xié)同式線程調(diào)度;

搶占式線程調(diào)度

每個(gè)線程由系統(tǒng)來分配執(zhí)行時(shí)間,線程的切換不由線程本身決定;Java默認(rèn)使用的線程調(diào)度方式是搶占式線程調(diào)度;我們可以通過Thread.yield()使當(dāng)前正在執(zhí)行的線程讓出執(zhí)行時(shí)間,但是,卻沒有辦法使線程去獲取執(zhí)行時(shí)間;

### 協(xié)同式線程調(diào)度

每個(gè)線程的執(zhí)行時(shí)間由線程本身來控制,線程執(zhí)行完任務(wù)后主動(dòng)通知系統(tǒng),切換到另一個(gè)線程上;

兩種線程調(diào)度方式的優(yōu)缺點(diǎn)

協(xié)同式的優(yōu)點(diǎn):實(shí)現(xiàn)簡單,可以通過對線程的切換控制避免線程安全問題; 協(xié)同式的缺點(diǎn):一旦當(dāng)前線程出現(xiàn)問題,將有可能影響到其他線程的執(zhí)行,最終可能導(dǎo)致系統(tǒng)崩潰; 搶占式的優(yōu)點(diǎn):一個(gè)線程出現(xiàn)問題不會(huì)影響到其他線程的執(zhí)行(線程的執(zhí)行時(shí)間是由系統(tǒng)分配的,因此,系統(tǒng)可以將處理器執(zhí)行時(shí)間分配給其他線程從而避免一個(gè)線程出現(xiàn)故障導(dǎo)致整個(gè)系統(tǒng)崩潰的現(xiàn)象發(fā)生);

結(jié)論

在Java中,線程的調(diào)度策略主要是搶占式調(diào)度策略,正是因?yàn)閾屨际秸{(diào)度策略,導(dǎo)致多線程程序執(zhí)行過程中,實(shí)際的運(yùn)行過程與我們邏輯上理解的順序存在較大的區(qū)別,也就是多線程程序的執(zhí)行具有不確定性,從而會(huì)導(dǎo)致一些線程安全性問題的發(fā)生;那么,什么是線程安全呢?


線程安全

線程安全的定義

簡單來說,線程安全就是對于多個(gè)線程并發(fā)執(zhí)行的操作不需要進(jìn)行任何外部的控制,也不需要進(jìn)行任何的協(xié)調(diào),就能夠保證程序的執(zhí)行結(jié)果與開發(fā)人員的預(yù)期結(jié)果保持一致,那么這個(gè)多線程程序就是線程安全的; 注意: 線程安全問題一定是基于多個(gè)線程之間存在訪問共享數(shù)據(jù)這一前提下的;如果多個(gè)線程之間不會(huì)訪問同一個(gè)變量,那么就不存在線程安全的問題;

線程安全的分類

線程安全這一概念并不僅僅分為線程安全和非線程安全,按照線程安全的強(qiáng)弱程度可以將各種共享變量的操作分為:不可變、絕對線程安全、相對線程安全、線程兼容以及線程對立這五種情況;

  • 不可變:如果共享變量是不可變的對象,那么對該共享變量的多線程操作一定是線程安全的,因?yàn)閷ο笫遣豢勺兊?,所以任何線程都不可以改變共享變量的狀態(tài),也就不會(huì)出現(xiàn)臟讀等現(xiàn)象;

    • 如果共享變量是一個(gè)基本數(shù)據(jù)類型的變量,那么可以使用final關(guān)鍵字保證其是不可變的;

    • 如果共享變量是一個(gè)對象,那么就需要保證對象的行為不會(huì)改變該對象的狀態(tài),可以將一個(gè)類的所有字段使用final關(guān)鍵字修飾,那么就可以保證該類的對象是不可變的,如java.lang.String類;

  • 絕對線程安全:不需要在調(diào)用端進(jìn)行任何同步處理,就能保證代碼在多線程并發(fā)的場景下保證線程安全的,即多線程并發(fā)執(zhí)行的結(jié)果符合預(yù)期的結(jié)果;Java API中標(biāo)注為線程安全的類,大多數(shù)都不是絕對線程安全;

  • 相對線程安全:Java API中標(biāo)注為線程安全的類,大多數(shù)都是相對的線程安全,也就是通常意義上的線程安全,保證對共享變量單獨(dú)操作時(shí)是線程安全的,調(diào)用時(shí)可以不用額外的保障措施;例如Vector、HashTable或通過Collections的synchronizedCollection()方法包裝的集合等;

  • 線程兼容:線程兼容指對象本身并不是線程安全的,但是**可以通過在調(diào)用端正確采用同步手段來保證對象在并發(fā)環(huán)境中可以安全地使用,是通常意義上的非線程安全;Java API中的大部分類都是線程兼容的,**例如ArrayList、HashMap等;

  • 線程對立:無論調(diào)用端采用什么同步措施都不能保證多線程環(huán)境中的線程安全;線程對立很少出現(xiàn);


線程安全問題的解決方法

介紹了線程的調(diào)度原理之后,其實(shí)可以分析出線程安全問題的起因在于多線程的執(zhí)行順序具有不確定性,那么當(dāng)多個(gè)線程同時(shí)操作一份資源就不出現(xiàn)意想不到的情況,而編譯器和處理器會(huì)對執(zhí)行的指令進(jìn)行重排序,這些因素導(dǎo)致了線程安全問題;

那么,在實(shí)際開發(fā)中,我們一般需要解決的都是上述的相對線程安全以及線程兼容這兩種線程安全性問題;那么,對于這兩類問題,又可以細(xì)分為可見性、原子性以及有序性這三類問題;這里暫且先不進(jìn)行細(xì)分,就線程安全問題,我們給出常用解決措施;

線程安全問題重現(xiàn)

下面結(jié)合具體的代碼來看一下使用多線程編程時(shí)可能出現(xiàn)的線程安全問題:

	package com.thread;
	public class ThreadSafe implements Runnable{
   		//靜態(tài)變量,所有對象共享
    	private static int count = 0;
    	@Override
    	public void run() {
        	for(int i = 0 ; i < 100 ; i++){
            	count();
        	}
    	}
    	public void count(){
        	try {
            	Thread.currentThread().sleep(10);
        	} catch (InterruptedException e) {
            	e.printStackTrace();
        	}
        	count++;
    	}	
    	public static void main(String[] args) throws InterruptedException {
        	ThreadSafe threadSafe1 = new ThreadSafe();
        	ThreadSafe threadSafe2 = new ThreadSafe();
        	Thread thread1 = new Thread(threadSafe1);
        	Thread thread2 = new Thread(threadSafe2);
       		thread1.start();
        	thread2.start();
        	Thread.currentThread().sleep(1000);
        	System.out.println(count);
    	}
	}

運(yùn)行結(jié)果: 線程的實(shí)現(xiàn)方式有哪些

這一段代碼的目的是開啟兩個(gè)線程對同一個(gè)變量分別進(jìn)行100次的累加,按照正常的邏輯(串行化執(zhí)行),累加后的結(jié)果應(yīng)該為200,但是實(shí)際輸出的結(jié)果卻是190,顯然這和我們的預(yù)期結(jié)果不同,這就是線程安全問題;我們分析一下,為什么會(huì)出現(xiàn)這樣的情況,之前提到過,多線程執(zhí)行的時(shí)候代碼執(zhí)行的順序具有不確定性,那么就可能出現(xiàn),線程1(thread1)在獲取到count的值之后,CPU執(zhí)行權(quán)被分配給了線程2(thread2),線程2獲取到的值與線程1獲取到的相同,那么兩個(gè)線程累加操作執(zhí)行后,相當(dāng)于只累加來一次,這樣就會(huì)導(dǎo)致線程不安全問題產(chǎn)生;那么,如何解決這個(gè)問題,我們可以利用Java中的synchronized關(guān)鍵字對線程體進(jìn)行同步,代碼如下:

	package com.thread;
	public class ThreadSafeTwo implements Runnable{
    	//靜態(tài)變量,所有對象共享
    	private static int count = 0;
    	@Override
    	public void run() {
        	//這里對線程體進(jìn)行同步
        	synchronized(ThreadSafeTwo.class){
            	for(int i = 0 ; i < 100 ; i++){
                	count();
            	}
        	}
    	}
    	public void count(){
        	try {
            	Thread.currentThread().sleep(10);
        	} catch (InterruptedException e) {
            	e.printStackTrace();
        	}
        	count++;
    	}
    	public static void main(String[] args) throws InterruptedException {
        	ThreadSafeTwo threadSafe = new ThreadSafeTwo();
        	Thread thread1 = new Thread(threadSafe);
        	Thread thread2 = new Thread(threadSafe);
        	thread1.start();
        	thread2.start();
        	thread1.join();
        	thread2.join();
        	System.out.println(count);
    	}
	}

同步處理后代碼執(zhí)行的結(jié)果如下: 線程的實(shí)現(xiàn)方式有哪些

顯然,經(jīng)過同步后的代碼,就可以保證多線程并發(fā)執(zhí)行的情況下,結(jié)果依然符合預(yù)期結(jié)果;關(guān)于synchronized關(guān)鍵字的實(shí)現(xiàn)原理將會(huì)另起一文進(jìn)行分析,下面我們看一下,synchronized關(guān)鍵字的使用方式有哪些?

** synchronized關(guān)鍵字的使用方式**

  • synchronized同步代碼塊

    • 鎖的對象為指定的對象

  • synchronized同步實(shí)例方法

    • 鎖的對象為當(dāng)前實(shí)例

  • synchronized同步靜態(tài)方法

    • 鎖的對象為Class對象

synchronized關(guān)鍵字的應(yīng)用實(shí)例

線程安全的單例模式實(shí)現(xiàn)

	package com.thread;
	public class SingleTonThreadSafe {
    	//屬性私有化,volatile實(shí)現(xiàn)內(nèi)存可見性、禁止指令重排序
    	private volatile static SingleTonThreadSafe singleTonThreadSafe = null;
    	//無參構(gòu)造函數(shù)私有化
    	private SingleTonThreadSafe(){}
    	//靜態(tài)方法外部使用,獲取對象實(shí)例
    	public static SingleTonThreadSafe getInstance(){
        	//第一次判斷,避免不必要的加鎖
        	if(singleTonThreadSafe == null){
            	//同步實(shí)例化代碼塊
            	synchronized(SingleTonThreadSafe.class){
                	//再次檢測,避免其它線程已經(jīng)實(shí)例化
                	if(singleTonThreadSafe == null){
                    	//實(shí)例化,其他線程立即可見
                    	singleTonThreadSafe = new SingleTonThreadSafe();
                	}
            	}
        	}
        	return singleTonThreadSafe;
    	}
	}

synchronized同步鎖的使用注意點(diǎn)

  • 死鎖

    • 定義:多個(gè)線程互相等待已被對方占有的鎖,同時(shí)都不釋放自己已經(jīng)占有的鎖,導(dǎo)致線程之間陷入僵持,致使系統(tǒng)不可用

    • 形成條件:互斥鎖、鎖只能主動(dòng)釋放、循環(huán)等待

    • 避免策略:順序加鎖、超時(shí)獲取自動(dòng)放棄、死鎖檢測

  • 活鎖

    • 定義:線程等待被其他線程喚醒,但是實(shí)際沒有線程來喚醒,導(dǎo)致線程一直無法恢復(fù)到運(yùn)行狀態(tài)

    • 避免策略:編程時(shí)有等待,就必須有對應(yīng)的喚醒


線程間通信

如果你的多線程程序僅僅是每個(gè)線程獨(dú)立完成各自的任務(wù),相互之間并沒有交互和協(xié)作,那么,你的程序是無法發(fā)揮出多線程的優(yōu)勢的,只有有交互的多線程程序才是有意義的程序,否則,還不如使用單線程執(zhí)行多個(gè)方法實(shí)現(xiàn)程序來的簡單、易懂、有效!

那么,線程間進(jìn)行交互通信的手段有哪些呢?下面,將給出常用的多線程通信的實(shí)現(xiàn)手段以及相應(yīng)的代碼示例,并結(jié)合具體的代碼進(jìn)行分析,對其中需要注意的地方進(jìn)行突出提示;

等待通知機(jī)制

我們先看這樣一個(gè)場景:線程A修改了對象O的值,線程B感知到對象O的變化,執(zhí)行相應(yīng)的操作,這樣就是一個(gè)線程間交互的場景;可以看出,這種方式,相當(dāng)于線程A是發(fā)送了消息,線程B接收到消息,進(jìn)行后續(xù)操作,是不是很像生產(chǎn)者與消費(fèi)者的關(guān)系?我們都知道,生產(chǎn)者與消費(fèi)者模式可以實(shí)現(xiàn)解耦,使得程序結(jié)構(gòu)上具備伸縮性;那么Java中如何實(shí)現(xiàn)這種功能呢?

一種簡單的方式是,線程B每隔一段時(shí)間就輪詢對象O是否發(fā)生變化,如果發(fā)生變化,就結(jié)束輪詢,執(zhí)行后續(xù)操作;

但是,這種方式不能保證對象O的變更及時(shí)被線程B感知,同時(shí),不斷地輪詢也會(huì)造成較大的開銷;分析這些問題的癥結(jié)在哪?其實(shí),可以發(fā)現(xiàn)狀態(tài)的感知是拉取的,而不是推送的,因此才會(huì)導(dǎo)致這樣的問題產(chǎn)生;

那么,我們就會(huì)思考,如何將拉取變?yōu)橥扑蛠韺?shí)現(xiàn)這樣的功能呢?

這就引出了Java內(nèi)置的經(jīng)典的等待/通知機(jī)制,通過查看Object類的源碼發(fā)現(xiàn),該類中有三個(gè)方法,我們一般不會(huì)使用,但是在多線程編程中,這三個(gè)方法卻是能夠大放異彩的!那就是wait()/notify()/notifyAll();

   /**
     * 調(diào)用此方法會(huì)導(dǎo)致當(dāng)前線程進(jìn)入等待狀態(tài)直到其它線程調(diào)用同一對象的notify()或者notifyAll()方法
     * 當(dāng)前線程必須擁有對象O的監(jiān)視器,調(diào)用了對象O的此方法會(huì)導(dǎo)致當(dāng)前線程釋放已占有的監(jiān)視器,并且等待
     * 其它線程對象O的notify()或者notifyAll()方法,當(dāng)其它線程執(zhí)行了這兩個(gè)方法中的一個(gè)之后,并且
     * 當(dāng)前線程獲取到處理器執(zhí)行權(quán),就可以嘗試獲取監(jiān)視器,進(jìn)而繼續(xù)后續(xù)操作的執(zhí)行
     * 推薦使用方式:
     *     synchronized (obj) {
     *         while (&lt;condition does not hold&gt;)
     *             obj.wait();
     *         ... // Perform action appropriate to condition
     *     }
     * @throws  IllegalMonitorStateException  如果當(dāng)前線程沒有獲取到對象O的監(jiān)視器時(shí),拋出異常
     * @throws  InterruptedException 如果在調(diào)用了此方法之后,其他線程調(diào)用notify()或者notifyAll()
     * 方法之前,線程被中斷,則會(huì)清除中斷標(biāo)志并拋出異常
     */
    public final void wait() throws InterruptedException {
        wait(0);
    }
    /**
     * 喚醒等待在對象O的監(jiān)視器上的一個(gè)線程,如果多個(gè)線程等待在對象O的監(jiān)視器上,那么將會(huì)選擇其中的一個(gè)進(jìn)行喚醒
     * 被喚醒的線程只有在當(dāng)前線程釋放鎖之后才能夠繼續(xù)執(zhí)行.
     * 被喚醒的線程將會(huì)與其他線程一同競爭對象O的監(jiān)視器鎖
     * 這個(gè)方法必須在擁有對象O的監(jiān)視器的線程中進(jìn)行調(diào)用
     * 同一個(gè)時(shí)刻,只能有一個(gè)線程擁有該對象的監(jiān)視器
     * @throws  IllegalMonitorStateException  如果當(dāng)前線程沒有獲取到對象O的監(jiān)視器時(shí),拋出異常
     */
    public final native void notify();
    /**
     * 喚醒等待在對象O的監(jiān)視器上的所有線程
     * 被喚醒的線程只有在當(dāng)前線程釋放鎖之后才能夠繼續(xù)執(zhí)行.
     * 被喚醒的線程將會(huì)與其他線程一同競爭對象O的監(jiān)視器鎖
     * 這個(gè)方法必須在擁有對象O的監(jiān)視器的線程中進(jìn)行調(diào)用
     * 同一個(gè)時(shí)刻,只能有一個(gè)線程擁有該對象的監(jiān)視器
     * @throws  IllegalMonitorStateException  如果當(dāng)前線程沒有獲取到對象O的監(jiān)視器時(shí),拋出異常
     */
    public final native void notifyAll();

下面看一下如何通過這三個(gè)方法實(shí)現(xiàn)經(jīng)典的等待通知機(jī)制吧! 按照J(rèn)DK中推薦的使用方式實(shí)現(xiàn)了等待通知樣例代碼如下:

	package com.thread;	
	public class WaitAndNotify {
    	//輪詢標(biāo)志位
    	private static boolean stop = false;
    	//監(jiān)視器對應(yīng)的對象
    	private static Object monitor = new Object();
    	//等待線程
    	static class WaitThread implements Runnable{
        	@Override
        	public void run() {
            	synchronized(monitor){
                	//循環(huán)檢測標(biāo)志位是否變更
                	while(!stop){
                    	try {
                        	//標(biāo)志位未變更,進(jìn)行等待
                        	monitor.wait();
                    	} catch (InterruptedException e) {
                        	e.printStackTrace();
                    	}
                	}
                	//被喚醒后獲取到對象的監(jiān)視器之后執(zhí)行的代碼
                	System.out.println("Thread "+Thread.currentThread().getName()+" is awakened at first time");
                	stop = false;
            	}
            	//休眠1秒之后,線程角色轉(zhuǎn)換為喚醒線程
            	try {
                	Thread.sleep(1000);
            	} catch (InterruptedException e) {
               	 	e.printStackTrace();
            	}
	            //與上述代碼相反的邏輯
	            synchronized(monitor){
	                while(stop){
	                    try {
	                        monitor.wait();
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                }
	                monitor.notify();
	                stop = true;
	                System.out.println("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
	            }
	        }
	    }
	    //通知線程
	    static class NotifyThread implements Runnable{
	        @Override
	        public void run() {
	            synchronized (monitor){
	                while(stop){
	                    try {
	                        monitor.wait();
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                }
	                stop = true;
	                monitor.notify();
	                System.out.println("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
	            }
	            try {
	                Thread.sleep(1000);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	            synchronized (monitor){
	                while(!stop){
	                    try {
	                        monitor.wait();
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                }
	                System.out.println("Thread "+Thread.currentThread().getName()+" is awakened at first time");
	            }
	        }
	    }
	    public static void main(String[] args){
	        Thread waitThread = new Thread(new WaitThread());
	        waitThread.setName("waitThread");
	        Thread notifyThread = new Thread(new NotifyThread());
	        notifyThread.setName("notifyThread");
	        waitThread.start();
	        notifyThread.start();
	    }
	}

通過上述代碼,可以提煉出等待通知機(jī)制的經(jīng)典模式:

等待方實(shí)現(xiàn)步驟:

  • 加鎖同步

  • 條件不滿足,進(jìn)入等待,被喚醒之后,繼續(xù)檢查條件是否滿足(循環(huán)檢測)

  • 條件滿足,退出循環(huán),繼續(xù)執(zhí)行后續(xù)代碼

對應(yīng)的偽代碼:

	synchronized(obj){
		while(condition不滿足){
			obj.wait();
		}
		//后續(xù)操作
	}
123456

通知方實(shí)現(xiàn)步驟:

  • 加鎖同步

  • 條件不滿足,跳過循環(huán)檢測

  • 設(shè)置條件并喚醒線程

對應(yīng)的偽代碼:

	synchronized(obj){
		while(condition不滿足){
			obj.wait();
		}
		更新condition
		obj.notify();
		//后續(xù)操作
	}

生產(chǎn)者消費(fèi)者模式

基于等待通知機(jī)制,我們可以很容易地寫出生產(chǎn)者消費(fèi)者模式的代碼,下面給出一個(gè)實(shí)現(xiàn)樣例代碼:

	package com.thread;
	public class ProducerAndConsumer {
	    //商品庫存
	    private static int storeMount = 0;
	    //監(jiān)視器對應(yīng)的對象
	    private static Object monitor = new Object();
	    //生產(chǎn)者線程
	    static class ProducerThread implements Runnable{
	        @Override
	        public void run() {
	            try {
	                produce();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	        public void produce() throws InterruptedException {
	            while(true){
	                synchronized(monitor){
	                    //循環(huán)檢測庫存是否大于0,大于0表示還有商品可以消費(fèi),線程等待消費(fèi)者消費(fèi)商品
	                    while(storeMount > 0){
	                        monitor.wait();
	                    }
	                    //被喚醒后獲取到對象的監(jiān)視器之后執(zhí)行的代碼
	                    System.out.println("Thread "+Thread.currentThread().getName()+" begin produce goods");
	                    //生產(chǎn)商品
	                    storeMount = 1;
	                    //喚醒消費(fèi)者
	                    monitor.notify();
	                    Thread.sleep(1000);
	                }
	            }
	        }
	    }
	    //消費(fèi)者線程
	    static class ConsumerThread implements Runnable{
	        @Override
	        public void run() {
	            try {
	                consume();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	        public void consume() throws InterruptedException {
	            while(true){
	                synchronized (monitor){
	                    //檢測庫存是否不為0,如果不為0,那么有商品可供消費(fèi),否則等待生產(chǎn)者生產(chǎn)商品
	                    while(storeMount == 0){
	                        monitor.wait();
	                    }
	                    //消費(fèi)商品
	                    storeMount = 0;
	                    //喚醒生產(chǎn)者線程
	                    monitor.notify();
	                    System.out.println("Thread "+Thread.currentThread().getName()+" begin consume goods");
	                    Thread.sleep(1000);
	                }
	            }
	        }
	    }
	    public static void main(String[] args){
	        Thread producerThread = new Thread(new ProducerThread());
	        producerThread.setName("producerThread");
	        Thread consumerThread = new Thread(new ConsumerThread());
	        consumerThread.setName("consumerThread");
	        producerThread.start();
	        consumerThread.start();
	    }
	}

執(zhí)行結(jié)果如下圖所示: 線程的實(shí)現(xiàn)方式有哪些

上述代碼示例演示了一個(gè)生產(chǎn)者生產(chǎn)商品和一個(gè)消費(fèi)者消費(fèi)商品的場景,對于一個(gè)生產(chǎn)者多個(gè)消費(fèi)者、多個(gè)生產(chǎn)者一個(gè)消費(fèi)者、多個(gè)生產(chǎn)者多個(gè)消費(fèi)者等場景,只需要將喚醒的方法換為notifyAll()即可,否則,會(huì)出現(xiàn)饑餓現(xiàn)象!

總結(jié)

以上就是本文敘述的所有內(nèi)容,本文首先對于給出Java中線程調(diào)度形式,引出多線程編程中需要解決的線程安全問題,并分析線程安全問題,給出解決線程安全問題的常用手段(加鎖同步),最后,結(jié)合Java內(nèi)置的等待通知機(jī)制,進(jìn)行了樣例代碼的展示以及分析,給出了經(jīng)典的等待通知機(jī)制的編程范式,最后,基于等待通知機(jī)制給出了生產(chǎn)者消費(fèi)者模式的實(shí)現(xiàn)樣例,希望本文能給想要學(xué)習(xí)多線程編程的朋友一點(diǎn)幫助,如有不正確的地方,還望指出,十分感謝!


注意細(xì)節(jié)

  • 線程分類

    • Thread.setDaemon(true)來設(shè)置線程屬性為守護(hù)線程,該操作必須在線程調(diào)用start()方法之前執(zhí)行

    • 守護(hù)線程中的finally代碼塊不一定會(huì)執(zhí)行,因此不要寄托于守護(hù)線程中的finally代碼塊來完成資源的釋放

    • 用戶線程:大多數(shù)線程都是用戶線程,用于完成業(yè)務(wù)功能

    • 守護(hù)線程:支持型線程,主要用于后臺(tái)調(diào)度以及支持性工作,比如GC線程,當(dāng)JVM中不存在非守護(hù)線程時(shí),JVM將會(huì)退出

  • 線程交互的方式

    • join

    • sleep/interrupt

    • wait/notify

  • 啟動(dòng)線程的方式

    • 只能通過線程對象調(diào)用start()方法來啟動(dòng)線程

    • start()方法的含義是,當(dāng)前線程(父線程)同步告知虛擬機(jī),只要線程規(guī)劃期空閑,就應(yīng)該立即啟動(dòng)調(diào)用了start()方法的線程

    • 線程啟動(dòng)前,應(yīng)該設(shè)置線程名,以便使用Jstack分析程序中線程運(yùn)行狀況時(shí),起到提示性作用

  • 終止線程的方式

    • 調(diào)用之后不一定保證線程資源的釋放

    • 調(diào)用后,線程不會(huì)釋放已經(jīng)占有的資源,容易引發(fā)死鎖問題

    • 線程通過調(diào)用目標(biāo)線程的interrupt()方法對目標(biāo)線程進(jìn)行中斷標(biāo)志,目標(biāo)線程通過檢測自身的中斷標(biāo)志位(interrupted()或isInterrupted())來響應(yīng)中斷,進(jìn)行資源的釋放以及最后的終止線程操作;

    • 拋出InterruptedException異常的方法在拋出異常之前,都會(huì)將該線程的中斷標(biāo)志位清除,然后拋出異常

    • 中斷檢測機(jī)制

    • suspend()/resume()(棄用)

    • stop()(棄用)

  • 鎖釋放的情況:

    • 同步方法或同步代碼塊的執(zhí)行結(jié)束(正常、異常結(jié)束)

    • 同步方法或同步代碼塊鎖對象調(diào)用wait方法

  • 鎖不會(huì)釋放的情況:

    • 調(diào)用Thead類的靜態(tài)方法yield()以及sleep()

    • 調(diào)用線程對象的suspend()

感謝各位的閱讀,以上就是“線程的實(shí)現(xiàn)方式有哪些”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對線程的實(shí)現(xiàn)方式有哪些這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI