Synchronized鎖的是什么?


Synchronized鎖的是什么?

臨界區與鎖

並發編程中不可避免的會出現多個線程共享同一個資源的情況,為了防止出現數據不一致情況的發生,人們引入了臨界區的概念。臨界區是一個用來訪問共享資源的代碼塊,同一時間內只運行一個線程進入。

那么如何實現這個臨界區呢?這就用到我們的鎖了,當進程想要訪問一個臨界區時,它先會去看看是否已經有其他線程進入了,也就是看是否能獲得鎖。如果沒有其他線程進入,那么它就進入臨界區,其他線程就無法進入,相當於加鎖。反之,則會被掛起,處於等待狀態,直到其他線程離開臨界區,且本線程被JVM選中才可進入(因為可能有其他線程也在等待)。

利用Synchronized解決並發問題

Synchronize是一個重量級鎖,它會降低程序性能,因此如果對數據一致性沒有要求,就不要使用它。如果方法被Synchronize關鍵字聲明,那么該方法的代碼塊被視為臨界區。當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程才會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。

下面我們將創建兩個線程A,B來同時訪問一個對象:A從賬戶里取錢,B從賬戶里存錢。首先是不使用Synchronized關鍵字。

創建賬戶類

它擁有一個私有變量balance表示金額,addAmount和subtractAmount分別對金額執行加減操作。

public class Account {
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public void addAmount(double amount){
        System.out.println("addAmount start");
        double temp=balance;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        temp+=amount;
        balance=temp;
        System.out.println("addAmount end");
    }

    public void subtractAmount(double amount){
        System.out.println("subtractAmount start");
        double temp=balance;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        temp-=amount;
        balance=temp;
        System.out.println("subtractAmount end");
    }
}

創建A,B倆線程,分別對賬戶存錢和取錢。

public class A implements Runnable {
    private Account account;
    public A(Account account){
        this.account=account;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            account.addAmount(1000);
        }
    }
}
public class B implements Runnable  {
    private Account account;
    public B(Account account){
        this.account=account;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            account.subtractAmount(1000);
        }
    }
}

最后在main里面測試

public class Main {
    public static void main(String[] args) {
        Account account=new Account();
        account.setBalance(1000);
        A a=new A(account);
        Thread ThreadA=new Thread(a);
        B b=new B(account);
        Thread ThreadB=new Thread(b);
        System.out.println("Account Balance:"+account.getBalance());
        ThreadA.start();
        ThreadB.start();
        try {
            ThreadA.join();
            ThreadB.join();
            System.out.println("Account Balance:"+account.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadA往賬戶中執行了10次存入操作,每次存入1000元,ThreadB則是以同樣的金額執行了10次取出操作。那么按照我們的推測,最后賬戶的金額應該維持不變,但程序的結果卻不是我們想要的數字。這是為什么呢?因為我們在對數據進行操作的時候,另外一個線程可能也在進行操作,邏輯上應該先后執行的方法變成了同時執行,所以出現了錯誤。

現在我們給addAmount和subtractAmount加上synchronized關鍵字,保證數據一致性,這樣程序就不會出問題了。

如果是使用synchronize保護代碼塊,則需要將對象引用作為參數傳入。一般來說傳入this關鍵字作為引用執行方法的對象就可以了。

鎖的到底是什么?

或許在上面的例子你因為粗心只為其中一個方法加了關鍵字,那么你會看到這樣的現象:

保護代碼塊要將對象傳入,那應該鎖的是對象呀。你可能會想:我執行subtractAmout,按道理應該等我執行完addAmount才能執行,它都沒有account這個對象的鎖,不應該在中間插這么一段呀。但是,只有加了鎖的方法,線程執行該方法時才會去嘗試獲得鎖,看看是否有線程進入臨界區。訪問非同步方法無需獲得鎖,你把synchronized去掉跟你只加一個的情況是一樣的,同步方法與非同步遵循的是不同的規則。也就是說你可以在調用該對象的加了synchronized方法的同時,調用其他的非同步方法。

兩個線程怎么同時訪問了同一個對象的兩個synchronized方法?

你可能在搗鼓這個關鍵字的時候,驚訝的發現靜態方法的與眾不同。如果一個對象中的靜態方法用synchronized修飾,那么其他線程可以在該靜態方法被訪問的同時,訪問該對象中的非靜態方法(當然,該靜態方法同一時間只能被一個線程訪問)。換句話說,兩個線程可以同時訪問一個對象中的兩個synchronized方法。

等等,不是說鎖對象嗎?到底鎖的是什么?鎖的確實是對象,但對於靜態方法我們說的是T.class(T 為類名),非靜態方法鎖的是this ,也就是類的實例對象,兩者是不同的。

class T {
  // 修飾非靜態方法
  public synchronized void a() {
    // 臨界區
  }
  // 修飾靜態方法
  public synchronized static void b() {
    // 臨界區
  }
}  

上面那段代碼相當於:

class T {
  // 修飾非靜態方法
  public synchronized(this) void a() {
    // 臨界區
  }
  // 修飾靜態方法
  public synchronized(T.class) static void b() {
    // 臨界區
  }
}  

實際上加鎖本質就是在鎖對象的對象頭中寫入當前線程id。我們可以通過下面的代碼驗證,每次都傳入new Object()。

class Account {
    private double balance;
    public synchronized void addAmount(double amount){
        synchronized (new Object()){
            System.out.println("addAmount start");
            double temp=balance;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            temp+=amount;
            balance=temp;
            System.out.println("addAmount end");
        }
    }
    public void subtractAmount(double amount){
    	synchronized (new Object()){
            System.out.println("subtractAmount start");
            double temp=balance;
            try {
                Thread.sleep(100); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            temp-=amount;
            balance=temp;
            System.out.println("subtractAmount end");
        }
    }
}

因為線程每次調用方法鎖的都是新new的對象,所以加鎖無效。甚至編譯器可能會將synchronized給優化掉,因為這相當於多把鎖保護同一個資源,編譯器一看,每個人都弄把鎖就進來了,那我還不如不加,反正都一個樣。

另外需要注意的是,synchronized是可重入鎖。也就是說當線程訪問對象的同步方法時,在調用其他同步方法時無需再去獲取其訪問權。因為我們實際上鎖的是對象,對象頭里面紀錄的都是當前線程的ID。

總結

  • 修飾函數,鎖的是當前類的實例化對象
  • 修飾靜態方法,鎖的是當前類的Class對象
  • 修飾同步代碼塊,鎖的是括號里的對象

加鎖實際上就是在鎖對象的對象頭中寫入當前線程id,每個線程要想調用這個同步方法,都會先去鎖對象的對象頭看看當前線程id是不是自己的。

參考

synchronized鎖定的到底是什么?-知乎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM