Java多線程詳細介紹

線程是程序執(zhí)行的最小單元,多線程是指程序同一時間可以有多個執(zhí)行單元運行(這個與你的CPU核心有關)莉恼。
在java中開啟一個新線程非常簡單妨托,創(chuàng)建一個Thread對象陈轿,然后調(diào)用它的start方法,一個新線程就開啟了链沼。

那么執(zhí)行代碼放在那里呢默赂?有兩種方式:1. 創(chuàng)建Thread對象時,復寫它的run方法忆植,把執(zhí)行代碼放在run方法里放可。2. 創(chuàng)建Thread對象時,給它傳遞一個Runnable對象朝刊,把執(zhí)行代碼放在Runnable對象的run方法里。

如果多線程操作的是不同資源蜈缤,線程之間不會相互影響拾氓,不會產(chǎn)生任何問題。但是如果多線程操作相同資源(共享變量)底哥,就會產(chǎn)生多線程沖突咙鞍,要知道這些沖突產(chǎn)生的原因房官,就要先了解java內(nèi)存模型(簡稱JMM)。

一. java內(nèi)存模型(JMM)

1.1 java內(nèi)存模型(JMM)介紹

java內(nèi)存模型決定一個線程對共享變量的寫入何時對另一個線程可見续滋。從抽樣的角度來說:線程之間的共享變量存儲在主內(nèi)存(main memory)中翰守,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本疲酌。

  1. 存在兩種內(nèi)存:主內(nèi)存和線程本地內(nèi)存蜡峰,線程開始時,會復制一份共享變量的副本放在本地內(nèi)存中朗恳。
  2. 線程對共享變量操作其實都是操作線程本地內(nèi)存中的副本變量湿颅,當副本變量發(fā)生改變時,線程會將它刷新到主內(nèi)存中(并不一定立即刷新粥诫,何時刷新由線程自己控制)油航。
  3. 當主內(nèi)存中變量發(fā)生改變,就會通知發(fā)出信號通知其他線程將該變量的緩存行置為無效狀態(tài)怀浆,因此當其他線程從本地內(nèi)存讀取這個變量時谊囚,發(fā)現(xiàn)這個變量已經(jīng)無效了,那么它就會從內(nèi)存重新讀取执赡。

1.2 可見性

從上面的介紹中镰踏,我們看出多線程操作共享變量,會產(chǎn)生一個問題搀玖,那就是可見性問題: 即一個線程對共享變量修改余境,對另一個線程來說并不是立即可見的。

   class Data {
    int a = 0;
    int b = 0;
    int x = 0;
    int y = 0;


    // a線程執(zhí)行
    public void threadA() {
        a = 1;
        x = b;
    }

    // b線程執(zhí)行
    public void threadB() {
        b = 2; 
        y = a; 
    }
}

如果有兩個線程同時分別執(zhí)行了threadA和threadB方法灌诅》祭矗可能會出現(xiàn)x==y==0這個情況(當然這個情況比較少的出現(xiàn))。
因為a和b被賦值后猜拾,還沒有刷新到主內(nèi)存中即舌,就執(zhí)行x = b和y = a的語句,這個時候線程并不知道a和b還已經(jīng)被修改了挎袜,依然是原來的值0顽聂。

1.3 有序性

為了提高程序執(zhí)行性能,Java內(nèi)存模型允許編譯器和處理器對指令進行重排序盯仪。重排序過程不會影響到單線程程序的執(zhí)行紊搪,卻會影響到多線程并發(fā)執(zhí)行的正確性。

class Reorder {
    int x = 0;
    boolean flag = false;

    public void writer() {
        x = 1;
        flag = true;
    }

    public void reader() {
        if (flag) {
            int a = x * x;
            ...
        }

    }
}

