直想不明白的synchronized鎖竟如此簡(jiǎn)單!

線程問(wèn)題

線程出現(xiàn)問(wèn)題的根本原因是因?yàn)榫€程上下文切換镊辕,導(dǎo)致線程里的指令沒(méi)有執(zhí)行完就切換執(zhí)行其它線程了痛阻,

舉例

t1和t2線程分別并行執(zhí)行5000次++操作和--操作菌瘪,理論上結(jié)果應(yīng)該等于0。

代碼模擬

static int count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        for (int i = 1;i<5000;i++){
            count++;
        }
    });
    Thread t2 =new Thread(()->{
        for (int i = 1;i<5000;i++){
            count--;
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("count的值是{}",count);
}

實(shí)際count的值有正有負(fù)阱当,分析i++與i--操作的字節(jié)碼

getstatic i // 獲取靜態(tài)變量i的值
iconst_1 // 準(zhǔn)備常量1
iadd // 自增
putstatic i // 將修改后的值存入靜態(tài)變量i

getstatic i // 獲取靜態(tài)變量i的值
iconst_1 // 準(zhǔn)備常量1
isub // 自減
putstatic i // 將修改后的值存入靜態(tài)變量i

可以看到count++ 和 count-- 操作實(shí)際都是需要這個(gè)4個(gè)指令完成的俏扩,那么這里問(wèn)題就來(lái)了!Java 的內(nèi)存模型如下弊添,完成靜態(tài)變量的自增录淡,自減需要在主存和工作內(nèi)存中進(jìn)行數(shù)據(jù)交換:

臨界區(qū)

  1. 一個(gè)程序運(yùn)行多線程本身是沒(méi)有問(wèn)題的

  2. 問(wèn)題出現(xiàn)在多個(gè)線程共享資源的時(shí)候

  3. 多個(gè)線程同時(shí)對(duì)共享資源進(jìn)行讀操作本身也沒(méi)有問(wèn)題

  4. 問(wèn)題出現(xiàn)在對(duì)對(duì)共享資源同時(shí)進(jìn)行讀寫(xiě)操作時(shí)就有問(wèn)題了

  5. 先定義一個(gè)叫做臨界區(qū)的概念:一段代碼內(nèi)如果存在對(duì)共享資源的多線程讀寫(xiě)操作,那么稱這段代碼為臨界區(qū)

  6. static int counter = 0; static void increment()
    {// 臨界區(qū) counter++;
    } static void decrement()
    {// 臨界區(qū) counter--;
    }

競(jìng)態(tài)條件

多個(gè)線程在臨界區(qū)執(zhí)行油坝,那么由于代碼指令的執(zhí)行不確定(線程上下文切換)而導(dǎo)致的結(jié)果問(wèn)題嫉戚,稱為競(jìng)態(tài)條件

synchronized 解決方案

為了避免臨界區(qū)中的競(jìng)態(tài)條件發(fā)生,由多種手段可以達(dá)到

  • 阻塞式解決方案:synchronized 澈圈,Lock
  • 非阻塞式解決方案:原子變量

現(xiàn)在討論使用synchronized來(lái)進(jìn)行解決彬檀,即俗稱的對(duì)象鎖,它采用互斥的方式讓同一時(shí)刻至多只有一個(gè)線程持有對(duì)象鎖瞬女,其他線程如果想獲取這個(gè)鎖就會(huì)阻塞住窍帝,這樣就能保證擁有鎖的線程可以安全的執(zhí)行臨界區(qū)內(nèi)的代碼,不用擔(dān)心線程上下文切換

注意 雖然 java 中互斥和同步都可以采用 synchronized 關(guān)鍵字來(lái)完成诽偷,但它們還是有區(qū)別的:

互斥是保證臨界區(qū)的競(jìng)態(tài)條件發(fā)生坤学,同一時(shí)刻只能有一個(gè)線程執(zhí)行臨界區(qū)的代碼

同步是由于線程執(zhí)行的先后疯坤,順序不同但是需要一個(gè)線程等待其它線程運(yùn)行到某個(gè)點(diǎn)。

synchronized

synchronized(對(duì)象) // 線程1獲得鎖拥峦, 那么線程2的狀態(tài)是(blocked)
{
 臨界區(qū)
}

改進(jìn)的代碼

@Slf4j
public class Test03 {
    //定義鎖對(duì)象
    final private static Object lock = new Object();
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i < 5000; i++) {
                //臨界區(qū)加鎖
                synchronized (lock) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 1; i < 5000; i++) {
                //臨界區(qū)加鎖
                synchronized (lock) {
                    count--;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count的值是{}", count);
    }
}

synchronized原理

synchronized實(shí)際上利用對(duì)象保證了臨界區(qū)代碼的原子性贴膘,臨界區(qū)內(nèi)的代碼在外界看來(lái)是不可分割的,不會(huì)被線程切換所打斷略号。

小結(jié)

關(guān)注點(diǎn)【鎖對(duì)象】刑峡,【原子性】

面向?qū)ο笏枷雰?yōu)化

再次改進(jìn)的代碼

@Slf4j
public class Test03 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 1; i < 5000; i++) {
                synchronized (room) {
                    room.increment();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 1; i < 5000; i++) {
                room.decrease();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count的值是{}", room.getCounter());
    }
}

class Room {
    private Integer counter = 0;

    public void increment() {
        //臨界區(qū)加鎖
        synchronized (this) {
            counter++;
        }
    }

    public void decrease() {
        //臨界區(qū)加鎖
        synchronized (this) {
            counter--;
        }
    }

    public Integer getCounter() {
        synchronized (this) {
            //避免讀取到中間值,加鎖,因?yàn)橛衘oin方法玄柠,不加也沒(méi)問(wèn)題
            return counter;
        }
    }
}

變量的線程安全分析

成員變量和靜態(tài)變量的線程安全分析

  • 如果沒(méi)有變量沒(méi)有在線程間共享突梦,那么變量是安全的

  • 如果變量在線程間共享

  • 如果只有讀操作,則線程安全

  • 如果有讀寫(xiě)操作羽利,則這段代碼是臨界區(qū)宫患,需要考慮線程安全

public class Test04 {
    ArrayList<Integer> arrayList=new ArrayList<>(); //成員變量

    public void method1(int n){
        for (int i = 0; i < n; i++) {
            add();
            remove();
        }
    }

    public void add(){
        arrayList.add(1); //讀寫(xiě)操作
    }

    public void remove(){
        arrayList.remove(0); //讀寫(xiě)操作
    }
}

arrayList對(duì)象被兩線程共享,執(zhí)行讀寫(xiě)操作會(huì)出現(xiàn)線程安全問(wèn)題这弧。

