Java 基礎(chǔ) —— 多線程(讀書筆記)「一」

多線程對于 Android 開發(fā)者來說是基礎(chǔ)。而且這類知識在計算機(jī)里也是很重要的一環(huán)音比,所以很有必要整理一番俭尖。

目錄

多線程的實現(xiàn)

來上代碼:

// 最常見的兩種方法啟動新的線程
public static void startThread() {
    // 覆蓋 run 方法
    new Thread() {
        @Override
        public void run() {
            // 耗時操作
        }
    }.start();

    // 傳入 Runnable 對象
    new Thread(new Runnable() {
        public void run() {
            // 耗時操作
        }
    }).start();
}

其實第一個就是在 Thread 里覆寫了 run() 函數(shù),第二個是給 Thread 傳了一個 Runnable 對象,在 Runnable 對象 run() 方法里進(jìn)行耗時操作稽犁。
以前沒有怎么考慮過他們兩者的關(guān)系焰望,今天我們來具體看看到底是什么鬼?

Thread 源碼

進(jìn)入 Thread 源碼我們看看:

public class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;


    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
    }
}

源碼很長已亥,我進(jìn)行了一點分割熊赖。一點一點的來解析看看。
我們首先知道 Thread 也是一個 Runnable 虑椎,它實現(xiàn)了 Runnable 接口震鹉,并且在 Thread 類中有一個 Runnable 類型的 target 對象。

