java線程基礎(chǔ)

前言- CPU競爭策略

操作系統(tǒng)中,CPU競爭有很多種策略。Unix系統(tǒng)使用的是時(shí)間片算法,而Windows則屬于搶占式的。

  1. 在時(shí)間片算法中,所有的進(jìn)程排成一個(gè)隊(duì)列。操作系統(tǒng)按照他們的順序,給每個(gè)進(jìn)程分配一段時(shí)間,即該進(jìn)程允許運(yùn)行的時(shí)間。如果在 時(shí)間片結(jié)束時(shí)進(jìn)程還在運(yùn)行,則CPU將被剝奪并分配給另一個(gè)進(jìn)程。如果進(jìn)程在時(shí)間片結(jié)束前阻塞或結(jié)束,則CPU當(dāng)即進(jìn)行切換。調(diào)度程 序所要做的就是維護(hù)一張就緒進(jìn)程列表,當(dāng)進(jìn)程用完它的時(shí)間片后,它被移到隊(duì)列的末尾。

  2. 搶占式操作系統(tǒng),就是說如果一個(gè)進(jìn)程得到了 CPU 時(shí)間,除非它自己放棄使用 CPU ,否則將完全霸占 CPU 。因此可以看出,在搶 占式操作系統(tǒng)中,操作系統(tǒng)假設(shè)所有的進(jìn)程都是“人品很好”的,會(huì)主動(dòng)退出 CPU 。

在搶占式操作系統(tǒng)中,假設(shè)有若干進(jìn)程,操作系統(tǒng)會(huì)根據(jù)他們的優(yōu)先級、饑餓時(shí)間(已經(jīng)多長時(shí)間沒有使用過 CPU 了),給他們算出一 個(gè)總的優(yōu)先級來。操作系統(tǒng)就會(huì)把 CPU 交給總優(yōu)先級最高的這個(gè)進(jìn)程。當(dāng)進(jìn)程執(zhí)行完畢或者自己主動(dòng)掛起后,操作系統(tǒng)就會(huì)重新計(jì)算一 次所有進(jìn)程的總優(yōu)先級,然后再挑一個(gè)優(yōu)先級最高的把 CPU 控制權(quán)交給他。

1-線程的優(yōu)先級

在java線程中,可以在構(gòu)造線程時(shí)通過setPriority()方法設(shè)定線程的優(yōu)先級,優(yōu)先級為從1-10的整數(shù)(默認(rèn)為5),優(yōu)先級越高系統(tǒng)分配的時(shí)間就越多;這里有一個(gè)設(shè)置優(yōu)先級的一個(gè)常用經(jīng)驗(yàn)知識(shí):對于頻繁阻塞的線程(經(jīng)常休眠,IO操作等)需要設(shè)置較高的優(yōu)先級,因?yàn)檫@些經(jīng)常阻塞的線程即使設(shè)置為較高的優(yōu)先級,但是在大部分時(shí)間里,處于阻塞狀態(tài),會(huì)讓出CPU;而對于偏重計(jì)算的(將會(huì)長時(shí)間獨(dú)占CPU)線程設(shè)置為較低的優(yōu)先級,防止其他線程的不會(huì)長時(shí)間得不到執(zhí)行。

Thread thread = new Thread(job);
thread.setPriority(10);
thread.start();

注意:但是在很多系統(tǒng)下面對線程優(yōu)先級的設(shè)置可能無效(如類unix的分時(shí)系統(tǒng))

2-線程的狀態(tài)

給定一個(gè)時(shí)刻,線程只能處于6種狀態(tài)其中的一種狀態(tài)

  1. NEW:初始狀態(tài),線程被構(gòu)建,但是還沒有調(diào)用start()方法。
  2. RUNNABLE:運(yùn)行狀態(tài),java線程將操作系統(tǒng)中的就緒狀態(tài)和運(yùn)行兩種狀態(tài)籠統(tǒng)的稱作運(yùn)行中。
  3. BLOCKED:阻塞狀態(tài),特指線程阻塞于鎖synchronized關(guān)鍵字修飾的方法或者方法塊),并將該線程加入同步隊(duì)列中。
  4. WAITING:等待狀態(tài),表示線程進(jìn)入等待狀態(tài),進(jìn)入該狀態(tài)表示當(dāng)前線程需要等待其他線程做出一些特定的動(dòng)作(比如通知或者中斷),并將該線程加入等待隊(duì)列中。需要注意的是調(diào)用LockSupport.park()方法和Thread.join()會(huì)使得線程進(jìn)入這個(gè)狀態(tài),而不是阻塞狀態(tài)。
  5. TIME_WAITING:超時(shí)等待狀態(tài),該狀態(tài)是WAITING狀態(tài)的超時(shí)版本,它可以在指定的時(shí)間自行返回。一般由帶有超時(shí)設(shè)置的方法調(diào)用引起。
  6. TERMINATED:終止?fàn)顟B(tài),表示當(dāng)前線程已經(jīng)執(zhí)行完畢。
線程狀態(tài)轉(zhuǎn)移圖,源自《java并發(fā)編程的藝術(shù)》

注意上圖 Object.join()有誤,應(yīng)改成Thread.join()

線程start()和run()方法的區(qū)別
  • thread.start()方法

調(diào)用此方法將會(huì)由操作系統(tǒng)任務(wù)調(diào)度器在新創(chuàng)建的線程中執(zhí)行run()方法,可能不會(huì)立刻執(zhí)行,由任務(wù)調(diào)度器調(diào)度,但是一定是在新創(chuàng)建的線程中執(zhí)行。重復(fù)調(diào)用start方法將拋出異常IllegalThreadStateException

  • thread.run()方法

Thread實(shí)現(xiàn)了Runnable接口,默認(rèn)實(shí)現(xiàn)是調(diào)用target的run方法,調(diào)用此方法并不會(huì)再新創(chuàng)建的線程去執(zhí)行run方法,只會(huì)在調(diào)用Thread.run()方法的線程本地執(zhí)行,和調(diào)用一個(gè)普通對象的一個(gè)方法效果一樣,可以被重復(fù)調(diào)用。

線程方法
  • Thread.sleep(long n)靜態(tài)方法
  1. 當(dāng)n = 0 時(shí),thread 線程主動(dòng)放棄自己CPU控制權(quán),進(jìn)入就緒狀態(tài)。這種情況下只能調(diào)度優(yōu)先級相等或者更高的線程,低優(yōu)先級的線程很有能永遠(yuǎn)得不到執(zhí)行,當(dāng)沒有符合條件的線程時(shí),當(dāng)前會(huì)一直占用CPU,造成CPU滿載。
  1. 當(dāng)n > 0 時(shí),Thread線程將會(huì)被強(qiáng)制放棄CPU控制權(quán),并睡眠n毫秒,進(jìn)入阻塞狀態(tài)。這種情況下所有其他任意優(yōu)先級就緒的線程都有機(jī)會(huì)競爭CPU控制權(quán)。無論有沒有符合的線程,都會(huì)放棄CPU控制權(quán)-,因此CPU占用率較低。
  2. 上述1、2是從線程調(diào)度的角度分析的,無論1、2,都不會(huì)釋放對象的鎖,也就是說如果有synchronized方法塊,其他線程仍然不能訪問共享數(shù)據(jù),該方法拋出中斷異常。
  • thread.join()

使得調(diào)用thread.join()語句的線程等待thread線程的執(zhí)行完畢,才從這個(gè)語句返回,并繼續(xù)這個(gè)線程,該方法也需要捕獲中斷異常。這個(gè)方法含有超時(shí)重載版本

  • Thread.yield()靜態(tài)方法

將thread線程放入就緒隊(duì)列中,而不是同步隊(duì)列,由操作系統(tǒng)去調(diào)度。如果沒有找到其他就緒的線程,則當(dāng)前線程繼續(xù)運(yùn)行,比thread.sleep(0)速度快,只能讓相同優(yōu)先級的線程得以運(yùn)行。

重點(diǎn)分析join方法的實(shí)現(xiàn)


如何實(shí)現(xiàn)join方法的語義?

  1. ** 方法內(nèi)部調(diào)用Object.wait()方法進(jìn)行等待。**
  2. 當(dāng)線程終止時(shí),會(huì)調(diào)用線程自身的notifyAll()方法,通知所有等待在該線程對象監(jiān)視器上的線程。

屬于經(jīng)典的等待/通知模式

例子




解析:假設(shè)有兩個(gè)線程A、B,在B中調(diào)用方法A.join,由于join是同步方法,線程B排他獲取方法所屬的對象監(jiān)視器鎖,即線程對象A的監(jiān)視器鎖;線程B獲取線程A的對象監(jiān)視器鎖成功后,在join方法內(nèi)部,調(diào)用的是this.wait()方法,即在線程B在線程A對象上等待并釋放線程A上的對象監(jiān)視器鎖。

方法內(nèi)部有兩個(gè)循環(huán)判斷:

  1. join(0):Object.wait(0),在第一個(gè)while循環(huán)里始終對線程A是否終止進(jìn)行判斷,如果還在運(yùn)行,則使線程B等待,直到被通知或者中斷,當(dāng)被喚醒時(shí)還得去判斷線程A是否終止,如果終止則在獲取監(jiān)視器鎖后從join方法返回繼續(xù)代碼,否則繼續(xù)等待。
  2. join(millis > 0) : Object.wait(millis)分析方法和上面基本一樣,只不過加了超時(shí)返回,即從wait方法返回時(shí)判斷是否超時(shí),如果超時(shí)則在獲取對象鎖后跳出循環(huán),從join方法返回繼續(xù)執(zhí)行。
對象方法

object.wait(),object.notify(),object.notifyAll()

  1. 這3個(gè)方法在使用之前都要獲取object對象的鎖,即在synchronized(object){ object.wait();}
  2. 調(diào)用wait()方法后,線程狀態(tài)將由running變?yōu)閣aiting,并將當(dāng)前線程放置到等待隊(duì)列中,并釋放object上的鎖。
  3. notify() 和notifyAll()方法調(diào)用后,等待線程依舊不會(huì)從wait方法返回,而是將等待隊(duì)列的一個(gè)或全部的線程移動(dòng)到同步隊(duì)列中,被移動(dòng)的線程狀態(tài)變?yōu)閎locked,然后通知線程從同步方法塊返回,并釋放object上鎖,只有下一次鎖的競爭中,等待線程成功獲取到object上的鎖,才從wait方法返回。

3-線程的創(chuàng)建

提供了三個(gè)方法來創(chuàng)建Thread

  • 繼承Thread類來創(chuàng)建線程類,重寫run()方法作為線程執(zhí)行體。

缺點(diǎn):
線程類繼承了Thread類,無法在繼承其他父類。
因?yàn)槊織l線程都是一個(gè)Thread子類的實(shí)例,因此多個(gè)線程之間共享數(shù)據(jù)比較麻煩。

  • 用實(shí)現(xiàn)了Runnable接口的對象作為target來創(chuàng)建線程對象。

推薦,用來將沒有返回值和不會(huì)拋出異常的方法體run()傳遞給線程去執(zhí)行

  • 用實(shí)現(xiàn)了Callable接口的對象作為task提交給線程池ExecutorService 通過submit方法來提交執(zhí)行

推薦,用來將有返回值和會(huì)拋出異常的方法體run()傳遞給線程去執(zhí)行

4-線程中斷