局部變量線程安全分析

  • 局部變量【局部變量被初始化為基本數(shù)據(jù)類型】是安全的public void test1(){
    //局部變量 int類型變量i int i = 10;
    i++; //讀寫(xiě)操作 } 從圖中可以看到局部變量i并未被兩線程共享娃闲。

  • 局部變量引用的對(duì)象未必是安全的

  • 如果局部變量引用的對(duì)象沒(méi)有引用線程共享的對(duì)象,那么是線程安全的上述例子改善后的代碼****public void method1(int n){
    ArrayList<Integer> List=new ArrayList<>(); //局部變量 for (int i = 0; i < n; i++) {
    add(List);
    remove(List);
    }
    } public void add(ArrayList<Integer> arrayList){
    arrayList.add(1); //讀寫(xiě)操作 } public void remove(ArrayList<Integer> arrayList){
    arrayList.remove(0); //讀寫(xiě)操作 }

  • 如果局部變量引用的對(duì)象引用了一個(gè)線程共享的對(duì)象匾浪,那么要考慮線程安全的class Sub extends Test04 {
    @Override public void add(ArrayList<Integer> arrayList) {
    Thread sub = new Thread(() -> arrayList.add(1));
    sub.start();
    }
    } 如果子類創(chuàng)建了新的線程重寫(xiě)了父類的方法皇帮,那么又會(huì)造成線程安全問(wèn)題,子類創(chuàng)建的線程會(huì)共享其中一個(gè)線程的局部變量蛋辈,為避免此類問(wèn)題属拾,可加final或private關(guān)鍵字修飾,使得該方法不會(huì)被繼承或訪問(wèn)冷溶〗グ祝”常見(jiàn)線程安全類★注意:這里說(shuō)它們是線程安全的是指,多個(gè)線程調(diào)用它們<!!!同一個(gè)實(shí)例>的某個(gè)方法時(shí)逞频,是線程安全的纯衍。也可以理解為它們的每個(gè)方法是原子的,即每個(gè)方法都加了synchronized關(guān)鍵字苗胀,但是組合在一起線程就不安全了托酸。”Hashtable table = newHashtable(); new Thread(()->{
    table.put("key", "value1");
    }).start(); //安全 new Thread(()->{
    table.put("key", "value2");
    }).start(); //安全 線程安全類方法的組合注意多個(gè)線程調(diào)用同一實(shí)例(table)的讀寫(xiě)方法的組合(get方法和put方法組合)不是原子的Hashtable table = newHashtable(); // 線程1柒巫,線程2 if( table.get("key") == null) {
    table.put("key", value);
    } 結(jié)論:輸出table中的key值,結(jié)果可能是v1也可能是v2谷丸,組合方法導(dǎo)致線程不安全堡掏。

  • String

  • Integer

  • StringBuffer

  • Random

  • Vector

  • Hashtable

  • java.util.concurrent 包下的類

不可變類的線程安全

String和Integer類都是不可變的類,因?yàn)槠漕悆?nèi)部狀態(tài)是不可改變的刨疼,因此它們的方法都是線程安全的泉唁,有同

學(xué)或許有疑問(wèn)鹅龄,String 有 replace,substring 等方法【可以】改變值啊亭畜,其實(shí)調(diào)用這些方法返回的已經(jīng)是

一個(gè)新創(chuàng)建的對(duì)象了扮休!

習(xí)題一

方法中有多個(gè)實(shí)例對(duì)象的臨界區(qū)鎖住this無(wú)效。

@Slf4j
public class Test05 {
    public static void main(String[] args) throws InterruptedException {
        Account A = new Account(1000);
        Account B = new Account(1000);
        Thread T1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                A.transferMoney(B, randomMoney());
            }
        });
        Thread T2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                B.transferMoney(A, randomMoney());
            }
        });
        T1.start();
        T2.start();
        T1.join();
        T2.join();

        log.debug("總錢數(shù)為{}", A.getMoney() + B.getMoney());
    }

    static Random random = new Random();

    static public int randomMoney() {
        return random.nextInt(100) + 1;
    }

}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public Account() {
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public void transferMoney(Account target, int money) {
            if (this.money >= money){
                this.setMoney(this.getMoney()-money);
                target.setMoney(target.getMoney() + money);
        }
    }
}

★分析

線程t1和t2共享Account類中的成員變量money拴鸵,且調(diào)用方法中包含讀寫(xiě)操作(臨界區(qū))玷坠,因此會(huì)有線程安全問(wèn)題,但

是在transferMoney方法中添加synchronized關(guān)鍵字相當(dāng)于

public void transferMoney(Account target, int money) {
    synchronized (this){
        if (this.money >= money){
            this.setMoney(this.getMoney()-money);
            target.setMoney(target.getMoney() + money);
    }
}}

此時(shí)線程并不安全劲藐,因?yàn)閠arget對(duì)象的money變量并未加鎖八堡,t1和t2讀寫(xiě)操作還仍然可以訪問(wèn)并修改target對(duì)象的成員

變量money,造成線程不安全聘芜。

public void transferMoney(Account target, int money) {
    synchronized (Account.class){
        if (this.money >= money){
            this.setMoney(this.getMoney()-money);
            target.setMoney(target.getMoney() + money);
    }
}}

注意:

此次可以采取鎖住類對(duì)象解決兄渺,target和this都是類對(duì)象的實(shí)例,因此兩個(gè)對(duì)象中的成員變量都會(huì)被鎖住汰现,線程安全挂谍,

但是如果有多個(gè)無(wú)關(guān)實(shí)例對(duì)象也會(huì)被同時(shí)鎖住,處于阻塞狀態(tài)瞎饲,就會(huì)導(dǎo)致效率低下口叙。

Monitor 概念

Java 對(duì)象頭

  • Mark Word 包含對(duì)象hashcode值,年齡代企软,鎖狀態(tài)等信息
  • Klass Word 包含對(duì)象的類型信息

以 32 位虛擬機(jī)為例,普通對(duì)象的對(duì)象頭結(jié)構(gòu)如下庐扫,其中的Klass Word為指針,指向?qū)?yīng)的Class對(duì)象仗哨;

數(shù)組對(duì)象

其中 Mark Word 結(jié)構(gòu)為

1583651590160

所以一個(gè)對(duì)象的結(jié)構(gòu)如下:

1583678624634

原理

Monitor被翻譯為監(jiān)視器或者說(shuō)管程

每個(gè)java對(duì)象都可以關(guān)聯(lián)一個(gè)Monitor形庭,如果使用synchronized給對(duì)象上鎖(重量級(jí)),該對(duì)象頭的Mark Word中就被設(shè)置為指向Monitor對(duì)象的指針

  • 剛開(kāi)始時(shí)Monitor中的Owner為null
  • 當(dāng)Thread-2 執(zhí)行synchronized(obj){}代碼時(shí)就會(huì)將Monitor的所有者Owner 設(shè)置為 Thread-2厌漂,上鎖成功萨醒,Monitor中同一時(shí)刻只能有一個(gè)Owner
  • 當(dāng)Thread-2 占據(jù)鎖時(shí),如果線程Thread-3苇倡,Thread-4也來(lái)執(zhí)行synchronized(obj){}代碼富纸,就會(huì)進(jìn)入EntryList中變成BLOCKED狀態(tài)
  • Thread-2 執(zhí)行完同步代碼塊的內(nèi)容,然后喚醒 EntryList 中等待的線程來(lái)競(jìng)爭(zhēng)鎖旨椒,競(jìng)爭(zhēng)時(shí)是非公平的
  • 圖中 WaitSet 中的 Thread-0晓褪,Thread-1 是之前獲得過(guò)鎖,但條件不滿足進(jìn)入 WAITING 狀態(tài)的線程综慎,后面講wait-notify 時(shí)會(huì)分析

注意:synchronized 必須是進(jìn)入同一個(gè)對(duì)象的 monitor 才有上述的效果不加 synchronized 的對(duì)象不會(huì)關(guān)聯(lián)監(jiān)視器涣仿,不遵從以上規(guī)則

synchronized原理

    static final Object lock=new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }

反編譯后的部分字節(jié)碼

 0 getstatic #2 <com/concurrent/test/Test17.lock>
 # 取得lock的引用(synchronized開(kāi)始了)
 3 dup    
 # 復(fù)制操作數(shù)棧棧頂?shù)闹捣湃霔m敚磸?fù)制了一份lock的引用
 4 astore_1
 # 操作數(shù)棧棧頂?shù)闹祻棾觯磳ock的引用存到局部變量表中
 5 monitorenter
 # 將lock對(duì)象的Mark Word置為指向Monitor指針
 6 getstatic #3 <com/concurrent/test/Test17.counter>
 9 iconst_1