例如上例中全景,我們使用flag變量耀石,標志x變量已經(jīng)被賦值了。但是這兩個語句之間沒有數(shù)據(jù)依賴爸黄,所以它們可能會被重排序滞伟,即flag = true語句會在x = 1語句之前揭鳞,那么這么更改會不會產(chǎn)生問題呢?

  1. 在單線程模式下梆奈,不會有任何問題野崇,因為writer方法是一個整體,只有等writer方法執(zhí)行完畢亩钟,其他方法才能執(zhí)行乓梨,所以flag = true語句和x = 1語句順序改變沒有任何影響。
  2. 在多線程模式下径荔,就可能會產(chǎn)生問題督禽,因為writer方法還沒有執(zhí)行完畢,reader方法就被另一線程調(diào)用了总处,這個時候如果flag = true語句和x = 1語句順序改變狈惫,就有可能產(chǎn)生flag為true,但是x還沒有賦值情況鹦马,與程序意圖產(chǎn)生不一樣胧谈,就會產(chǎn)生意想不到的問題荸频。

1.4 原子性

在Java中旭从,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作稳强,即這些操作是不可被中斷的和悦,要么執(zhí)行,要么不執(zhí)行鸽素。

        x = 1;  // 原子性
        y = x; // 不是原子性
        x = x + 1; // 不是原子性
        x++; // 不是原子性
       System.out.println(x); // 原子性

公式2:有兩個原子性操作褒繁,讀取x的值,賦值給y棒坏。公式3:也是三個原子性操作遭笋,讀取x的值瓦呼,加1,賦值給x谎替。公式4:和公式3一樣钱贯。

所以對于原子性操作就兩種:1. 將基本數(shù)據(jù)類型常量賦值給變量侦另。2. 讀取基本數(shù)據(jù)類型的變量值褒傅。任何計算操作都不是原子的殿托。

1.5 小結

多線程操作共享變量,會產(chǎn)生上面三個問題旋廷,可見性饶碘、有序性和原子性馒吴。

  1. 可見性: 一個線程改變共享變量饮戳,可能并沒有立即刷新到主內(nèi)存,這個時候另一個線程讀取共享變量鬼吵,就是改變之前的值齿椅。所以這個共享變量的改變對其他線程并不是可見的涣脚。
  2. 有序性: 編譯器和處理器會對指令進行重排序寥茫,語句的順序發(fā)生改變,這樣在多線程的情況下险耀,可能出現(xiàn)奇怪的異常玖喘。
  3. 原子性: 只有對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作累奈。

要解決這三個問題有兩種方式:

  1. volatile關鍵字:它只能解決兩個問題可見性和有序性問題澎媒,但是如果volatile修飾基本數(shù)據(jù)類型變量,而且這個變量只做讀取和賦值操作请敦,那么也沒有原子性問題了冬三。比如說用它來修飾boolean的變量勾笆。
  2. 加鎖:可以保證同一時間只有同一線程操作共享變量桥滨,當前線程操作共享變量時齐媒,共享變量不會被別的線程修改喻括,所以可見性、有序性和原子性問題都得到解決望蜡。分為synchronized同步鎖和JUC框架下的Lock鎖脖律。

二. volatile關鍵字

看過volatile關鍵字底層實現(xiàn)就知道
我們使用volatile關鍵字修飾變量小泉,就相當于給這個變量添加加了內(nèi)存屏障。那么內(nèi)存屏障的作用是什么呢让簿?

  1. 它會讓本地內(nèi)存共享變量副本無效,即修改了這個共享變量,它會被強制刷新到主內(nèi)存界逛。讀取這個共享變量息拜,會強制從主內(nèi)存中讀取最新值净响。因此解決了可見性問題馋贤。
  2. 禁止指令重排序,即在程序中在volatile變量進行操作時配乓,在其之前的操作肯定已經(jīng)全部執(zhí)行了,而且結果已經(jīng)對后面的操作可見崎页,在其之后的操作肯定還沒有執(zhí)行飒焦。因此解決了有序性問題牺荠。
    這個的具體解釋志电,大家請看《深入理解Java內(nèi)存模型》里面關于happens-before規(guī)則的講解蛔趴。
class VolatileFeaturesExample {

    //使用volatile聲明一個基本數(shù)據(jù)類型變量vl
    volatile long vl = 0L;

    //對于單個volatile基本數(shù)據(jù)類型變量賦值
    public void set(long l) {
        vl = l;
    }

    //對于單個volatile基本數(shù)據(jù)類型變量的復合操作
    public void getAndIncrement () {
        vl++;
    }

    //對于單個volatile基本數(shù)據(jù)類型變量讀取
    public long get() {
        return vl;
    }

}