中斷是一種線程之間通信機(jī)制,但是中斷不像其名字一樣會(huì)讓線程中斷,而是線程通過循環(huán)判斷查看中斷標(biāo)志位,來及時(shí)的查看中斷狀態(tài)并采取下一步的操作

  1. 其他線程通過該線程的interrupt()方法對其進(jìn)行中斷操作。
  2. 線程通過調(diào)用自身的isInterrupted()來進(jìn)行判斷是否被中斷,也可以調(diào)用靜態(tài)方法Thread.interrupted()將清除當(dāng)前線程的中斷標(biāo)志并返回之前線程的中斷狀態(tài);如果該線程已經(jīng)處于終結(jié)狀態(tài),無論是否中斷,則調(diào)用該對象的isInterrupted()都將返回false。
  3. 拋出InterruptedException異常的方法,比如Thread.sleep(),這些方法在拋出異常之前,Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)志位清除,然后再拋出InterruptedException異常,這時(shí)在調(diào)用isInterrupted()方法進(jìn)行判斷將返回false。

5-等待/通知的經(jīng)典線程間通信范式

  • 等待方遵循如下原則
  1. 獲取對象的鎖。
  1. 如果條件不滿足,那么調(diào)用對象的wait()方法,被通知后還要檢查條件。
  2. 條件滿足則執(zhí)行對應(yīng)的邏輯 。
synchronized(object對象){
        while(條件不滿足){
              object.wait();
        }
        對應(yīng)的處理邏輯;
}
  • 通知方遵循如下原則
  1. 獲取對象的鎖。
  1. 改變條件
  2. 通知所有等待在對象上的線程
synchronized(object對象){
        改變條件
        object.notifyAll();
}
6-ThreadLocal

線程本地變量,是一個(gè)以TreadLocal變量為鍵,任意對象為值的存儲(chǔ)結(jié)構(gòu),將變量與線程綁定在一起,為每一個(gè)線程維護(hù)一個(gè)獨(dú)立的變量副本,ThreadLocal將變量的范圍限制在一個(gè)線程的上下文當(dāng)中,使得變量的作用域?yàn)榫€程級別。

  1. ThreadLocal僅僅是個(gè)變量訪問的入口;
  2. 每一個(gè)Thread對象都有一個(gè)ThreadLocalMap對象,這個(gè)ThreadLocalMap持有所有已經(jīng)初始化的ThreadLocal值對象的引用;
  3. 只有在線程中調(diào)用ThreadLocal的set(),或者get()方法時(shí)都會(huì)在當(dāng)前線程中綁定這個(gè)變量,否則不會(huì)綁定。第一次get()方法調(diào)用將會(huì)進(jìn)行初始化(如果set方法沒有調(diào)用過),而且初始化每個(gè)線程值進(jìn)行一次。
  4. 初始化方法

允許對默認(rèn)初始化方法進(jìn)行重寫

// 默認(rèn)初始化方法
protected T initialValue(){
        return null;
}

ThreadLocal源碼分析

  1. set()
// ThreadLocal.java
public void set(T value) {
   //1.首先獲取當(dāng)前線程對象
   Thread t = Thread.currentThread();
   //2.獲取該線程對象的ThreadLocalMap
       ThreadLocalMap map = getMap(t);
       //如果map不為空,執(zhí)行set操作,以當(dāng)前threadLocal對象為key
       //實(shí)際存儲(chǔ)對象為value進(jìn)行set操作
       if (map != null)
           map.set(this, value);
       //如果map為空,則為該線程創(chuàng)建ThreadLocalMap
       else
           createMap(t, value);
   }
ThreadLocalMap getMap(Thread t) {
   //線程對象持有ThreadLocalMap的引用
   return t.threadLocals;
}
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
  1. get()
public T get() {
    //1.首先獲取當(dāng)前線程
    Thread t = Thread.currentThread();
    //2.獲取線程的map對象
    ThreadLocalMap map = getMap(t);
    //3.如果map不為空,以threadlocal實(shí)例為key獲取到對應(yīng)Entry,然后從Entry中取出對象即可。
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    //如果map為空,也就是第一次沒有調(diào)用set直接get
    //(或者調(diào)用過set,又調(diào)用了remove)時(shí),為其設(shè)定初始值
    return setInitialValue();
 }