10 iadd
11 putstatic #3 <com/concurrent/test/Test17.counter>
14 aload_1
# 從局部變量表中取得lock的引用好港,放入操作數(shù)棧棧頂
15 monitorexit
# 將lock對(duì)象的Mark Word重置愉镰,喚醒EntryList
16 goto 24 (+8)
# 下面是異常處理指令,可以看到钧汹,如果出現(xiàn)異常丈探,也能自動(dòng)地釋放鎖
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return

注意:方法級(jí)別的 synchronized 不會(huì)在字節(jié)碼指令中有所體現(xiàn)

重量級(jí)鎖優(yōu)化

輕量級(jí)鎖

輕量級(jí)鎖的使用場(chǎng)景是:如果一個(gè)對(duì)象雖然有多個(gè)線程要對(duì)它進(jìn)行加鎖,但是加鎖的時(shí)間是錯(cuò)開(kāi)的(也就是沒(méi)有人可以競(jìng)爭(zhēng)的)拔莱,那么可以使用輕量級(jí)鎖來(lái)進(jìn)行優(yōu)化碗降。輕量級(jí)鎖對(duì)使用者是透明的,即語(yǔ)法仍然是synchronized辨宠,假設(shè)有兩個(gè)方法同步塊遗锣,利用同一個(gè)對(duì)象加鎖

static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步塊 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步塊 B
     }
}

  1. 每次指向到synchronized代碼塊時(shí),都會(huì)創(chuàng)建鎖記錄(Lock Record)對(duì)象嗤形,每個(gè)線程都會(huì)包括一個(gè)鎖記錄的結(jié)構(gòu)精偿,鎖記錄內(nèi)部可以儲(chǔ)存對(duì)象的Mark Word和對(duì)象引用reference

  2. 1583755737580

  3. 讓鎖記錄中的Object reference指向?qū)ο螅⑶覈L試用cas(compare and sweep)替換Object對(duì)象的Mark Word 赋兵,將Mark Word 的值存入鎖記錄中

  4. 1583755888236

  5. 如果cas替換成功笔咽,那么對(duì)象的對(duì)象頭儲(chǔ)存的就是鎖記錄的地址和狀態(tài)01,如下所示

  6. 1583755964276

  7. 如果cas失敗霹期,有兩種情況

  8. 1583756190177

  9. 如果是其它線程已經(jīng)持有了該Object的輕量級(jí)鎖叶组,那么表示有競(jìng)爭(zhēng),將進(jìn)入鎖膨脹階段

  10. 如果是自己的線程已經(jīng)執(zhí)行了synchronized進(jìn)行加鎖历造,那么那么再添加一條 Lock Record 作為重入的計(jì)數(shù)

  11. 當(dāng)線程退出synchronized代碼塊的時(shí)候甩十,**如果獲取的是取值為 null 的鎖記錄 **,表示有重入吭产,這時(shí)重置鎖記錄侣监,表示重入計(jì)數(shù)減一

  12. 1583756357835

  13. 當(dāng)線程退出synchronized代碼塊的時(shí)候,如果獲取的鎖記錄取值不為 null臣淤,那么使用cas將Mark Word的值恢復(fù)給對(duì)象

  14. 成功則解鎖成功

  15. 失敗橄霉,則說(shuō)明輕量級(jí)鎖進(jìn)行了鎖膨脹或已經(jīng)升級(jí)為重量級(jí)鎖,進(jìn)入重量級(jí)鎖解鎖流程

鎖膨脹

如果在嘗試加輕量級(jí)鎖的過(guò)程中邑蒋,cas操作無(wú)法成功姓蜂,這是有一種情況就是其它線程已經(jīng)為這個(gè)對(duì)象加上了輕量級(jí)鎖,這是就要進(jìn)行鎖膨脹医吊,將輕量級(jí)鎖變成重量級(jí)鎖钱慢。

  1. 當(dāng) Thread-1 進(jìn)行輕量級(jí)加鎖時(shí),Thread-0 已經(jīng)對(duì)該對(duì)象加了輕量級(jí)鎖

  2. 1583757433691

  3. 這時(shí) Thread-1 加輕量級(jí)鎖失敗卿堂,進(jìn)入鎖膨脹流程

  4. 即為對(duì)象申請(qǐng)Monitor鎖滩字,讓Object指向重量級(jí)鎖地址,然后自己進(jìn)入Monitor 的EntryList 變成BLOCKED狀態(tài)

  5. 1583757586447

  6. 當(dāng)Thread-0 推出synchronized同步塊時(shí),使用cas將Mark Word的值恢復(fù)給對(duì)象頭麦箍,失敗,那么會(huì)進(jìn)入重量級(jí)鎖的解鎖過(guò)程陶珠,即按照Monitor的地址找到Monitor對(duì)象挟裂,將Owner設(shè)置為null,喚醒EntryList 中的Thread-1線程

自旋優(yōu)化

重量級(jí)鎖競(jìng)爭(zhēng)的時(shí)候揍诽,還可以使用自旋來(lái)進(jìn)行優(yōu)化诀蓉,如果當(dāng)前線程自旋成功(即在自旋的時(shí)候持鎖的線程釋放了鎖),那么當(dāng)前線程就可以不用進(jìn)行上下文切換就獲得了鎖

  1. 自旋重試成功的情況

  2. 1583758113724

  3. 自旋重試失敗的情況暑脆,自旋了一定次數(shù)還是沒(méi)有等到持鎖的線程釋放鎖

  4. 1583758136650

自旋會(huì)占用 CPU 時(shí)間渠啤,單核 CPU 自旋就是浪費(fèi),多核 CPU 自旋才能發(fā)揮優(yōu)勢(shì)添吗。

智能控制自選可能性

在 Java 6 之后自旋鎖是自適應(yīng)的沥曹,比如對(duì)象剛剛的一次自旋操作成功過(guò),那么認(rèn)為這次自旋成功的可能性會(huì)高碟联,就多自旋幾次妓美;

反之,就少自旋甚至不自旋鲤孵,總之壶栋,比較智能。Java 7 之后不能控制是否開(kāi)啟自旋功能

偏向鎖

在輕量級(jí)的鎖中普监,我們可以發(fā)現(xiàn)贵试,如果同一個(gè)線程對(duì)同一個(gè)2對(duì)象進(jìn)行重入鎖時(shí),也需要執(zhí)行CAS操作凯正,這是有點(diǎn)耗時(shí)

滴毙玻,那么Java6開(kāi)始引入了偏向鎖概念,只有第一次使用CAS時(shí)將對(duì)象的Mark Word頭設(shè)置為入鎖線程ID(操作系統(tǒng)提供)

之后這個(gè)入鎖線程再進(jìn)行重入鎖時(shí)漆际,發(fā)現(xiàn)線程ID是自己的淆珊,那么就不用再進(jìn)行CAS了

1583760728806

偏向狀態(tài)

1583762169169

一個(gè)對(duì)象的創(chuàng)建過(guò)程

  1. 如果開(kāi)啟了偏向鎖(默認(rèn)是開(kāi)啟的),那么對(duì)象剛創(chuàng)建之后奸汇,Mark Word 最后三位的值101施符,并且這是它的Thread,epoch擂找,age都是0戳吝,在加鎖的時(shí)候進(jìn)行設(shè)置這些的值.

  2. 偏向鎖默認(rèn)是延遲的,不會(huì)在程序啟動(dòng)的時(shí)候立刻生效贯涎,如果想避免延遲听哭,可以添加虛擬機(jī)參數(shù)來(lái)禁用延遲:-XX:BiasedLockingStartupDelay=0來(lái)禁用延遲

  3. 注意:處于偏向鎖的對(duì)象解鎖后,線程 ID(不能用Java方法獲取是操作系統(tǒng)提供的)仍存儲(chǔ)于對(duì)象頭中

  4. 加上虛擬機(jī)參數(shù)-XX:BiasedLockingStartupDelay=0進(jìn)行測(cè)試

  5. 輸出結(jié)果如下,三次輸出的狀態(tài)碼都為101

  6. public static void main(String[] args) throws InterruptedException {
    Test1 t = new Test1();
    test.parseObjectHeader(getObjectHeader(t))陆盘;
    synchronized(t){
    test.parseObjectHeader(getObjectHeader(t));
    }
    test.parseObjectHeader(getObjectHeader(t));
    } biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
    biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
    biasedLockFlag (1bit): 1
    LockFlag (2bit): 01

禁用偏向鎖

測(cè)試禁用:如果沒(méi)有開(kāi)啟偏向鎖普筹,那么對(duì)象創(chuàng)建后最后三位的值為001,這時(shí)候它的hashcode隘马,age都為0太防,hashcode

是第一次用到hashcode時(shí)才賦值的。在上面測(cè)試代碼運(yùn)行時(shí)<typo id="typo-11993" data-origin="在" ignoretag="true">在</typo>添加 VM 參數(shù)-XX:-UseBiasedLocking禁用偏向鎖

(禁用偏向鎖則優(yōu)先使用輕量級(jí)鎖)酸员,退出synchronized狀態(tài)變回001

  1. 虛擬機(jī)參數(shù)-XX:-UseBiasedLocking
  2. 輸出結(jié)果如下蜒车,最開(kāi)始狀態(tài)為001,然后加輕量級(jí)鎖變成00幔嗦,最后恢復(fù)成001biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
    LockFlag (2bit): 00
    biasedLockFlag (1bit): 0
    LockFlag (2bit): 01

撤銷偏向鎖-【鎖對(duì)象調(diào)用hashcode方法】

測(cè)試 hashCode:當(dāng)調(diào)用對(duì)象的hashcode方法的時(shí)候就會(huì)撤銷這個(gè)對(duì)象的偏向鎖酿愧,因?yàn)槭褂闷蜴i時(shí)沒(méi)有位置存

hashcode的值了。

僅限于偏向鎖

  • 輕量級(jí)鎖會(huì)將hashcode值
  1. 測(cè)試代碼如下邀泉,使用虛擬機(jī)參數(shù)-XX:BiasedLockingStartupDelay=0 嬉挡,確保我們的程序最開(kāi)始使用了偏向鎖!但是結(jié)果顯示程序還是使用了輕量級(jí)鎖呼渣。 public static void main(String[] args) throws InterruptedException {
    Test1 t = new Test1();
    t.hashCode();
    test.parseObjectHeader(getObjectHeader(t));

        **synchronized** (t){
            test.parseObjectHeader(getObjectHeader(t));
        }
        test.parseObjectHeader(getObjectHeader(t));
    }
    
  2. 輸出結(jié)果biasedLockFlag (1bit): 0
    LockFlag (2bit): 01
    LockFlag (2bit): 00
    biasedLockFlag (1bit): 0
    LockFlag (2bit): 01

撤銷偏向鎖-【其它線程使用該鎖對(duì)象】

這里我們演示的是偏向鎖撤銷變成輕量級(jí)鎖的過(guò)程棘伴,那么就得滿足輕量級(jí)鎖的使用條件,就是沒(méi)有線程對(duì)同一個(gè)對(duì)象進(jìn)行鎖競(jìng)爭(zhēng)屁置,我們使用wait 和 notify 來(lái)輔助實(shí)現(xiàn)

  1. 代碼焊夸,虛擬機(jī)參數(shù)-XX:BiasedLockingStartupDelay=0確保我們的程序最開(kāi)始使用了偏向鎖!
  2. 輸出結(jié)果蓝角,最開(kāi)始使用的是偏向鎖阱穗,但是第二個(gè)線程嘗試獲取對(duì)象鎖時(shí),發(fā)現(xiàn)本來(lái)對(duì)象偏向的是線程一使鹅,那么偏向鎖就會(huì)失效揪阶,加的就是輕量級(jí)鎖biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
    biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
    biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
    biasedLockFlag (1bit): 1
    LockFlag (2bit): 01
    LockFlag (2bit): 00
    biasedLockFlag (1bit): 0
    LockFlag (2bit): 01

撤銷 - 調(diào)用 wait/notify

會(huì)使對(duì)象的鎖變成重量級(jí)鎖,因?yàn)閣ait/notify方法之后重量級(jí)鎖才支持

批量重偏向

如果對(duì)象被多個(gè)線程訪問(wèn)患朱,但是沒(méi)有競(jìng)爭(zhēng)升敲,這時(shí)候偏向了線程一的對(duì)象仍有機(jī)會(huì)重新偏向線程二秸弛,會(huì)重置其的線程ID衙猪。

重新偏向條件:當(dāng)撤銷偏向鎖 >= 20次時(shí)

當(dāng)撤銷偏向鎖閾值超過(guò)40次后垂券,該類中所有的實(shí)例對(duì)象都會(huì)變?yōu)椴豢善虻摹?/p>

批量撤銷偏向條件:當(dāng)撤銷偏向鎖 >= 40次時(shí)

鎖消除

Java運(yùn)行時(shí)有JIT即時(shí)編譯器,會(huì)對(duì)Java的字節(jié)碼進(jìn)行進(jìn)一步優(yōu)化执虹,如對(duì)熱點(diǎn)代碼進(jìn)行優(yōu)化拓挥,在優(yōu)化過(guò)程中也會(huì)分析變量

是否會(huì)被共享,如果不可能被共享袋励,就不會(huì)執(zhí)行 synchronized 關(guān)鍵字

總結(jié)

Java6開(kāi)始侥啤,鎖根據(jù)不同情況進(jìn)行了優(yōu)化当叭,偏向鎖(默認(rèn)延時(shí))--> 輕量級(jí)鎖 --> (鎖自旋) --> 重量級(jí)鎖

wait和notify

  • Owener發(fā)現(xiàn)條件不滿足,調(diào)用wait方法盖灸,即可進(jìn)入WitSet變?yōu)閃AITING狀態(tài)
  • BLOCKERD和WAITING狀態(tài)的線程都處于堵塞狀態(tài)蚁鳖,不占用CPU時(shí)間片
  • BLOCKED線程會(huì)在Owner線程釋放鎖時(shí)喚醒
  • WAITING線程會(huì)在Owner線程調(diào)用notify或notifyAll時(shí)喚醒,但喚醒后并不意味著立刻獲得鎖赁炎,仍需要進(jìn)入EntryList重新競(jìng)爭(zhēng)鎖

API介紹

  • obj.wait()讓進(jìn)入Object監(jiān)視器的線程到waitSet等待才睹,如果不喚醒一直等待
  • obj.wait(long timeout) 讓進(jìn)入Object監(jiān)視器的線程到waitSet等待,即使不喚醒超時(shí)也會(huì)被喚醒
  • obj.notify()在Object上正在waitSet等待的線程中挑一個(gè)線程喚醒
  • obj.notifyAll()讓Object上正在waitSet等待的線程的所有線程全部喚醒

前提:這些都是線程之間進(jìn)行協(xié)作的手段甘邀,都屬于Object對(duì)象的方法,調(diào)用前必須獲得此對(duì)象的鎖垮庐。

