天天看點

Java并發程式設計3 —— 對象鎖和類鎖

synchronized關鍵字作用在同步代碼塊上,給共享變量“上鎖”可以解決線程安全問題。這把“鎖”可以作用在某個對象上,也可以作用在某個類上。

舉個栗子,有個自助銀行,裡面有兩台ATM機,從業人員可以看到每次存取款之後機器裡鈔票的總金額數。現在有兩個人來存錢,各存50元。

沒有鎖

在下面的代碼中,兩個線程t1、t2相當于兩個人,每個Service對象相當于一台ATM機。這裡先隻建立了一個Service對象,也就是兩個人向同一台ATM機裡存錢。顯然,第一個人存完後機器裡應當有150元,第二個人存完後有200元。

public class Bank {

    public static void main(String[] args) {
        Service s1 = new Service();
        //Service s2 = new Service();
        Thread t1 = new Thread(s1, "t1");
        Thread t2 = new Thread(s1, "t2");
        t1.start();
        t2.start();
    }
}

class Service implements Runnable{
    private int total = 100;

    @Override
    public void run() {
        total += 50;
        System.out.println(Thread.currentThread().getName() + " --- total = " + total);
    }
}
           

輸出:

t1 --- total = 200
t2 --- total = 200
           

顯然是兩個人同時操作“餘額”這個共享變量時出現了問題,第一個人存完50元後即讀到了第二個人存完50元後的餘額200元。

對象鎖

我們給run方法加上synchronized關鍵字,相當于給存錢和修改餘額的操作原子化,加上一把鎖。

public class Bank {

    public static void main(String[] args) {
        Service s1 = new Service();
        //Service s2 = new Service();
        Thread t1 = new Thread(s1, "t1");
        Thread t2 = new Thread(s1, "t2");
        t1.start();
        t2.start();
    }
}

class Service implements Runnable{
    private int total = 100;

    @Override
    public synchronized void run() {
        total += 50;
        System.out.println(Thread.currentThread().getName() + " --- total = " + total);
    }
}
           

輸出:

t1 --- total = 150
t2 --- total = 200
           

這裡的結果就是正确的了。第一個人存完後看到餘額150元,第二個人存完後看到餘額200元。

如果線程t2使用另一個對象s2會怎麼樣?

public class Bank {

    public static void main(String[] args) {
        Service s1 = new Service();
        Service s2 = new Service();
        Thread t1 = new Thread(s1, "t1");
        Thread t2 = new Thread(s2, "t2");
        t1.start();
        t2.start();
    }
}

class Service implements Runnable{
    private int total = 100;

    @Override
    public synchronized void run() {
        total += 50;
        System.out.println(Thread.currentThread().getName() + " --- total = " + total);
    }
}
           

輸出

t1 --- total = 150
t2 --- total = 150
           

這裡的邏輯相當于兩個人分别在兩台ATM機上存錢,而這兩台機器裡鈔票的餘額是互相獨立、互不幹擾的。因為s1和s2每個對象都有它自己的“total”變量,分别給了t1和t2去操作。是以在這種邏輯下也就不會産生線程安全問題,這兩個變量并不是共享的。代碼也證明了這一點,如果把此時run方法的synchronized關鍵字去掉,得到的是同樣的結果。

這也就是對象鎖的概念。對象鎖作用在某個(非靜态)方法上,這個方法為每個對象所獨享。隻有當不同的線程處理同一個對象時,相當于走了同一個方法處理同一個共享變量,才會産生線程安全問題。當一個線程處理這個對象或對象的方法時,其他線程必須等待,直到這個線程處理完畢,釋放對象鎖,其他線程才重新競争并獲得對象鎖。

類鎖

繼續上面存錢的例子。每個ATM機内鈔票的金額是每台機器獨享的,但是整個銀行擁有的資金總額是各個機器所共享的,在每台機器上存錢取錢後都會修改這同一個銀行資金總額。

靜态變量、靜态方法不再屬于某個具體的對象執行個體,而是屬于某個類的。是以我們把total變量定義為static。

public class Bank {

    public static void main(String[] args) {
        Service s1 = new Service();
        Service s2 = new Service();
        Thread t1 = new Thread(s1, "t1");
        Thread t2 = new Thread(s2, "t2");
        t1.start();
        t2.start();
    }
}

class Service implements Runnable{
    private static int total = 100;

    @Override
    public synchronized void run() {
        total += 50;
        System.out.println(Thread.currentThread().getName() + " --- total = " + total);
    }
}
           

輸出

t1 --- total = 150
t2 --- total = 200
           

兩個線程去操作了這個類的不同對象執行個體,但static變量total屬于Service這個類,是以兩個線程操作了相同的共享變量。如果不加synchronized關鍵字,就有點類似本文第一段代碼的情形,産生了線程安全問題。

這就是類鎖的概念。靜态方法、靜态變量隻會存在一份,給它加上鎖,不同線程使用不同對象操作這個變量時,也就使用的相同的一把鎖。

總結

對象鎖即“一個對象一把鎖”,當多個線程使用同一個對象時會有線程安全問題,多個對象之間的鎖互不影響。

類鎖即“多個對象一把鎖”,當多個線程使用多個對象操作同一個靜态共享變量時存線上程安全問題。

繼續閱讀