Java 8 併發篇 - 冷靜分析 Synchronized(上)

1.Java的鎖

1.1 鎖的內存語義

  • 鎖可讓臨界區互斥執行,還可讓釋放鎖的線程向同一個鎖的線程發送消息
  • 鎖的釋放要遵循Happens-before原則(鎖規則:解鎖必然發生在隨後的加鎖以前
  • 鎖在Java中的具體表現是 SynchronizedLock

1.2 鎖的釋放

線程A釋放鎖後,會將共享變動操做刷新到主內存中

1.3 鎖的獲取

線程B獲取鎖時,JMM會將該線程的本地內存置爲無效,被監視器保護的臨界區代碼必須從主內存中讀取共享變量

1.4 鎖的釋放與獲取

  • 鎖獲取與volatile讀有相同的內存語義,讀者可參見筆者的 併發番@Java內存模型&Volatile一文通(1.7版)
  • 線程A釋放一個鎖,實質是線程A告知下一個獲取到該鎖的某個線程其已變動該共享變量
  • 線程B獲取一個鎖,實質是線程B獲得了線程A告知其(在釋放鎖以前)變動共享變量的消息
  • 線程A釋放鎖,隨後線程B競爭到該鎖,實質是線程A經過主內存向線程B發消息告知其變動了共享變量

2.Synchronized的綜述

  • 同步機制: synchronized是Java同步機制的一種實現,即互斥鎖機制,它所得到的鎖叫作互斥鎖
  • 互斥鎖: 指的是每一個對象的鎖一次只能分配給一個線程,同一時間只能由一個線程佔用
  • 做用: synchronized用於保證同一時刻只能由一個線程進入到臨界區,同時保證共享變量的可見性、原子性和有序性
  • 使用: 當一個線程試圖訪問同步代碼方法(塊)時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖

3.Synchronized的使用

3.1 Synchronized的三種應用方式

補充: 使用同步代碼塊的好處在於其餘線程仍能夠訪問非synchronized(this)的同步代碼塊

3.2 Synchronized的使用規則

/**
  * 先定義一個測試模板類
  *     這裏補充一個知識點:Thread.sleep(long)不會釋放鎖
  *     讀者可參見筆者的`併發番@Thread一文通`
  */ 
public class SynchronizedDemo {
    public static synchronized void staticMethod(){
        System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問靜態同步方法staticMethod");
    }
    public static void staticMethod2(){
        System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod2");
        synchronized (SynchronizedDemo.class){
            System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中獲取了SynchronizedDemo.class");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void synMethod(){
        System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod");
    }
    public synchronized void synMethod2(){
        System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod2");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod2");
    }
    public void method(){
        System.out.println(Thread.currentThread().getName() + "訪問了普通方法method");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問普通方法method");
    }
    private Object lock = new Object();
    public void chunkMethod(){
        System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中獲取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod2(){
        System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod2方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中獲取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod3(){
        System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod3方法");
        //同步代碼塊
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中獲取了this");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void stringMethod(String lock){
        synchronized (lock){
            while (true){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製代碼

3.2.1 普通方法與同步方法調用互不關聯

當一個線程進入同步方法時,其餘線程能夠正常訪問其餘非同步方法
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //調用普通方法
        synDemo.method();
    });
    Thread thread2 = new Thread(() -> {
        //調用同步方法
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-1訪問了同步方法synMethod
Thread-0訪問了普通方法method
Thread-0結束訪問普通方法method
Thread-1結束訪問同步方法synMethod
//分析:經過結果可知,普通方法和同步方法是非阻塞執行的
複製代碼

3.2.2 全部同步方法只能被一個線程訪問

當一個線程執行同步方法時,其餘線程不能訪問任何同步方法
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
//分析:經過結果可知,任務的執行是阻塞的,顯然Thread-1必須等待Thread-0執行完畢以後才能繼續執行
複製代碼

3.2.3 同一個鎖的同步代碼塊同一時刻只能被一個線程訪問

當同步代碼塊都是同一個鎖時,方法能夠被全部線程訪問,但同一個鎖的同步代碼塊同一時刻只能被一個線程訪問
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod();
        synDemo.synMethod2();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock  
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
//分析可知:
//1.對比18行和19行可知,即便普通方法有同步代碼塊,但方法的訪問是非阻塞的,任何線程均可以自由進入
//2.對比20行、22行以及25行和27行可知,對於同一個鎖的同步代碼塊的訪問必定是阻塞的
複製代碼


3.2.4 線程間同時訪問同一個鎖的多個同步代碼的執行順序不定

  • 線程間同時訪問同一個鎖多個同步代碼的執行順序不定,即便是使用同一個對象鎖,這點跟同步方法有很大差別
  • ??讀者能夠先思考爲何會出現這樣的問題??
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod2();
        synDemo.chunkMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod2方法
Thread-0在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock

//分析可知:
//現象:對比20行、22行和24行、25行可知,雖然是同一個lock對象,但其不一樣代碼塊的訪問是非阻塞的
//緣由:根源在於鎖的釋放和從新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,依次類推到24行、25行時,Thread-0又從新獲取到鎖優先執行了
//注意:但有一點是必須的,對於同一個鎖的同步代碼塊的訪問必定是阻塞的
//補充:同步方法之全部會被所有阻塞,是由於synDemo對象一直被線程在內部把持住就沒釋放過,論把持住的重要性!
複製代碼

3.2.5 不一樣鎖之間訪問非阻塞

  • 因爲三種使用方式的鎖對象都不同,所以相互之間不會有任何影響
  • 但有兩種狀況除外:
    • 1.當同步代碼塊使用的Class對象和類對象一致時屬於同一個鎖,遵循上面的3.2.3原則
    • 2.當同步代碼塊使用的是this,即與同步方法使用鎖屬於同一個鎖,遵循上面的3.2.23.2.3原則
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
    Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
    Thread thread3 = new Thread(() -> staticMethod());
    Thread thread4 = new Thread(() -> staticMethod2());
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
}
---------------------
//輸出:
Thread-1訪問了chunkMethod3方法
Thread-1在chunkMethod3方法中獲取了this
Thread-2訪問了靜態同步方法staticMethod
Thread-0訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock
Thread-3訪問了靜態同步方法staticMethod2
...停頓等待...
Thread-2結束訪問靜態同步方法staticMethod
Thread-3在staticMethod2方法中獲取了SynchronizedDemo.class
//分析可知:
//現象:對比16行、18行和24行、25行可知,雖然是同一個lock對象,但其不一樣代碼塊的訪問是非阻塞的
//緣由:根源在於鎖的釋放和從新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,依次類推到24行、25行時,Thread-0又從新獲取到鎖優先執行了
複製代碼

3.3 Synchronized的可重入性

  • 重入鎖:當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入鎖,請求將會成功
  • 實現:一個線程獲得一個對象鎖後再次請求該對象鎖,是容許的,每重入一次,monitor進入次數+1
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
//分析:對比16行和18行可知,在代碼塊中繼續調用了當前實例對象的另一個同步方法,再次請求當前實例鎖時,將被容許,進而執行方法體代碼,這就是重入鎖最直接的體現
複製代碼

3.4 Synchronized與String鎖

  • 隱患:因爲在JVM中具備String常量池緩存的功能,所以相同字面量是同一個鎖!!!
  • 注意:嚴重不推薦將String做爲鎖對象,而應該改用其餘非緩存對象
  • 提示:對字面量有疑問的話請先回顧一下String的基礎,這裏不加以解釋
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
    Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0
Thread-0
Thread-0
Thread-0
...死循環...
//分析:輸出結果永遠都是Thread-0的死循環,也就是說另外一個線程,即Thread-1線程根本不會運行
//緣由:同步塊中的鎖是同一個字面量
複製代碼

3.5 Synchronized與不可變鎖

  • 隱患:當使用不可變類對象(final Class)做爲對象鎖時,使用synchronized一樣會有併發問題
  • 緣由:因爲不可變特性,看成爲鎖但同步塊內部仍然有計算操做,會生成一個新的鎖對象
  • 注意:嚴重不推薦將final Class做爲鎖對象時仍對其有計算操做
  • 補充:雖然String也是final Class,但它的緣由倒是字面量常量池
public class SynchronizedDemo {
    static Integer i = 0;   //Integer是final Class
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int j = 0;j<10000;j++){
                    synchronized (i){
                        i++;
                    }
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}
---------------------
//輸出:
14134
//分析:跟預想中的20000不一致,當使用Integer做爲對象鎖時但還有計算操做就會出現併發問題
複製代碼

咱們經過反編譯發現執行i++操做至關於執行了i = Integer.valueOf(i.intValue()+1)緩存

經過查看Integer的valueOf方法實現可知,其每次都new了一個新的Integer對象,鎖變了有木有!!!bash

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);  //每次都new一個新的鎖有木有!!!
}
複製代碼

3.6 Synchronized與死鎖

  • 死鎖:當線程間須要相互等待對方已持有的鎖時,就造成死鎖,進而產生死循環
  • 注意: 代碼中嚴禁出現死鎖!!!
public static void main(String[] args) {
    Object lock = new Object();
    Object lock2 = new Object();
    Thread thread1 = new Thread(() -> {
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2){
                System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
            }
        }
    });
    Thread thread2 = new Thread(() -> {
        synchronized (lock2){
            System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock){
                System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
            }
        }
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-1獲取到lock2鎖
Thread-0獲取到lock鎖
.....
//分析:線程0得到lock鎖,線程1得到lock2鎖,但以後因爲兩個線程還要獲取對方已持有的鎖,但已持有的鎖都不會被雙方釋放,線程"假死",沒法往下執行,從而造成死循環,即死鎖,以後一直在作無用的死循環,嚴重浪費系統資源
複製代碼

咱們用 jstack 查看一下這個任務的各個線程運行狀況,能夠發現兩個線程都被阻塞 BLOCKED併發

咱們很明顯的發現,Java-level=deadlock,即死鎖,兩個線程相互等待對方的鎖app


Synchronized一文通(1.8版) 黃志鵬kira 創做,採用 知識共享 署名-非商業性使用 4.0 國際 許可協議進行許可。
相關文章
相關標籤/搜索