class VolatileFeaturesExample {
    //聲明一個基本數(shù)據(jù)類型變量vl
    long vl = 0L;

    // 相當于加了同步鎖
    public synchronized void set(long l) {
      vl = l;
    }

    // 普通方法
    public void getAndIncrement () {
        long temp = get();
        temp += 1L;
        set(temp);
    }

    // 相當于加了同步鎖
    public synchronized long get() {
        return vl;
    }

}

如果volatile修飾基本數(shù)據(jù)類型變量鱼蝉,而且只對這個變量做讀取和賦值操作,那么就相當于加了同步鎖渔隶。

三. synchronized同步鎖

synchronized同步鎖作用是訪問被鎖住的資源時间唉,只要獲取鎖的線程才能操作被鎖住的資源呈野,其他線程必須阻塞等待印叁。
所以一個線程來說轮蜕,可以阻塞等待跃洛,可以運行税课,那么線程到底有哪些狀態(tài)呢韩玩?

3.1 線程狀態(tài)

在Thread類中,有一個枚舉對象State標志著所有的線程狀態(tài)合愈。

// 標志線程狀態(tài)的枚舉對象
public enum State {
    /**
     * 新建狀態(tài)佛析。當創(chuàng)建一個線程Thread對象寸莫,但是還沒有調(diào)用它的start方法膘茎,就是這個狀態(tài)。
      */
    NEW,

    /**
     * 運行狀態(tài)态坦。當前線程正在運行中
      */
    RUNNABLE,

    /**
     * 阻塞狀態(tài)伞梯。
     * 一般是鎖資源被另一線程持有谜诫,當前線程處于阻塞等待獲取鎖的狀態(tài)猜绣,
     * 當線程獲取了鎖,并獲取CPU執(zhí)行權牺陶,就會從BLOCKED狀態(tài)轉成RUNNABLE狀態(tài)掰伸。
     *
      */
    BLOCKED,

    /**
     * 等待狀態(tài)狮鸭。調(diào)用三個方法當前線程會進人這個狀態(tài):
     * 1. Object#wait() 方法
     * 2. #join() 方法 (這個方法在Thread對象中歧蕉,本質(zhì)上也是調(diào)用wait()方法)
     * 3. LockSupport#park() 方法
     * 這三個方法調(diào)用時都沒有傳遞時間參數(shù)惯退,所以沒有超時限制催跪。
     * WAITING狀態(tài)的線程是處于線程等待池中懊蒸,只有調(diào)用對應的喚醒方法,才能將當前線程從線程等待池中喚醒,
     * 否則線程一直等待舌仍。除非發(fā)生中斷請求抡笼,也會將線程喚醒推姻。
     * 喚醒線程的方法有:
     * 1. Object#notify() notifyAll()
     * 2. LockSupport#unpark()
     * 注意join()是線程對象的wait()方法實現(xiàn)的藏古,當線程執(zhí)行完畢時拧晕,會調(diào)用自己的notifyAll()方法厂捞,
     * 喚醒等待池中所有的線程靡馁。
     *
     * 還有要注意的是Object#wait() 方法只能在synchronized代碼塊中調(diào)用,
     * 所以當線程被喚醒時赔嚎,它并不是處于可運行狀態(tài)尤误,而是處于BLOCKED狀態(tài)损晤,
     * 因為只有獲取鎖的線程沉馆,才能執(zhí)行synchronized代碼塊中的代碼斥黑,所以被喚醒的線程要等待鎖锌奴。
     *
     * 而LockSupport#park()沒有這個方面的限制
     *
     */
    WAITING,