正確使用方法

final static Object lock = new Object(); //final避免修改
synchronized (lock){
    while(條件不成立){  //所有線程都喚醒后但條件仍可能不成立松邪,while循環(huán)可以再次等待
        lock.wait();
    }
    //繼續(xù)干活
}

synchronized (lock){
    lock.notifyAll(); //避免喚醒錯(cuò)誤線程
}

對(duì)比sleep(long n) 和 wait(long n)

  1. sleep是Thread方法,而是Object的方法
  2. sleep不需要強(qiáng)制和synchronized配合使用哨查,但wait需要
  3. sleep在睡眠的同時(shí)不會(huì)釋放對(duì)象鎖逗抑,但wait在等待時(shí)候會(huì)釋放對(duì)象鎖
  4. 線程的狀態(tài)都是TIME_WAIT

同步模式之保護(hù)性暫停

即 Guarded Suspension,用在一個(gè)線程等待另一個(gè)線程的執(zhí)行結(jié)果寒亥,要點(diǎn):

  1. 有一個(gè)結(jié)果需要從一個(gè)線程傳遞到另一個(gè)線程邮府,讓他們關(guān)聯(lián)同一個(gè) GuardedObject
  2. 如果有結(jié)果不斷從一個(gè)線程到另一個(gè)線程那么可以使用消息隊(duì)列(見(jiàn)生產(chǎn)者/消費(fèi)者)
  3. JDK 中,join 的實(shí)現(xiàn)溉奕、Future 的實(shí)現(xiàn)褂傀,采用的就是此模式
  4. 因?yàn)橐却硪环降慕Y(jié)果,因此歸類到同步模式

1594473284105

@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        GuardedObject guardedObject = new GuardedObject();
        new Thread("t1"){
            @Override
            public void run() {
                log.debug("等待結(jié)果...");
                Object o = guardedObject.get(2000);
                log.debug("獲得了結(jié)果{}",o);
            }
        }.start();
        Thread.sleep(1000);
        log.debug("執(zhí)行下載");
//        guardedObject.complete(new Object());
        guardedObject.complete(null); //虛假喚醒加勤,喚醒線程但條件仍不滿足
    }
}

class GuardedObject {
    // 結(jié)果
    private Object response;

    //獲取結(jié)果
    //timeout表示要等待多久
    public synchronized Object get(long timeout) {
        long begin = System.currentTimeMillis();
        long passedTime = 0;
        while (response == null) {
            long waitTime = timeout - passedTime;
            if (waitTime <= 0)
                break;
            else {
                try {
                    this.wait(waitTime);//參數(shù)為waitTime,避免虛假喚醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //求得經(jīng)歷時(shí)間
                passedTime = System.currentTimeMillis() - begin;
            }
        }
        return response;
    }

    //產(chǎn)生結(jié)果
    public synchronized void complete(Object response){
        this.response =response;
        this.notifyAll();
    }
}

join方法

關(guān)于超時(shí)的增強(qiáng)仙辟,在join(long millis) 的源碼中得到了體現(xiàn):

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) { //join()底層就是調(diào)用了wait(0)但是不是無(wú)限等待而是等待線程代碼執(zhí)行結(jié)束
        while (isAlive()) {
            wait(0);
        }
    } else {
    // join一個(gè)指定的時(shí)間
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

多任務(wù)版 GuardedObject

圖中 Futures 就好比居民樓一層的信箱(每個(gè)信箱有房間編號(hào)),

左側(cè)的 t0鳄梅,t2叠国,t4 就好比等待郵件的【居民】,

右側(cè)的 t1戴尸,t3粟焊,t5 就好比【郵遞員】

如果需要在多個(gè)類之間使用 GuardedObject 對(duì)象,作為參數(shù)傳遞不是很方便孙蒙,

因此設(shè)計(jì)一個(gè)用來(lái)解耦的中間類(信箱)项棠,這樣不僅能夠解耦【結(jié)果等待者】和【結(jié)果生產(chǎn)者】,還能夠同時(shí)支持多個(gè)任務(wù)的管理马篮。

和生產(chǎn)者消費(fèi)者模式的區(qū)別就是:這個(gè)生產(chǎn)者和消費(fèi)者之間是一一對(duì)應(yīng)的關(guān)系沾乘,但是生產(chǎn)者消費(fèi)者模式并不是。rpc框

架的調(diào)用中就使用到了這種模式浑测。

1594518049426

@Slf4j
public class Main {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new People().start();
        }
        Thread.sleep(1000);
        for (Integer id : Mailbox.getIds()) {
            new Postman(id,"內(nèi)容"+id).start();
        }
    }
}
@Slf4j
class Postman extends Thread{
    private int id;
    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        GuardedObject guardedObject = Mailbox.getGuardedObject(id);
        log.debug("開(kāi)始送信...,id為{},內(nèi)容為{}",id,mail);
        guardedObject.complete(mail);
    }
}

@Slf4j
class People extends Thread {
    @Override
    public void run() {
        //收信
        GuardedObject guardedObject = Mailbox.createdObject();
        log.debug("開(kāi)始收信 id:{}", guardedObject.getId());
        Object mail = guardedObject.get(5000);
        log.debug("收到id為{},結(jié)果是{}",guardedObject.getId(),mail);
    }
}

class Mailbox {
    private static Hashtable<Integer, GuardedObject> boxes = new Hashtable<>();

    private static int id = 1;

    private synchronized static int generatedId() {
        return id++;
    }

    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    public static GuardedObject createdObject() {
        GuardedObject object = new GuardedObject(generatedId());
        boxes.put(object.getId(), object);
        return object;
    }

    public static Set<Integer> getIds() {
        return boxes.keySet();
    }

}

class GuardedObject {
    private int id;

    public int getId() {
        return id;
    }

    public GuardedObject(int id) {
        this.id = id;
    }

    // 結(jié)果
    private Object mail;

    //獲取結(jié)果
    //timeout表示要等待多久
    public synchronized Object get(long timeout) {
        long begin = System.currentTimeMillis();
        long passedTime = 0;
        while (mail == null) {
            long waitTime = timeout - passedTime;
            if (waitTime <= 0)
                break;
            else {
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //求得經(jīng)歷時(shí)間
                passedTime = System.currentTimeMillis() - begin;
            }
        }
        return mail;
    }

    //產(chǎn)生結(jié)果
    public synchronized void complete(Object mail){
        this.mail =mail;
        this.notifyAll();
    }
}

異步模式之生產(chǎn)/消費(fèi)者

要點(diǎn)

  1. 與前面的保護(hù)性暫停中的 GuardObject 不同翅阵,不需要產(chǎn)生結(jié)果和消費(fèi)結(jié)果的線程一一對(duì)應(yīng)
  2. 消費(fèi)隊(duì)列可以用來(lái)平衡生產(chǎn)和消費(fèi)的線程資源
  3. 生產(chǎn)者僅負(fù)責(zé)產(chǎn)生結(jié)果數(shù)據(jù)歪玲,不關(guān)心數(shù)據(jù)該如何處理,而消費(fèi)者專心處理結(jié)果數(shù)據(jù)
  4. 消息隊(duì)列是有容量限制的掷匠,滿時(shí)不會(huì)再加入數(shù)據(jù)滥崩,空時(shí)不會(huì)再消耗數(shù)據(jù)
  5. JDK 中各種阻塞隊(duì)列,采用的就是這種模式

“異步”的意思就是生產(chǎn)者產(chǎn)生消息之后消息沒(méi)有被立刻消費(fèi)讹语,而“同步模式”中钙皮,消息在產(chǎn)生之后被立刻消費(fèi)了。