構(gòu)造方法里我們都會調(diào)用 init() 方法捆姜,接下來看看在該方法里做了如何的初始化配置传趾。

    private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {

    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    // group 參數(shù)如果為 null ,則獲得當(dāng)前線程的 group(線程組)
    if (g == null) {
            g = parent.getThreadGroup();
    }
    // 代碼省略

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    // 設(shè)置 target( Runnable 類型 )
    this.target = target;

    }


    public synchronized void start() {

    // 將當(dāng)前線程加入線程組
    group.add(this);

    boolean started = false;
    try {
        // 啟動 native 方法啟動新的線程
        start0();
        started = true;
    } finally {
        // 代碼省略
    }

   private native void start0();



   @Override
   public void run() {
       if (target != null) {
        target.run();
       }
    }

從上我們可以明白泥技,最終被線程執(zhí)行的任務(wù)是 Runnable 浆兰,Thread 只是對 Runnable 的一個包裝,并且通過一些狀態(tài)對 Thread 進(jìn)行管理和調(diào)度珊豹。
當(dāng)啟動一個線程時簸呈,如果 Thread 的 target 不為空,則會在子線程中執(zhí)行這個 target 的 run() 函數(shù)店茶,否則虛擬機(jī)就會執(zhí)行該線程自身的 run() 函數(shù)蜕便。

線程的幾個重要的函數(shù)
  • wait()
    當(dāng)一個線程執(zhí)行到 wait() 方法時,它就進(jìn)入到一個和該對象相關(guān)的等待池中贩幻,同時失去(釋放)了對象的機(jī)鎖轿腺,使得其他線程可以訪問。用戶可以使用 notify 段直、notifyAll 或者指定睡眠時間來喚醒當(dāng)前等待池中的線程吃溅。
    注意:wait() notify() notifyAll() 必須放在 synchronized block 中,否則會拋出異常鸯檬。
  • sleep()
    該函數(shù)是 Thread 的靜態(tài)函數(shù)决侈,作用是使調(diào)用線程進(jìn)入睡眠狀態(tài)。因為 sleep() 是 Thread 類的靜態(tài)方法喧务,因此他不能改變對象的機(jī)鎖赖歌。所以,當(dāng)在一個 synchronized 塊中調(diào)用 sleep() 方法時功茴,線程雖然休眠了庐冯,但是對象的機(jī)鎖并沒有被釋放,其他線程無法訪問這個對象坎穿。
  • join()
    等待目標(biāo)線程執(zhí)行完成之后繼續(xù)執(zhí)行展父。
  • yield()
    線程禮讓返劲。目前線程由運(yùn)行狀態(tài)轉(zhuǎn)換為就緒狀態(tài),也就是讓出執(zhí)行權(quán)限栖茉,讓其他線程得以優(yōu)先執(zhí)行篮绿,但其他線程能否優(yōu)先執(zhí)行未知。

在源碼中吕漂,查看 Thread 里的 State 亲配,對幾種狀態(tài)解釋的很清楚。

NEW 狀態(tài)是指線程剛創(chuàng)建惶凝,尚未啟動

RUNNABLE 狀態(tài)是線程正在正常運(yùn)行中吼虎,當(dāng)然可能會有某種耗時計算 / IO 等待的操作 / CPU 時間片切換等, 這個狀態(tài)下發(fā)生的等待一般是其他系統(tǒng)資源, 而不是鎖, Sleep 等

BLOCKED 這個狀態(tài)下,是在多個線程有同步操作的場景, 比如正在等待另一個線程的 synchronized 塊的執(zhí)行釋放苍鲜,或者可重入的 synchronized 塊里別人調(diào)用 wait() 方法思灰,也就是這時線程在等待進(jìn)入臨界區(qū)

WAITING 這個狀態(tài)下是指線程擁有了某個鎖之后,調(diào)用了他的 wait 方法坡贺,等待其他線程 / 鎖擁有者調(diào)用 notify / notifyAll 一遍該線程可以繼續(xù)下一步操作官辈,這里要區(qū)分 BLOCKED 和 WATING 箱舞,一個是在臨界點外面等待進(jìn)入遍坟, 一個是在臨界點里面 wait 等待別人 notify , 線程調(diào)用了 join 方法 進(jìn)入另外的線程的時候, 也會進(jìn)入 WAITING 狀態(tài)晴股,等待被他 join 的線程執(zhí)行結(jié)束

TIMED_WAITING 這個狀態(tài)就是有限的 (時間限制) 的 WAITING愿伴, 一般出現(xiàn)在調(diào)用 wait(long), join(long) 等情況下,另外电湘,一個線程 sleep 后, 也會進(jìn)入 TIMED_WAITING 狀態(tài)

TERMINATED 這個狀態(tài)下表示 該線程的 run 方法已經(jīng)執(zhí)行完畢了, 基本上就等于死亡了 (當(dāng)時如果線程被持久持有, 可能不會被回收)

Wait() 的實踐

我們來看一段隔节,wait() 的用途和效果。

    static void waitAndNotifyAll() {

        System.out.println("主線程運(yùn)行");

        Thread thread = new WaitThread();
        thread.start();
        long startTime = System.currentTimeMillis();
        try {
            synchronized (sLockOject) {
                System.out.println("主線程等待");
                sLockOject.wait();
            }
        } catch (Exception e) {
        }

        long timeMs = System.currentTimeMillis() - startTime;
        System.out.println("主線程繼續(xù) —-> 等待耗時:" + timeMs + " ms");

    }

    static class WaitThread extends Thread {

        @Override
        public void run() {
            try {
                synchronized (sLockOject) {
                    System.out.println("進(jìn)入子線程");
                    Thread.sleep(3000);
                    System.out.println("喚醒主線程");
                    sLockOject.notifyAll();
                }
            } catch (Exception e) {
            }
        }

    }

waitAndNotifyAll() 函數(shù)里寂呛,會啟動一個 WaitThread 線程怎诫,在該線程中將會調(diào)用 sleep 函數(shù)睡眠 3 秒。線程啟動之后在主線程調(diào)用 sLockOject 的 wait() 函數(shù)贷痪,使主線程進(jìn)入等待狀態(tài)幻妓,此時將不會繼續(xù)執(zhí)行。等 WaitThread 在 run() 函數(shù)沉睡了 3 秒后會調(diào)用 sLockOject 的 notifyAll() 函數(shù)劫拢,此時就會重新喚醒正在等待中的主線程肉津,因此會繼續(xù)往下執(zhí)行。

結(jié)果如下:

主線程運(yùn)行
主線程等待
進(jìn)入子線程
喚醒主線程
主線程繼續(xù) —-> 等待耗時:3005 ms

wait()舱沧、notify() 機(jī)制通常用于等待機(jī)制的實現(xiàn)妹沙,當(dāng)條件未滿足時調(diào)用 wait 進(jìn)入等待狀態(tài),一旦條件滿足熟吏,調(diào)用 notifynotifyAll 喚醒等待的線程繼續(xù)執(zhí)行距糖。

對于這里細(xì)節(jié)可能會有一些疑問玄窝。</br>

在子線程啟動的時候,run() 函數(shù)里面已經(jīng)持有了該對象鎖悍引。</br>

但是真實環(huán)境下哆料,其實是主線程先持有對象鎖,然后調(diào)用 wait() 進(jìn)入等待區(qū)并且釋放鎖等待喚醒吗铐。

這個問題涉及到 JNI 代碼东亦,目前我只能從理論上來解釋這個問題。
我們都知道一個線程 start() 并不是馬上啟動唬渗,而是需要 CPU 分配資源的典阵,根據(jù)目前運(yùn)行來看,分配資源的時間大于 Java 虛擬機(jī)運(yùn)行指令的時間镊逝,所以主線程比子線程先拿到鎖壮啊。
我們還可以知道一點,控制臺打印出的時間是 3005 ms 撑蒜,在代碼里我們只等待了 3s 多出來的 5ms (這個數(shù)字會浮動)我們可以推斷是歹啼,子線程獲取 CPU 的時間加上喚醒主線程的時間。

上述只是自己的一個猜測座菠,能力還有欠缺狸眼,準(zhǔn)備深入學(xué)習(xí)。

不過推薦大家看看這篇文章 Synchnornized 在 JVM 下的實現(xiàn) - 簡書浴滴。

Join() 的實踐

join() 的注釋上面寫著:

Waits for this thread to die.

意思是拓萌,阻塞當(dāng)前調(diào)用 join() 函數(shù)所在的線程,直到接收線程執(zhí)行完畢之后再繼續(xù)升略。
我們來看看實踐代碼:

public class JoinThread {

    public static void main(String[] args) {
        joinDemo();
    }

    public static void joinDemo() {
        Worker worker1 = new Worker("work-1");
        Worker worker2 = new Worker("work-2");
        worker1.start();
        System.out.println("啟動線程 1 ");

        try {
            // 調(diào)用 worker1 的 join 函數(shù)微王,主線,程會阻塞直到 woker1 執(zhí)行完成
            worker1.join();
            System.out.println("啟動線程 2");
            // 再啟動線程 2 品嚣,并且調(diào)用線程 2 的 join 函數(shù)炕倘,主線程會阻塞直到 woker2 執(zhí)行完成
            worker2.start();
            worker2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主線程繼續(xù)執(zhí)行");
    }

    static class Worker extends Thread {
        public Worker(String name) {
            super(name);
        }

        @Override
        public void run() {

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {

                e.printStackTrace();
            }

            System.out.println("work in " + getName());

        }
    }
}

運(yùn)行之后我們得到:

啟動線程 1
work in work-1
啟動線程 2
work in work-2
主線程繼續(xù)執(zhí)行

joinDemo() 方法里我們創(chuàng)建兩個子線程,然后啟動了 work1 線程翰撑,下一步調(diào)用了 woker1 的 join() 函數(shù)罩旋。此時,主線程會進(jìn)入阻塞狀態(tài)额嘿,直到 work1 執(zhí)行完畢之后才開始繼續(xù)執(zhí)行瘸恼。因為 Worker 的 run() 方法里會休眠 2 秒,因此線程每次調(diào)用了 join() 方法實際上都會阻塞 2 秒册养,直到 run() 方法執(zhí)行完畢再繼續(xù)东帅。
所以,上述代碼邏輯其實就是:

啟動線程1 —-> 等待線程 1 執(zhí)行完畢 —-> 啟動線程2 —-> 等待線程 2 執(zhí)行完畢 —-> 繼續(xù)執(zhí)行主線程代碼

Yield() 的實踐

yield() 是 Thread 的靜態(tài)方法球拦,注釋上說:

A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

大致意思是說:當(dāng)前線程讓出執(zhí)行時間給其他的線程靠闭。
我們都知道帐我,線程的執(zhí)行是有時間片的,每個線程輪流占用 CPU 固定時間愧膀,執(zhí)行周期到了之后讓出執(zhí)行權(quán)給其他線程拦键。
yield() 就是主動讓出執(zhí)行權(quán)給其他線程。

來看看我們實踐的代碼:

public class YieldThreadTest {

    public static void main(String[] args) {
        YieldTread t1 = new YieldTread("thread-1");
        YieldTread t2 = new YieldTread("thread-2");
        t1.start();
        t2.start();
    }

    public static class YieldTread extends Thread {

        public YieldTread(String name) {
            super(name);
        }

        public synchronized void run() {
            for (int i = 0; i < 5; i++) {
                System.out.printf("%s 優(yōu)先級為 [%d] -------> %d\n", this.getName(), this.getPriority(), i);
                // 當(dāng) i 為 2 時檩淋,調(diào)用當(dāng)前線程的 yield 函數(shù)
                if (i == 2) {
                    Thread.yield();

                }
            }
        }

    }

}

main() 方法里創(chuàng)建了兩個 YieldTread 線程芬为,控制臺輸出結(jié)果如下:

thread-1 優(yōu)先級為 [5] -------> 0
thread-1 優(yōu)先級為 [5] -------> 1
thread-1 優(yōu)先級為 [5] -------> 2

thread-2 優(yōu)先級為 [5] -------> 0
thread-2 優(yōu)先級為 [5] -------> 1
thread-2 優(yōu)先級為 [5] -------> 2

thread-1 優(yōu)先級為 [5] -------> 3
thread-1 優(yōu)先級為 [5] -------> 4
thread-2 優(yōu)先級為 [5] -------> 3
thread-2 優(yōu)先級為 [5] -------> 4

通常情況下 t1 首先執(zhí)行,讓 t1 的 run() 函數(shù)執(zhí)行到了 i 等于 2 時讓出當(dāng)前線程的執(zhí)行時間蟀悦。所以我們看到前三行都是 t1 在執(zhí)行媚朦,讓出執(zhí)行時間后 t2 開始執(zhí)行。后面邏輯簡單思考下就得知了日戈,這里也不做過多詮釋奉狈。

因此括饶,調(diào)用 yield() 就是讓出當(dāng)前線程的執(zhí)行權(quán),這樣一來讓其他線程得到優(yōu)先執(zhí)行醉箕。

總結(jié)與參考

本章內(nèi)容屬于線程的基礎(chǔ)押框,本系列會更新到線程池相關(guān)冻记。
這章內(nèi)容也及其重要匀们,因為它是后面的基礎(chǔ)献汗。
正確理解才能讓我們對各種線程問題有方向和思路。

參考讀物

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末季俩,一起剝皮案震驚了整個濱河市钮糖,隨后出現(xiàn)的幾起案子梅掠,更是在濱河造成了極大的恐慌酌住,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阎抒,死亡現(xiàn)場離奇詭異酪我,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)且叁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門都哭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逞带,你說我怎么就攤上這事欺矫。” “怎么了展氓?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵穆趴,是天一觀的道長。 經(jīng)常有香客問我遇汞,道長未妹,這世上最難降的妖魔是什么簿废? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮络它,結(jié)果婚禮上族檬,老公的妹妹穿的比我還像新娘。我一直安慰自己化戳,他們只是感情好单料,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著点楼,像睡著了一般看尼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盟步,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天藏斩,我揣著相機(jī)與錄音,去河邊找鬼却盘。 笑死狰域,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的黄橘。 我是一名探鬼主播兆览,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼塞关!你這毒婦竟也來了抬探?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤帆赢,失蹤者是張志新(化名)和其女友劉穎小压,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體椰于,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡怠益,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瘾婿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜻牢。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖偏陪,靈堂內(nèi)的尸體忽然破棺而出抢呆,到底是詐尸還是另有隱情,我是刑警寧澤笛谦,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布抱虐,位于F島的核電站,受9級特大地震影響揪罕,放射性物質(zhì)發(fā)生泄漏梯码。R本人自食惡果不足惜宝泵,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望轩娶。 院中可真熱鬧儿奶,春花似錦、人聲如沸鳄抒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽许溅。三九已至瓤鼻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贤重,已是汗流浹背茬祷。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留并蝗,地道東北人祭犯。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像滚停,于是被迫代替她去往敵國和親沃粗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容