【從入門到放棄-Java】併發編程-鎖-synchronized

簡介

上篇【從入門到放棄-Java】併發編程-線程安全中,咱們瞭解到,能夠經過加鎖機制來保護共享對象,來實現線程安全。html

synchronized是java提供的一種內置的鎖機制。經過synchronized關鍵字同步代碼塊。線程在進入同步代碼塊以前會自動得到鎖,並在退出同步代碼塊時自動釋放鎖。內置鎖是一種互斥鎖。java

本文來深刻學習下synchronized。編程

使用

同步方法

同步非靜態方法

public class Synchronized {
    private static int count;

    private synchronized void add1() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();

            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

結果符合預期:synchronized做用於非靜態方法,鎖定的是實例對象,如上所示鎖的是sync對象,所以線程可以正確的運行,count的結果總會是20000。安全

public class Synchronized {
    private static int count;

    private synchronized void add1() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

結果不符合預期:如上所示,做用於非靜態方法,鎖的是實例化對象,所以當sync和sync1同時運行時,仍是會出現線程安全問題,由於鎖的是兩個不一樣的實例化對象。併發

同步靜態方法

public class Synchronized {
    private static int count;

    private static synchronized void add1() {
        count++;
        System.out.println(count);
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                Synchronized.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                Synchronized.add11();

            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

結果符合預期:鎖靜態方法時,鎖的是類對象。所以在不一樣的線程中調用add1和add11依然會獲得正確的結果。jvm

同步代碼塊

鎖當前實例對象

public class Synchronized {
    private static int count;

    private void add1() {
        synchronized (this) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

結果不符合預期:當synchronized同步方法塊時,鎖的是實例對象時,如上示例在不一樣的實例中調用此方法仍是會出現線程安全問題。ide

鎖其它實例對象

public class Synchronized {
    private static int count;
    public String lock = new String();

    private void add1() {
        synchronized (lock) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);

        System.out.println(sync.lock == sync1.lock);
    }
}

結果不符合預期:當synchronized同步方法塊時,鎖的是其它實例對象時,如上示例在不一樣的實例中調用此方法仍是會出現線程安全問題。性能

public class Synchronized {
    private static int count;
    public String lock = "";

    private void add1() {
        synchronized (lock) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);

        System.out.println(sync.lock == sync1.lock);
    }
}

結果符合預期:當synchronized同步方法塊時,鎖的雖然是其它實例對象時,但已上實例中,由於String = "" 是存放在常量池中的,實際上鎖的仍是相同的對象,所以是線程安全的學習

鎖類對象

public class Synchronized {
    private static int count;

    private void add1() {
        synchronized (Synchronized.class) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

結果符合預期:當synchronized同步方法塊時,鎖的是類對象時,如上示例在不一樣的實例中調用此方法是線程安全的。this

鎖機制

public class Synchronized {
    private static int count;

    public static void main(String[] args) throws InterruptedException {
        synchronized (Synchronized.class) {
            count++;
        }
    }
}

使用javap -v Synchronized.class反編譯class文件。

能夠看到synchronized其實是經過monitorenter和monitorexit來實現鎖機制的。同一時刻,只能有一個線程進入監視區。從而保證線程的同步。

正常狀況下在指令4進入監視區,指令14退出監視區而後指令15直接跳到指令23 return

可是在異常狀況下異常都會跳轉到指令18,依次執行到指令20monitorexit釋放鎖,防止出現異常時未釋放的狀況。
這其實也是synchronized的優勢:不管代碼執行狀況如何,都不會忘記主動釋放鎖。

想了解Monitors更多的原理能夠點擊查看

鎖升級

由於monitor依賴操做系統的Mutex lock實現,是一個比較重的操做,須要切換系統至內核態,開銷很是大。所以在jdk1.6引入了偏向鎖和輕量級鎖。
synchronized有四種狀態:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖。

無鎖

沒有對資源進行鎖定,全部線程都能訪問和修改。但同時只有一個線程能修改爲功

偏向鎖

在鎖競爭不強烈的狀況下,一般一個線程會屢次獲取同一個鎖,爲了減小獲取鎖的代價 引入了偏向鎖,會在java對象頭中記錄獲取鎖的線程的threadID。

  • 當線程發現對象頭的threadID存在時。判斷與當前線程是不是同一線程。
  • 若是是則不須要再次加、解鎖。
  • 若是不是,則判斷threadID是否存活。不存活:設置爲無鎖狀態,其餘線程競爭設置偏向鎖。存活:查找threadID堆棧信息判斷是否須要繼續持有鎖。須要持有則升級threadID線程的鎖爲輕量級鎖。不須要持有則撤銷鎖,設置爲無鎖狀態等待其它線程競爭。

由於偏向鎖的撤銷操做仍是比較重的,致使進入安全點,所以在競爭比較激烈時,會影響性能,可使用-XX:-UseBiasedLocking=false禁用偏向鎖。

輕量級鎖

當偏向鎖升級爲輕量級鎖時,其它線程嘗試經過CAS方式設置對象頭來獲取鎖。

  • 會先在當前線程的棧幀中設置Lock Record,用於存儲當前對象頭中的mark word的拷貝。
  • 複製mark word的內容到lock record,並嘗試使用cas將mark word的指針指向lock record
  • 若是替換成功,則獲取偏向鎖
  • 替換不成功,則會自旋重試必定次數。
  • 自旋必定次數或有新的線程來競爭鎖時,輕量級鎖膨脹爲重量級鎖。

CAS

CAS即compare and swap(比較並替換)。是一種樂觀鎖機制。一般有三個值

  • V:內存中的實際值
  • A:舊的預期值
  • B:要修改的新值
    即V與A相等時,則替換V爲B。即內存中的實際值與咱們的預期值相等時,則替換爲新值。

CAS可能遇到ABA問題,即內存中的值爲A,變爲B後,又變爲了A,此時A爲新值,不該該替換。
能夠採起:A-1,B-2,A-3的方式來避免這個問題

重量級鎖

自旋是消耗CPU的,所以在自旋一段時間,或者一個線程在自旋時,又有新的線程來競爭鎖,則輕量級鎖會膨脹爲重量級鎖。
重量級鎖,經過monitor實現,monitor底層實際是依賴操做系統的mutex lock(互斥鎖)實現。
須要從用戶態,切換爲內核態,成本比較高

總結

本文咱們一塊兒學習了

  • synchronized的幾種用法:同步方法、同步代碼塊。其實是同步類或同步實例對象。
  • 鎖升級:無鎖、偏向鎖、輕量級鎖、重量級鎖以及其膨脹過程。

synchronized做爲內置鎖,雖然幫咱們解決了線程安全問題,可是帶來了性能的損失,所以必定不能濫用。使用時請注意同步塊的做用範圍。一般,做用範圍越小,對性能的影響也就越小(注意權衡獲取、釋放鎖的成本,不能爲了縮小做用範圍,而頻繁的獲取、釋放)。



本文做者:aloof_

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索