1594524622020

注意:我們寫(xiě)一個(gè)線程間通信的消息隊(duì)列顽决,要注意區(qū)別短条,像rabbit mq等消息框架是進(jìn)程間通信的。

代碼實(shí)現(xiàn)

@Slf4j
public class Main {
    public static void main(String[] args) {
        //創(chuàng)建容量為2的雙向鏈表
        MessageQueue messageQueue = new MessageQueue(2);
        for (int i = 1; i <= 3; i++) {
            int id = i;
            new Thread(() -> {
                while (true) {
                    log.debug(id + "號(hào)生產(chǎn)者開(kāi)始生產(chǎn)了...");
                    messageQueue.put(new Message(id, "內(nèi)容" + id));
                }
            }).start();
        }

        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("消費(fèi)開(kāi)始消費(fèi)了...");
                Message message = messageQueue.take();
                log.debug("消費(fèi)的id為{}內(nèi)容為{}",message.getId(),message.getValue());
            }
        }).start();
    }
}

@Slf4j
class MessageQueue {
    private LinkedList<Message> list = new LinkedList();

    private int capcity;

    public MessageQueue(int capcity) {
        this.capcity = capcity;
    }

    //獲取消息
    public Message take() {
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    log.debug("隊(duì)列為空才菠,消費(fèi)者線程開(kāi)始阻塞...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //從頭部取消息
            Message message = list.removeFirst();
            //通知生產(chǎn)者可以生產(chǎn)了
            log.debug("生產(chǎn)者可以生產(chǎn)了");
            list.notifyAll();
            return message;
        }
    }

    //存入消息
    public void put(Message message) {
        synchronized (list) {
            while (list.size() == capcity) {
                try {
                    log.debug("隊(duì)列已滿茸时,生產(chǎn)者線程開(kāi)始阻塞...");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //從隊(duì)列尾部加入
            list.addLast(message);
            //通知消費(fèi)者可以消費(fèi)了
            log.debug("消費(fèi)者可以消費(fèi)了");
            list.notifyAll();
        }
    }
}

final class Message {
    private int id;
    private Object value;

    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }

    public int getId() {
        return id;
    }
}

park & unpack

基本使用

它們是 LockSupport 類中的方法

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        log.debug("begin...");
        log.debug("park...");
        LockSupport.park();
        log.debug("resume...");
    },"t1");
    t1.start();
    Thread.sleep(1000);
    log.debug("unpark...");
    LockSupport.unpark(t1); //t1線程解鎖
}

20:59:18.776 [t1] DEBUG com.Long.Park.Park - begin...
20:59:18.791 [t1] DEBUG com.Long.Park.Park - park...
20:59:19.789 [main] DEBUG com.Long.Park.Park - unpark...
20:59:19.789 [t1] DEBUG com.Long.Park.Park - resume...

特點(diǎn)

與Object的wait & notify 相比

  • wait,notify 和 notifyAll 必須配合Object Monitor 一起使用赋访,而 park & unpark 不必
  • park & unpark 是以線程為單位 【阻塞】和【喚醒】線程可都,而notify 只能隨機(jī)喚醒一個(gè)等待線程,而 notifyAll 是喚醒所有等待線程蚓耽,就不那么精確渠牲。
  • park & unpark 可以先unpark,而wait & notify不能先 notify 和 notifyAll

原理

每個(gè)線程都有自己的一個(gè) Parker 對(duì)象步悠,由三部分組成 _counter签杈, _cond和 _mutex

  1. 打個(gè)比喻線程就像一個(gè)旅人,Parker 就像他隨身攜帶的背包贤徒,條件變量 _ cond就好比背包中的帳篷芹壕。_counter 就好比背包中的備用干糧(0 為耗盡,1 為充足)

  2. 調(diào)用 park 就是要看需不需要停下來(lái)歇息

  3. 如果備用干糧耗盡接奈,那么鉆進(jìn)帳篷歇息

  4. 如果備用干糧充足踢涌,那么不需停留,繼續(xù)前進(jìn)

  5. 調(diào)用 unpark序宦,就好比令干糧充足

  6. 因?yàn)楸嘲臻g有限睁壁,多次調(diào)用 unpark 僅會(huì)補(bǔ)充一份備用干糧

  7. 如果這時(shí)線程還在帳篷,就喚醒讓他繼續(xù)前進(jìn)

  8. 如果這時(shí)線程還在運(yùn)行互捌,那么下次他調(diào)用 park 時(shí)潘明,僅是消耗掉備用干糧,不需停留繼續(xù)前進(jìn)

先調(diào)用park再調(diào)用upark的過(guò)程

1.先調(diào)用park

  1. 當(dāng)前線程調(diào)用 Unsafe.park() 方法
  2. 檢查 _counter 秕噪,本情況為 0钳降,這時(shí),獲得 _mutex 互斥鎖(mutex對(duì)象有個(gè)等待隊(duì)列 _cond)
  3. 線程進(jìn)入 _cond 條件變量阻塞
  4. 設(shè)置 _counter = 0

1594531894163

2.調(diào)用upark

  1. 調(diào)用 Unsafe.unpark(Thread_0) 方法腌巾,設(shè)置 _counter 為 1
  2. 喚醒 _cond 條件變量中的 Thread_0
  3. Thread_0 恢復(fù)運(yùn)行
  4. 設(shè)置 _counter 為 0

1594532057205

先調(diào)用upark再調(diào)用park的過(guò)程

  1. 調(diào)用 Unsafe.unpark(Thread_0) 方法遂填,設(shè)置 _counter 為 1
  2. 當(dāng)前線程調(diào)用 Unsafe.park() 方法
  3. 檢查 _counter 铲觉,本情況為 1,這時(shí)線程無(wú)需阻塞吓坚,繼續(xù)運(yùn)行
  4. 設(shè)置 _counter 為 0

1594532135616

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

img

①NEW = = > RUNNABLE

  • 調(diào)用線程的start方法時(shí) NEW = = > RUNNABLE

②RUNNABLE < = = > WAITING

  1. 線程用synchronized(obj)獲取了對(duì)象鎖后

  2. 競(jìng)爭(zhēng)鎖成功撵幽,t 線程從WAITING = = >RUNNABLE

  3. 競(jìng)爭(zhēng)鎖失敗,t 線程從WAITING = = > BLOCKED

  4. 調(diào)用obj.wait()方法時(shí)礁击,t 線程從RUNNABLE = = > WAITING

  5. 調(diào)用obj.notify()盐杂,obj.notifyAll(),t.interrupt()時(shí)

③RUNNABLE < = = > WAITING

  • 線程調(diào)用t.join()方法時(shí)(當(dāng)前線程方法內(nèi)在t線程對(duì)象的監(jiān)視器上等待) RUNNABLE = = > WAITING
  • t線程運(yùn)行結(jié)束或調(diào)用當(dāng)前線程的interrupt()方法時(shí) WAITING = = > RUNNABLE

④RUNNABLE < = = > WAITING

  1. 當(dāng)前線程調(diào)用 LockSupport.park() 方法會(huì)讓當(dāng)前線程從 RUNNABLE = = > WAITING
  2. 調(diào)用 LockSupport.unpark(目標(biāo)線程) 或調(diào)用了線程 的 interrupt() 哆窿,會(huì)讓目標(biāo)線程從 WAITING = = > RUNNABLE

⑤RUNNABLE < = = > TIMED_WAITING

