Java Synchronized同步不正確的用法

    1.概述

    Java中的同步對於擺脫多線程問題很有幫助。但是,如果不仔細使用同步原理,可能會給我們帶來很多麻煩。

    在本教程中,我們將討論與同步相關的一些不良做法以及每種用例的更好方法。

    2.同步原理

    通常,我們應該僅在確定沒有外部代碼鎖定的對像上進行同步

    換句話說,使用池化或可重用的對象進行同步是一種不好的做法。原因是JVM中的其他進程可以訪問池化/可重用的對象,並且外部/不受信任的代碼對此類對象進行的任何修改都可能導致死鎖和不確定性行為。

    現在,讓我們討論基於某些類型的同步原理,例如StringBooleanIntegerObject

    3. String字符串

    3.1 不良做法

    字符串文字被合併,並經常在Java中重用。因此,建議不要將String類型與synchronized關鍵字一起使用來進行同步:

    public void stringBadPractice1() {
    
     String stringLock = "LOCK_STRING";
    
     synchronized (stringLock) {
    
     // ...
    
     }
    
     }

    同樣,如果我們使用private final String文字,則仍從常量池中引用它:

    private final String stringLock = "LOCK_STRING";
    
     public void stringBadPractice2() {
    
     synchronized (stringLock) {
    
     // ...
    
     }
    
     }

    此外,它被認為是不好的做法, internString進行同步:

    private final String internedStringLock = new String("LOCK_STRING").intern();
    
     public void stringBadPractice3() {
    
     synchronized (internedStringLock) {
    
     // ...
    
     }
    
     }

    根據Javadocsintern方法為我們提供String對象的規範表示。換句話說, intern方法從池中返回一個String ,並將其顯式添加到池中(如果不存在),其內容與此String相同。

    因此,對於可重複使用的對象,同步問題對於持久化的String像也仍然存在。

    注意:所有String文字和字符串值常量表達式都將自動被interned

    3.2 解決辦法

    建議避免對String文字進行同步的不良做法,建議使用new關鍵字創建String的新實例

    讓我們用已經討論過的代碼解決問題。首先,我們將創建一個新的String對象,使其具有唯一的引用(以避免任何重用)和它自己的固有鎖定,這有助於同步。

    然後,我們將對象保持privatefinal以防止任何外部/不受信任的代碼訪問它:

    private final String stringLock = new String("LOCK_STRING");
    
     public void stringSolution() {
    
     synchronized (stringLock) {
    
     // ...
    
     }
    
     }

    4. Boolean類型

    具有兩個值truefalseBoolean類型不適用於鎖定目的。類似於JVM中的String文字, boolean文字值還共享Boolean類的唯一實例。

    讓我們看一個錯誤的代碼示例,該示例在Boolean鎖定對像上進行同步:

    private final Boolean booleanLock = Boolean.FALSE;
    
     public void booleanBadPractice() {
    
     synchronized (booleanLock) {
    
     // ...
    
     }
    
     }

    在這裡,如果任何外部代碼也在具有相同值的Boolean文字上同步,則係統可能變得無響應或導致死鎖情況。

    因此,我們不建議將Boolean對像用作同步鎖。

    5.裝箱基本類型

    5.1。不良做法

    類似於boolean文字,裝箱的類型可能會將實例重用於某些值。原因是JVM緩存並共享可以表示為字節的值。

    例如,讓我們寫一個糟糕的代碼示例,對盒裝類型Integer同步:

    private int count = 0;
    
     private final Integer intLock = count;
    
     public void boxedPrimitiveBadPractice() {
    
     synchronized (intLock) {
    
     count++;
    
     // ...
    
     }
    
     }

    5.2 解決辦法

    但是,與boolean文字不同,對盒裝原語進行同步的解決方案是創建一個新實例。

    String像類似,我們應該使用new關鍵字創建具有自己的內部鎖的Integer對象的唯一實例,並將其保持privatefinal

    private int count = 0;
    
     private final Integer intLock = new Integer(count);
    
     public void boxedPrimitiveSolution() {
    
     synchronized (intLock) {
    
     count++;
    
     // ...
    
     }
    
     }

    6.this同步

    當類使用this關鍵字實現方法同步或阻止同步時,JVM將對象本身用作監視器(其內部鎖)。

    不受信任的代碼可以獲取並無限期持有可訪問類的內部鎖。因此,這可能導致死鎖情況。

    6.1。不良做法

    例如,讓我們使用synchronized方法setName和帶有synchronized塊的方法setOwner創建Animal類:

    public class Animal {
    
     private String name;
    
     private String owner;
    
    
    
     // getters and constructors
    
    
    
     public synchronized void setName(String name) {
    
     this.name = name;
    
     }
    
    
    
     public void setOwner(String owner) {
    
     synchronized (this) {
    
     this.owner = owner;
    
     }
    
     }
    
     }

    現在,讓我們編寫一些錯誤的代碼來創建Animal類的實例並對其進行同步:

    Animal animalObj = new Animal("Tommy", "John");
    
     synchronized (animalObj) {
    
     while(true) {
    
     Thread.sleep(Integer.MAX_VALUE);
    
     }
    
     }

    在這裡,不受信任的代碼示例引入了不確定的延遲,從而阻止了setNamesetOwner方法的實現獲取相同的鎖。

    6.2 解決辦法

    防止此漏洞的解決方案是私有鎖對象

    這個想法是使用與在我們的類中定義的Object的**private final實例相關聯的內在鎖來代替對象本身的內在鎖**。

    另外,我們應該使用塊同步來代替方法同步,以增加靈活性,以將非同步代碼排除在塊外。

    因此,讓我們對Animal類進行必要的更改:

    public class Animal {
    
     // ...
    
    
    
     private final Object objLock1 = new Object();
    
     private final Object objLock2 = new Object();
    
    
    
     public void setName(String name) {
    
     synchronized (objLock1) {
    
     this.name = name;
    
     }
    
     }
    
    
    
     public void setOwner(String owner) {
    
     synchronized (objLock2) {
    
     this.owner = owner;
    
     }
    
     }
    
     }

    在這裡,為了更好的並發性,我們通過定義多個private final鎖定對象來細化鎖定方案,以分離我們對兩個方法setNamesetOwner同步關注。

    此外,如果實現synchronized塊的方法修改了static變量,則必須通過鎖定static對象來進行同步:

    private static int staticCount = 0;
    
     private static final Object staticObjLock = new Object();
    
     public void staticVariableSolution() {
    
     synchronized (staticObjLock) {
    
     count++;
    
     // ...
    
     }
    
     }

    7.結論

    在本文中,我們討論了與某些類型(如StringBooleanIntegerObject上的同步相關的一些不良做法。

    本文最重要的要點是,不建議使用池化或可重用的對象進行同步。

    另外,建議在Object類的private final實例上進行同步。外部/不受信任的代碼將無法訪問此對象,否則這些代碼可能會與我們的public類進行交互,從而減少了此類交互可能導致死鎖的可能性。