    /**
     * 等待超時狀態(tài)鹿蜀,調(diào)用下面五個方法當前線程會進人這個狀態(tài):
     * 1. Object#wait(long)
     * 2. #join(long) Thread.join茴恰,就是使用wait方法實現(xiàn)的往枣。
     * 3. LockSupport#parkNanos
     * 4. LockSupport#parkUntil
     * 5. Thread#sleep
     *
     * 與WAITING狀態(tài)相比較分冈,當線程處于線程等待池中雕沉,如果沒有調(diào)用對應的喚醒方法,
     * 但是超出規(guī)定時間扰路,那么線程自動會被喚醒幼衰。所以就是多出了一種喚醒方式。
     * 注意Thread#sleep 沒有對應的喚醒方法肥印。
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    // 終止狀態(tài)深碱,當線程運行完畢時敷硅,就處于這個狀態(tài)绞蹦,而且該狀態(tài)不能再轉換成其他狀態(tài)幽七。
    TERMINATED;
}

線程一共有六種狀態(tài):

  1. NEW: 新建狀態(tài)澡屡。當創(chuàng)建一個線程Thread對象驶鹉,但是還沒有調(diào)用它的start方法室埋,就是這個狀態(tài)词顾。
  2. RUNNABLE: 運行狀態(tài)肉盹。當前線程正在運行中上忍。
  3. BLOCKED: 阻塞狀態(tài)窍蓝。當前線程正在等待鎖資源吓笙。
  4. WAITING: 等待狀態(tài)面睛。當前線程處于線程等待池中叁鉴,需要被喚醒幌墓。
  5. TIMED_WAITING: 等待超時狀態(tài)常侣。與WAITING狀態(tài)相比袭祟,多了一種超時會被自動喚醒的方法。
  6. TERMINATED: 終止狀態(tài)鸟召,當線程運行完畢時欧募,就處于這個狀態(tài)跟继,而且該狀態(tài)不能再轉換成其他狀態(tài)舔糖。

注意處于等待狀態(tài)的線程只有兩種方式被喚醒:

  1. 調(diào)用對應的喚醒方法金吗。
  2. 調(diào)用該線程變量的interrupt()方法摇庙,會喚醒該線程卫袒,并拋出InterruptedException異常夕凝。

3.2 synchronized同步方法或者同步塊

synchronized同步方法或者同步塊具體是怎樣操作的呢?

  1. 相當于有一個大房間,房間門上有一把鎖lock膜蠢,房間里面存放的是所有與這把鎖lock關聯(lián)的同步方法或者同步塊挑围。
  2. 當某一個線程要執(zhí)行這把鎖lock的一個同步方法或者同步塊時杉辙,它就來到房間門前蜘矢,如果發(fā)現(xiàn)鎖lock還在品腹,那么它就拿著鎖進入房間舞吭,并將房間鎖上羡鸥,它可以執(zhí)行房間中任何一個同步方法或者同步塊存和。
  3. 這時又有另一個線程要執(zhí)行這把鎖lock的一個同步方法或者同步塊時哑姚,它就來到房間門前,發(fā)現(xiàn)鎖lock沒有了芜茵,就只能在門外等待叙量,此時該線程就在synchronized同步阻塞線程池中。
  4. 等到拿到鎖lock的線程九串,同步方法或者同步塊代碼執(zhí)行完畢绞佩,它就會從房間中退出來,將鎖放到門上猪钮。
  5. 這時在門外等待的線程就爭奪這把鎖lock,拿到鎖的線程就可以進入房間烤低,其他線程則又要繼續(xù)等待肘交。

注:synchronized 鎖是鎖住所有與這個鎖關聯(lián)的同步方法或者同步塊。

synchronized的同步鎖到底是什么呢扑馁?

其實就是java對象涯呻,在Java中,每一個對象都擁有一個鎖標記(monitor)腻要,也稱為監(jiān)視器复罐,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問雄家。

3.3 wait與notify效诅、notifyAll

這三個方法主要用于實現(xiàn)線程之間相互等待的問題。

調(diào)用對象lock的wait方法,會讓當前線程進行等待乱投,即將當前線程放入對象lock的線程等待池中咽笼。調(diào)用對象lock的notify方法會從線程等待池中隨機喚醒一個線程,notifyAll方法會喚醒所有線程篡腌。
注:對象lock的wait與notify褐荷、notifyAll方法調(diào)用必須放在以對象lock為鎖的同步方法或者同步塊中,否則會拋出IllegalMonitorStateException異常嘹悼。

wait與notify叛甫、notifyAll具體是怎么操作的呢?

  1. 前面過程與synchronized中介紹的一樣杨伙,當調(diào)用鎖lock的wait方法時其监,該線程(即當前線程)退出房間,歸還鎖lock限匣,但并不是進入synchronized同步阻塞線程池中抖苦,而是進入鎖lock的線程等待池中。
  2. 這時另一個線程拿到鎖lock進行房間米死,如果它執(zhí)行了鎖lock的notify方法锌历,那么就會從鎖lock的線程等待池中隨機喚醒一個線程,將它放入synchronized同步阻塞線程池中(記住只有拿到鎖lock的線程才能進行房間)峦筒。調(diào)用鎖lock的notifyAll方法,即喚醒線程等待池所有線程卤材。

使用wait與notify尉辑、notifyAll方法時,有兩點需要注意:

  1. wait與notify末贾、notifyAll方法這三個方法必須在synchronized同步代碼塊中執(zhí)行辉川,否則拋出IllegalMonitorStateException異常屿愚。
  2. 所以當我們使用notify娱据、notifyAll方法喚醒等待的線程時结啼,該線程不能立即執(zhí)行糕珊,因為它在synchronized同步代碼塊中,所以必須獲取鎖坟乾,才能繼續(xù)執(zhí)行印荔。

四. 其他重要方法

4.1 join方法

讓當前線程等待另一個線程執(zhí)行完成后实柠,才繼續(xù)執(zhí)行水泉。

    public final void join() throws InterruptedException {
        join(0);
    }
    public final synchronized void join(long millis) throws InterruptedException {
        // 獲取當前系統(tǒng)毫秒數(shù)
        long base = System.currentTimeMillis();
        long now = 0;

        // millis小于0,拋出異常
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            // 通過isAlive判斷當前線程是否存活
            while (isAlive()) {
                // wait(0)表示當前線程無限等待
                wait(0);
            }
        } else {
            // 通過isAlive判斷當前線程是否存活
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                // 當前線程等待delay毫秒窒盐,超過時間草则,當前線程就被喚醒
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

join方法是Thread中的方法,synchronized方法同步的鎖對象就是Thread對象登钥,通過調(diào)用Thread對象的wait方法畔师,讓當前線程等待

注意:這里是讓當前線程等待,即當前調(diào)用join方法的線程牧牢,而不是Thread對象的線程看锉。那么當前線程什么時候會被喚醒呢?
當Thread對象線程執(zhí)行完畢塔鳍,進入死亡狀態(tài)時伯铣,會調(diào)用Thread對象的notifyAll方法,來喚醒Thread對象的線程等待池中所有線程轮纫。

示例:

      public static void joinTest() {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":  i==="+i);
                }
            }
        }, "t1");
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+": end");
    }

4.2 sleep方法

只是讓當前線程等待一定的時間腔寡,然后繼續(xù)執(zhí)行。

4.3 yield方法

將當前線程狀態(tài)從運行狀態(tài)轉成可運行狀態(tài)掌唾,如果再獲取CPU執(zhí)行權放前,還會繼續(xù)執(zhí)行。

4.4 interrupt方法

它會中斷處于WAITING和TIMED_WAITING狀態(tài)下的線程糯彬,而對其他狀態(tài)下的線程不起任何作用凭语。

示例:

    public static void interruptTest() {
        // 處于TIMED_WAITING狀態(tài)下的線程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName()+" 開始");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+" 結束");
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+" 產(chǎn)生異常");
                }
            }
        }, "t1");
        thread.start();

        // 處于運行狀態(tài)下的線程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" 開始");
                int i = 0;
                while(i < Integer.MAX_VALUE - 10) {
                    i = i + 1;
                    for (int j = 0; j < i; j++);
                }
                System.out.println(Thread.currentThread().getName()+" i=="+i);
                System.out.println(Thread.currentThread().getName()+" 結束");
            }
        }, "t2");
        thread1.start();

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" 進行中斷");
        thread.interrupt();
        thread1.interrupt();
    }

4.5 isInterrupted方法

返回這個線程的中斷標志位。注意當調(diào)用線程的interrupt方法后撩扒,該線程的isInterrupted的方法就會返回true似扔。如果異常被處理了,又會將該標志位置位false搓谆,即isInterrupted的方法返回false炒辉。

4.6 線程優(yōu)先級以及守護線程

在java中線程優(yōu)先級范圍是1~10,默認的優(yōu)先級是5泉手。
在java中線程分為用戶線程和守護線程黔寇,isDaemon返回是true,表示它是守護線程斩萌。當所有的用戶線程執(zhí)行完畢后缝裤,java虛擬機就會退出状囱,不管是否還有守護線程未執(zhí)行完畢。
當創(chuàng)建一個新線程時倘是,這個新線程的優(yōu)先級等于創(chuàng)建它線程的優(yōu)先級,且只有當創(chuàng)建它線程是守護線程時袭艺,新線程才是守護線程搀崭。
當然也可以通過setPriority方法修改線程的優(yōu)先級,已經(jīng)setDaemon方法設置線程是否為守護線程猾编。

五. synchronized同步鎖與lock鎖

synchronized同步鎖與lock獨占鎖都可以保證并發(fā)操作安全問題瘤睹,即保證同一時間只有獲取鎖的那個線程才可以運行,其他線程必須等待答倡。

關于lock獨占鎖請閱讀我的AQS詳細介紹ReentrantLock詳細分析相關文章轰传。

那么它們有什么異同點呢?

5.1 獲取鎖的方式不同

  1. 對于synchronized同步鎖:進入synchronized代碼塊中的線程瘪撇,會自動獲取鎖获茬,而其他線程就只能阻塞等待。
  2. 對于lock鎖:想要獲取lock鎖倔既,必須調(diào)用lock的lock系列方法恕曲,根據(jù)方法不同獲取鎖的方式也不同。
    // 獲取鎖渤涌,如果獲取不到佩谣,就一直等待。不響應中斷請求
    void lock();

    // 獲取鎖实蓬,如果獲取不到茸俭,就一直等待。如果在線程等待期間有中斷請求就拋出異常
    void lockInterruptibly() throws InterruptedException;

    // 嘗試獲取鎖安皱,立即返回调鬓。返回true表示獲取成功,返回false表示獲取失敗
    boolean tryLock();

    // 在規(guī)定的unit時間內(nèi)獲取鎖练俐,如果時間到了還沒有獲取到鎖袖迎,則返回false,表示獲取失敗
    // 如果在線程等待期間有中斷請求就拋出異常
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

5.2 釋放鎖的方式不同

  1. 當synchronized代碼塊執(zhí)行完成腺晾,或者拋出異常返回燕锥,都會自動釋放鎖,不需要用戶手動釋放悯蝉。
  2. 如果執(zhí)行完成归形,用戶必須主動調(diào)用unlock來釋放鎖,否則等待鎖的線程就會一直阻塞鼻由。為了防止發(fā)生異常暇榴,導致unlock方法沒有執(zhí)行厚棵,所以這個方法必須放在finally的代碼塊中。

5.3 等待鎖的線程狀態(tài)不一樣

  1. 等待synchronized同步鎖線程的狀態(tài)是 BLOCKED(阻塞狀態(tài))蔼紧。
  2. 等待lock鎖線程的狀態(tài)是WAITING(等待狀態(tài))或TIMED_WAITING(等待超時狀態(tài))婆硬。

記得我們在線程狀態(tài)中介紹過,處于WAITING與TIMED_WAITING狀態(tài)的線程奸例,是可以響應線程中斷的彬犯。而處于BLOCKED狀態(tài)的線程則不可以。

如果獲取synchronized鎖的線程一直不釋放鎖查吊,那么等待鎖的線程只能一直等待谐区,而獲取lock鎖的線程一直不釋放鎖,我們可以調(diào)用等待鎖的線程的interrupt()方法逻卖,將這個線程喚醒宋列。

其實Lock鎖中線程等待和喚醒主要是通過LockSupport類實現(xiàn)的,關于LockSupport請看JUC鎖框架_ LockSupport詳細分析這篇文章评也。

六. 實例講解

6.1 不加任何同步鎖

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;

class Data {
    int num;

    public Data(int num) {
        this.num = num;
    }

    public int getAndDecrement() {
        return num--;
    }
}

class MyRun implements Runnable {

    private Data data;
    // 用來記錄所有賣出票的編號
    private List<Integer> list;
    private CountDownLatch latch;

    public MyRun(Data data, List<Integer> list, CountDownLatch latch) {
        this.data = data;
        this.list = list;
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            action();
        }  finally {
            // 釋放latch共享鎖
            latch.countDown();
        }
    }

    // 進行買票操作炼杖,注意這里沒有使用data.num>0作為判斷條件,直到賣完線程退出仇参。
    // 那么做會導致這兩處使用了共享變量data.num嘹叫,那么做多線程同步時,就要考慮更多條件诈乒。
    // 這里只for循環(huán)了5次罩扇,表示每個線程只賣5張票,并將所有賣出去編號存入list集合中怕磨。
    public void action() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int newNum = data.getAndDecrement();

            System.out.println("線程"+Thread.currentThread().getName()+"  num=="+newNum);
            list.add(newNum);
        }
    }
}

public class ThreadTest {


    public static void startThread(Data data, String name, List<Integer> list,CountDownLatch latch) {
        Thread t = new Thread(new MyRun(data, list, latch), name);
        t.start();
    }

    public static void main(String[] args) {
        // 使用CountDownLatch來讓主線程等待子線程都執(zhí)行完畢時喂饥,才結束
        CountDownLatch latch = new CountDownLatch(6);

        long start = System.currentTimeMillis();
        // 這里用并發(fā)list集合
        List<Integer> list = new CopyOnWriteArrayList();
        Data data = new Data(30);
        startThread(data, "t1", list, latch);
        startThread(data, "t2", list, latch);
        startThread(data, "t3", list, latch);
        startThread(data, "t4", list, latch);
        startThread(data, "t5", list, latch);
        startThread(data, "t6", list, latch);


        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 處理一下list集合,進行排序和翻轉
        Collections.sort(list);
        Collections.reverse(list);
        System.out.println(list);

        long time = System.currentTimeMillis() - start;
        // 輸出一共花費的時間
        System.out.println("\n主線程結束 time=="+time);
    }
}

輸出的結果是

線程t2  num==29
線程t6  num==27
線程t5  num==28
線程t4  num==28
線程t1  num==30
線程t3  num==30
線程t2  num==26
線程t4  num==24
線程t6  num==25
線程t5  num==23
線程t1  num==22
線程t3  num==21
線程t4  num==20
線程t6  num==19
線程t5  num==18
線程t2  num==17
線程t1  num==16
線程t3  num==15
線程t4  num==14
線程t5  num==12
線程t6  num==13
線程t1  num==9
線程t3  num==10
線程t2  num==11
線程t1  num==8
線程t6  num==5
線程t2  num==7
線程t5  num==3
線程t3  num==4
線程t4  num==6
[30, 30, 29, 28, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3]

主線程結束 time==62

從結果中發(fā)現(xiàn)問題肠鲫,出現(xiàn)了重復票员帮,所以30張票沒有被賣完。最主要的原因就是Data類的getAndDecrement方法操作不是多線程安全的导饲。

  1. 首先它不能保證原子性捞高,分為三個操作,先讀取num的值渣锦,然后num自減硝岗,在返回自減前的值。
  2. 因為num不是volatile關鍵字修飾的袋毙,它也不能保證可見性和有序性型檀。

所以只要保證getAndDecrement方法多線程安全,那么就可以解決上面出現(xiàn)的問題听盖。那么保證getAndDecrement方法多線程安全呢胀溺?最簡單的方式就是在getAndDecrement方法前加synchronized關鍵字裂七。

這是synchronized關鍵鎖就是這個data對象實例,所以保證了多線程調(diào)用getAndDecrement方法時仓坞,只有一個線程能調(diào)用背零,等待調(diào)用完成,其他線程才能調(diào)用getAndDecrement方法无埃。
因為同一時間只有一個線程調(diào)用getAndDecrement方法捉兴,所以它在做num--操作時,不用擔心num變量會發(fā)生改變录语。所以原子性、可見性和有序性都可以得到保證禾乘。

6.2 使用最小同步鎖

class Data {
    int num;

    public Data(int num) {
        this.num = num;
    }
    // 將getAndDecrement方法加了同步鎖
    public synchronized int getAndDecrement() {
        return num--;
    }
}

輸出結果

線程t1  num==30
線程t2  num==29
線程t6  num==28
線程t4  num==26
線程t3  num==27
線程t5  num==25
線程t6  num==22
線程t2  num==21
線程t3  num==23
線程t1  num==24
線程t4  num==20
線程t5  num==19
線程t2  num==18
線程t3  num==17
線程t5  num==13
線程t4  num==14
線程t6  num==16
線程t1  num==15
線程t2  num==12
線程t4  num==9
線程t1  num==7
線程t5  num==10
線程t3  num==11
線程t6  num==8
線程t4  num==6
線程t2  num==3
線程t1  num==2
線程t3  num==4
線程t5  num==5
線程t6  num==1
[30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

主線程結束 time==61

我們只是將Data的getAndDecrement方法加了同步鎖澎埠,發(fā)現(xiàn)解決了多線程并發(fā)問題。主要是因為我們只在一處使用了共享變量num始藕,所以只需要將這處加同步就行了蒲稳。而且你會發(fā)現(xiàn)最后花費的總時間與沒加同步鎖時幾乎一樣,那么因為我們同步代碼足夠小伍派。
相反地江耀,我們加地同步鎖不合理,可能也能實現(xiàn)多線程安全诉植,但是耗時就會大大增加祥国。

6.3 不合理地使用同步鎖

@Override
    public void run() {
        try {
            synchronized (data){
                action();
            }
        }  finally {
            // 釋放latch共享鎖
            latch.countDown();
        }
    }

輸入結果:

線程t1  num==30
線程t1  num==29
線程t1  num==28
線程t1  num==27
線程t1  num==26
線程t6  num==25
線程t6  num==24
線程t6  num==23
線程t6  num==22
線程t6  num==21
線程t5  num==20
線程t5  num==19
線程t5  num==18
線程t5  num==17
線程t5  num==16
線程t4  num==15
線程t4  num==14
線程t4  num==13
線程t4  num==12
線程t4  num==11
線程t3  num==10
線程t3  num==9
線程t3  num==8
線程t3  num==7
線程t3  num==6
線程t2  num==5
線程t2  num==4
線程t2  num==3
線程t2  num==2
線程t2  num==1
[30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

主線程結束 time==342

在這里我們將整個action方法,放入同步代碼塊中晾腔,也可以解決多線程沖突問題舌稀,但是所耗費的時間是在getAndDecrement方法上加同步鎖時間的幾倍。

所以我們在加同步鎖的時候灼擂,那些需要同步壁查,就是看那些地方使用了共享變量。比如這里只在getAndDecrement方法中使用了同步變量剔应,所以只要給它加鎖就行了睡腿。
但是如果在action方法中,使用data.num>0來作為循環(huán)條件峻贮,那么在加同步鎖時席怪,就必須將整個action方法放在同步模塊中,因為我們必須保證月洛,在data.num>0判斷到getAndDecrement方法調(diào)用這些代碼都是在同步模塊中何恶,不然就會產(chǎn)生多線程沖突問題。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嚼黔,一起剝皮案震驚了整個濱河市细层,隨后出現(xiàn)的幾起案子惜辑,更是在濱河造成了極大的恐慌,老刑警劉巖疫赎,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盛撑,死亡現(xiàn)場離奇詭異,居然都是意外死亡捧搞,警方通過查閱死者的電腦和手機抵卫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胎撇,“玉大人介粘,你說我怎么就攤上這事⊥硎鳎” “怎么了姻采?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爵憎。 經(jīng)常有香客問我慨亲,道長,這世上最難降的妖魔是什么宝鼓? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任刑棵,我火速辦了婚禮,結果婚禮上愚铡,老公的妹妹穿的比我還像新娘蛉签。我一直安慰自己,他們只是感情好沥寥,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布正蛙。 她就那樣靜靜地躺著,像睡著了一般营曼。 火紅的嫁衣襯著肌膚如雪乒验。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天蒂阱,我揣著相機與錄音锻全,去河邊找鬼。 笑死录煤,一個胖子當著我的面吹牛鳄厌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妈踊,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼了嚎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起歪泳,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤萝勤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呐伞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敌卓,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年伶氢,在試婚紗的時候發(fā)現(xiàn)自己被綠了趟径。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡癣防,死狀恐怖蜗巧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蕾盯,我是刑警寧澤惧蛹,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站刑枝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏迅腔。R本人自食惡果不足惜装畅,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沧烈。 院中可真熱鬧掠兄,春花似錦、人聲如沸锌雀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腋逆。三九已至婿牍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惩歉,已是汗流浹背等脂。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留撑蚌,地道東北人上遥。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像争涌,于是被迫代替她去往敵國和親粉楚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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