t 線程用 synchronized(obj) 獲取了對(duì)象鎖后

  1. 調(diào)用 obj.wait(long n) 方法時(shí)链烈,t 線程從 RUNNABLE = = > TIMED_WAITING

  2. t 線程等待時(shí)間超過(guò)了 n 毫秒,或調(diào)用 obj.notify() 挚躯, obj.notifyAll() 测垛, t.interrupt() 時(shí)

  3. 競(jìng)爭(zhēng)鎖成功,t 線程從 TIMED_WAITING = = > RUNNABLE

  4. 競(jìng)爭(zhēng)鎖失敗秧均,t 線程從 TIMED_WAITING = = > BLOCKED

⑥RUNNABLE < = = > TIMED_WAITING

  1. 當(dāng)前線程調(diào)用 t.join(long n) 方法時(shí),當(dāng)前線程從 RUNNABLE = = >TIMED_WAITING注意是當(dāng)前線程在t 線程對(duì)象的監(jiān)視器上等待
  2. 當(dāng)前線程等待時(shí)間超過(guò)了 n 毫秒号涯,或t 線程運(yùn)行結(jié)束目胡,或調(diào)用了當(dāng)前線程的 interrupt() 時(shí),當(dāng)前線程從TIMED_WAITING = = > RUNNABLE

⑦RUNNABLE < = = > TIMED_WAITING

  1. 當(dāng)前線程調(diào)用 Thread.sleep(long n) 链快,當(dāng)前線程從 RUNNABLE = = > TIMED_WAITING
  2. 當(dāng)前線程等待時(shí)間超過(guò)了 n 毫秒或調(diào)用了線程的interrupt() 誉己,當(dāng)前線程從 TIMED_WAITING = = > RUNNABLE

⑧RUNNABLE < = = > TIMED_WAITING

  1. 當(dāng)前線程調(diào)用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(longmillis) 時(shí),當(dāng)前線程從 RUNNABLE = = > TIMED_WAITING
  2. 調(diào)用 LockSupport.unpark(目標(biāo)線程) 或調(diào)用了線程 的 interrupt() 域蜗,或是等待超時(shí)巨双,會(huì)讓目標(biāo)線程從TIMED_WAITING = = > RUNNABLE

⑨RUNNABLE < = = > BLOCKED

  • t線程用synchronized(obj)獲取了對(duì)象鎖如果競(jìng)爭(zhēng)失敗,RUNNABLE = = > BLOCKED
  • 持obj鎖線程的同步代碼塊執(zhí)行完畢霉祸,會(huì)喚醒該對(duì)象上所有BLOCKED的線程重新競(jìng)爭(zhēng)筑累,如果其中t線程競(jìng)爭(zhēng)成功從BLOCKED = = > RUNNABLE,其他失敗的線程仍然是BLOCKED

⑩RUNNABLE < = = > TIMED_WAITING

當(dāng)前線程所有代碼運(yùn)行完畢丝蹭,進(jìn)入TIMED_WAITING

多把鎖【增加并發(fā)度】

只使用一把鎖的情況下慢宗,不相干的任務(wù)也會(huì)等待,比如一個(gè)線程睡覺(jué)和一個(gè)線程學(xué)習(xí)

這種情況下奔穿,睡覺(jué)和學(xué)習(xí)不應(yīng)該只是一把鎖镜沽,應(yīng)該分開(kāi),兩者便可并發(fā)執(zhí)行贱田。

但使用多把鎖既有好處缅茉,也有壞處

  • 優(yōu)點(diǎn)可以增加并發(fā)度,即不相關(guān)的任務(wù)可以并行
  • 缺點(diǎn)會(huì)造成死鎖等問(wèn)題
@Slf4j
public class MultiLock {
    private final Object studyLock = new Object(); //學(xué)習(xí)鎖
    private final Object sleepLock = new Object(); //睡覺(jué)鎖

    public void study() throws InterruptedException {
        synchronized (studyLock){
            log.debug(Thread.currentThread().getName()+"在學(xué)習(xí)男摧!");
            Thread.sleep(2000);
            log.debug("學(xué)習(xí)結(jié)束蔬墩!");
        }
    }

    public void sleep() throws InterruptedException {
        synchronized (sleepLock){
            log.debug(Thread.currentThread().getName()+"在睡覺(jué)译打!");
            Thread.sleep(1000);
            log.debug("睡覺(jué)結(jié)束!");
        }
    }
}
class Test{
    public static void main(String[] args) {
        MultiLock multiLock = new MultiLock();
        new Thread(()->{
            try {
                multiLock.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                multiLock.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

活躍性

線程沒(méi)有按預(yù)期結(jié)束筹我,執(zhí)行不下去的情況扶平,歸類為【活躍性】問(wèn)題,除了死鎖以外蔬蕊,還有活鎖和饑餓者兩種情況

死鎖

一個(gè)線程在占用一個(gè)鎖的同時(shí)需要獲取多把鎖,但這些鎖被其他線程占用结澄,其他線程也需要該線程占用的鎖,就會(huì)陷入死循環(huán)岸夯,無(wú)限等待麻献,造成死鎖問(wèn)題,

  • t1線程需要獲取A對(duì)象鎖,接下來(lái)要獲取B對(duì)象鎖猜扮,但被t2線程占用
  • t2線程需要獲取B對(duì)象鎖,接下來(lái)要獲取A對(duì)象鎖勉吻,但被t1線程占用

檢測(cè)方法

  1. 檢測(cè)死鎖可以使用 jconsole工具;
  2. 使用 jps 定位進(jìn)程 id旅赢,再用 jstack 定位死鎖

哲學(xué)家就餐問(wèn)題

1594553609905

有五位哲學(xué)家齿桃,圍坐在圓桌旁。他們只做兩件事煮盼,思考和吃飯短纵,思考一會(huì)吃口飯,吃完飯后接著思考僵控。

吃飯時(shí)要用兩根筷子吃香到,桌上共有 5 根筷子,每位哲學(xué)家左右手邊各有一根筷子报破。如果筷子被身邊的人拿著悠就,自己就得等待

代碼實(shí)現(xiàn)

public class Main {
    public static void main(String[] args) {
        chopstick c1 = new chopstick("c1");
        chopstick c2 = new chopstick("c2");
        chopstick c3 = new chopstick("c3");
        chopstick c4 = new chopstick("c4");
        chopstick c5 = new chopstick("c5");
        new philosopher("阿基米德",c1,c2).start();
        new philosopher("蘇格拉底",c2,c3).start();
        new philosopher("柏拉圖",c3,c4).start();
        new philosopher("黑格爾",c4,c5).start();
        new philosopher("亞里士多德",c5,c1).start();
    }
}
class chopstick{
    private String name;

    public chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}

@Slf4j
class philosopher extends Thread{
    private chopstick left;
    private chopstick right;

    public philosopher(String name,chopstick left,chopstick right){
        super(name);
        this.left=left;
        this.right=right;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (left){
                synchronized (right){
                    eat();
                }
            }
        }
    }

    private void eat()  {
        log.debug(Thread.currentThread().getName()+"開(kāi)始吃飯了");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

當(dāng)每個(gè)哲學(xué)家即線程持有一根筷子時(shí),他們都在等待另一個(gè)線程釋放鎖充易,因此造成了死鎖梗脾。

活鎖

出現(xiàn)在兩個(gè)線程互相改變對(duì)方的結(jié)束條件(二者條件互斥),最后誰(shuí)也無(wú)法結(jié)束盹靴。

結(jié)束條件count<20, 初始 count =25

  • t1線程執(zhí)行count++操作
  • t2線程并行執(zhí)行 count-- 操作

解決辦法:

設(shè)置隨機(jī)的睡眠時(shí)間藐唠,避免類似加減這樣的操作一起執(zhí)行

饑餓

很多教程中把饑餓定義為,一個(gè)線程由于優(yōu)先級(jí)太低鹉究,始終得不到 CPU 調(diào)度執(zhí)行宇立,也不能夠結(jié)束,饑餓的情況不易演

示自赔,講讀寫(xiě)鎖時(shí)會(huì)涉及饑餓問(wèn)題妈嘹。

一個(gè)線程饑餓的例子:

先來(lái)看看使用順序加鎖的方式解決之前的死鎖問(wèn)題,就是兩個(gè)線程對(duì)兩個(gè)不同的對(duì)象加鎖的時(shí)候都使用相同的順序進(jìn)行

加鎖绍妨。但是這樣又會(huì)產(chǎn)生饑餓問(wèn)題

1594558469826

順序加鎖的解決方案(會(huì)造成饑餓問(wèn)題,其中一個(gè)線程會(huì)一直執(zhí)行润脸,其他線程執(zhí)行的機(jī)會(huì)大大減少)

ReentrantLock (JUC下的類)

相對(duì)于 synchronized 它具備如下特點(diǎn)

  1. 可中斷
  2. 可以設(shè)置超時(shí)時(shí)間
  3. 可以設(shè)置為公平鎖
  4. 支持多個(gè)條件變量柬脸,即<typo id="typo-29757" data-origin="對(duì)與" ignoretag="true">對(duì)與</typo>不滿足條件的線程可以放到不同的集合中等待

與 synchronized 一樣,都支持可重入

基本語(yǔ)法

// 獲取鎖
reentrantLock.lock();
try {
 // 臨界區(qū)
} finally {
 // 釋放鎖
 reentrantLock.unlock();
}

可重入

可重入是指同一個(gè)線程如果首次獲得了這把鎖毙驯,那么它就是這把鎖的擁有者倒堕,因此有權(quán)利再次獲取這把鎖

如果是不可重入鎖,那么第二次獲得鎖時(shí)爆价,自己也會(huì)被鎖擋住

可打斷

鎖超時(shí)

使用鎖超時(shí)解決哲學(xué)家就餐死鎖問(wèn)題:

公平鎖

synchronized鎖中垦巴,在entrylist等待的鎖在競(jìng)爭(zhēng)時(shí)不是按照先到先得來(lái)獲取鎖的,所以說(shuō)synchronized鎖時(shí)不公平的铭段;ReentranLock鎖默認(rèn)是不公平的骤宣,但是可以通過(guò)設(shè)置實(shí)現(xiàn)公平鎖。本意是為了解決之前提到的饑餓問(wèn)題序愚,但是公平鎖一般沒(méi)有必要憔披,會(huì)降低并發(fā)度,使用trylock也可以實(shí)現(xiàn)爸吮。

條件變量(休息室)

synchronized 中也有條件變量芬膝,就是我們講原理時(shí)那個(gè) waitSet 休息室,當(dāng)條件不滿足時(shí)進(jìn)入 waitSet 等待 ReentrantLock 的條件變量比 synchronized 強(qiáng)大之處在于形娇,它是支持多個(gè)條件變量的蔗候,這就好比

  1. synchronized 是那些不滿足條件的線程都在一間休息室等消息
  2. 而 ReentrantLock 支持多間休息室,有專門等煙的休息室埂软、專門等早餐的休息室、喚醒時(shí)也是按休息室來(lái)喚 醒

使用要點(diǎn):

  1. await 前需要獲得鎖
  2. await 執(zhí)行后纫事,會(huì)釋放鎖勘畔,進(jìn)入 conditionObject 等待
  3. await 的線程被喚醒(或打斷、或超時(shí))<typo id="typo-30493" data-origin="取" ignoretag="true">取</typo>重新競(jìng)爭(zhēng) lock 鎖丽惶,執(zhí)行喚醒的線程<typo id="typo-30513" data-origin="爺" ignoretag="true">爺</typo>必須先獲得鎖
  4. 競(jìng)爭(zhēng) lock 鎖成功后炫七,從 await 后繼續(xù)執(zhí)行

同步模式之順序控制

  1. 固定運(yùn)行順序,比如钾唬,必須先 2 后 1 打印

  2. wait notify 版

  3. Park Unpark 版

  4. Lock 條件變量版

  5. 交替輸出万哪,線程 1 輸出 a 5 次,線程 2 輸出 b 5 次抡秆,線程 3 輸出 c 5 次∞任。現(xiàn)在要求輸出 abcabcabcabcabc 怎么實(shí)現(xiàn)

  6. wait notify 版

  7. Lock 條件變量版

  8. Park Unpark 版

本章小結(jié)

本章我們需要重點(diǎn)掌握的是

  1. 分析多線程訪問(wèn)共享資源時(shí),哪些代碼片段屬于臨界區(qū)

  2. 使用 synchronized 互斥解決臨界區(qū)的線程安全問(wèn)題

  3. 掌握 synchronized 鎖對(duì)象語(yǔ)法

  4. 掌握 synchronzied 加載成員方法和靜態(tài)方法語(yǔ)法

  5. 掌握 wait/notify 同步方法

  6. 使用 lock 互斥解決臨界區(qū)的線程安全問(wèn)題 掌握 lock 的使用細(xì)節(jié):可打斷儒士、鎖超時(shí)的止、公平鎖、條件變量

  7. 學(xué)會(huì)分析變量的線程安全性着撩、掌握常見(jiàn)線程安全類的使用

  8. 了解線程活躍性問(wèn)題:死鎖诅福、活鎖匾委、饑餓

  9. 應(yīng)用方面

  10. 互斥:使用 synchronized 或 Lock 達(dá)到共享資源互斥效果,實(shí)現(xiàn)原子性效果氓润,保證線程安全赂乐。

  11. 同步:使用 wait/notify 或 Lock 的條件變量來(lái)達(dá)到線程間通信效果。

  12. 原理方面

  13. monitor咖气、synchronized 挨措、wait/notify 原理

  14. synchronized 進(jìn)階原理

  15. park & unpark 原理

  16. 模式方面

  17. 同步模式之保護(hù)性暫停

  18. 異步模式之生產(chǎn)者消費(fèi)者

  19. 同步模式之順序控制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市采章,隨后出現(xiàn)的幾起案子运嗜,更是在濱河造成了極大的恐慌,老刑警劉巖悯舟,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件担租,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡抵怎,警方通過(guò)查閱死者的電腦和手機(jī)奋救,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)反惕,“玉大人尝艘,你說(shuō)我怎么就攤上這事∽巳荆” “怎么了背亥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)悬赏。 經(jīng)常有香客問(wèn)我狡汉,道長(zhǎng),這世上最難降的妖魔是什么闽颇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任盾戴,我火速辦了婚禮,結(jié)果婚禮上兵多,老公的妹妹穿的比我還像新娘尖啡。我一直安慰自己,他們只是感情好剩膘,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布衅斩。 她就那樣靜靜地躺著,像睡著了一般怠褐。 火紅的嫁衣襯著肌膚如雪矛渴。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,549評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音具温,去河邊找鬼蚕涤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛铣猩,可吹牛的內(nèi)容都是我干的揖铜。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼达皿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼天吓!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起峦椰,我...
    開(kāi)封第一講書(shū)人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤龄寞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后汤功,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體物邑,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年滔金,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了色解。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡餐茵,死狀恐怖科阎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忿族,我是刑警寧澤锣笨,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站道批,受9級(jí)特大地震影響错英,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屹徘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衅金。 院中可真熱鬧噪伊,春花似錦、人聲如沸氮唯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惩琉。三九已至豆励,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背良蒸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工技扼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫩痰。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓剿吻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親串纺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子丽旅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

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