1. 程式人生 > >【從入門到放棄-Java】併發程式設計-鎖-synchronized

【從入門到放棄-Java】併發程式設計-鎖-synchronized

簡介

上篇【從入門到放棄-Java】併發程式設計-執行緒安全中,我們瞭解到,可以通過加鎖機制來保護共享物件,來實現執行緒安全。

synchronized是java提供的一種內建的鎖機制。通過synchronized關鍵字同步程式碼塊。執行緒在進入同步程式碼塊之前會自動獲得鎖,並在退出同步程式碼塊時自動釋放鎖。內建鎖是一種互斥鎖。

本文來深入學習下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依然會得到正確的結果。

同步程式碼塊

鎖當前例項物件

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同步方法塊時,鎖的是例項物件時,如上示例在不同的例項中呼叫此方法還是會出現執行緒安全問題。

鎖其它例項物件

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同步方法塊時,鎖的是類物件時,如上示例在不同的例項中呼叫此方法是執行緒安全的。

鎖機制

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_

原文連結

本文為雲棲社群原創內容,未經