private T setInitialValue() {
    T value = initialValue();//獲取初始值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

場景一:為每一個(gè)線程分配一個(gè)遞增無重復(fù)的ID

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalDemo {
    public static void main(String []args){
        for(int i=0;i<5;i++){
            final Thread t = new Thread(){
                @Override
                public void run(){
                    System.out.println("當(dāng)前線程:"+Thread.currentThread().getName()
                        +",已分配ID:"+ThreadId.get());
                }
            };
            t.start();
        }
    }
    static class ThreadId{
        //一個(gè)遞增的序列,使用AtomicInger原子變量保證線程安全
        private static final AtomicInteger nextId = new AtomicInteger(0);
        //線程本地變量,為每個(gè)線程關(guān)聯(lián)一個(gè)唯一的序號
        private static final ThreadLocal<Integer> threadId =
                new ThreadLocal<Integer>() {
                    @Override
                    protected Integer initialValue() {
                        //相當(dāng)于nextId++,由于nextId++這種操作是個(gè)復(fù)合操作而非原子操作,
                        //會(huì)有線程安全問題(可能在初始化時(shí)就獲取到相同的ID,所以使用原子變量
                        return nextId.getAndIncrement();
                    }
                };

       //返回當(dāng)前線程的唯一的序列,如果第一次get,會(huì)先調(diào)用initialValue,后面看源碼就了解了
        public static int get() {
            return threadId.get();
        }
    }
}

說明:ThreadID是線程共享的,所以需要原子類來保證線程訪問的安全性,而ThreadID的成員變量threadId是線程封閉的,只是線程本地變量初始化時(shí)需要訪問原子類(多個(gè)線程同時(shí)訪問引起 )

場景二:web開發(fā)中,為每一個(gè)連接創(chuàng)建一個(gè)ThreadLocal保存session信息,如果web服務(wù)器使用線程池技術(shù)(比如Tomcat)進(jìn)行線程復(fù)用,則每一次連接都要重新的set,以保證session為本次連接的信息。當(dāng)session結(jié)束,調(diào)用remove方法,將線程本地變量從線程的ThreadLocalMap中移除。

7-等待超時(shí)

主要學(xué)習(xí)的是剩余時(shí)間的計(jì)算

等待超時(shí)模式的經(jīng)典模式

// 同步方法
public synchronized Object get(long millss) throws InterruptedException {
    // 獲取將來時(shí)間點(diǎn)
    long future = System.currentTimeMillis)() + millis;
    // 初始化剩余時(shí)間為millis,從這可以看出超時(shí)等待時(shí)間并不是十分的嚴(yán)格
    long remaining = millis;

    // 超時(shí)等待判斷,當(dāng)返回值的結(jié)果不滿足并且剩余時(shí)間小于0時(shí),從循環(huán)退出
    while((result == null) && remaining > 0){
        wait(remaining);
        remaining = future - System.currentTimeMillis();
    }
    return result;
}

參考鏈接
https://hacpai.com/article/1488015279637

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 線程的狀態(tài) 新建狀態(tài):用new語句創(chuàng)建的線程對象處于新建狀態(tài),此時(shí)它和其它的java對象一樣,僅僅在堆中被分配了內(nèi)...
    稻田上的稻草人閱讀 636評論 0 1
  • java線程基礎(chǔ) 線程和進(jìn)程進(jìn)程 : 進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。進(jìn)程由程序、數(shù)據(jù)和進(jìn)程控制塊三部...
    你好667閱讀 244評論 0 0
  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的,為什么轉(zhuǎn)載兩個(gè)字加“”呢?因?yàn)檫@絕不是簡單的復(fù)制粘貼,我花了五六個(gè)小...
    SmartSean閱讀 4,951評論 12 45
  • 本文主要講了java中多線程的使用方法、線程同步、線程數(shù)據(jù)傳遞、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等。 首先講...
    李欣陽閱讀 2,599評論 1 15
  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 3,111評論 1 18

友情鏈接更多精彩內(nèi)容