1. 程式人生 > >Java高併發程式設計之synchronized關鍵字(一)

Java高併發程式設計之synchronized關鍵字(一)

首先看一段簡單的程式碼:

public class T001 {
    private int count = 0;
    private Object o = new Object();
    public void m() {
        //任何執行緒要執行下面這段程式碼,必須先拿到o的鎖
        synchronized (o) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

這段程式碼非常簡單,o指向了堆記憶體中的一個物件,synchronized(o)起到的作用是,當有執行緒要執行其作用域中的程式碼的時候,需要先獲取到o所指向的物件的鎖,注意o是物件的引用,物件存在於堆記憶體中,鎖的資訊也是記錄在堆記憶體中。只有拿到了鎖的執行緒,才能執行,否則只有等待其他執行緒釋放鎖,所以這把鎖叫做互斥鎖。注意,synchronized鎖定的不是一個程式碼塊,而是一個物件。
將上面程式碼稍加修改,如下:

public class T002 {
    private int count = 0;
    public void m() {
        //任何執行緒要執行下面這段程式碼,必須先拿到this的鎖
        synchronized (this) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

synchronized (this)鎖定的是自身這個物件,如果一段程式,開始的時候就要鎖定自身,結束時才釋放,那麼有一種簡單的寫法:

public class T003 {
    private int count = 0;
    //任何執行緒要執行下面這段程式碼,必須先拿到this的鎖
    public synchronized void m() {
        count++;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

synchronized關鍵字直接寫在方法的宣告中,它跟synchronized (this)是等價的,鎖定的都是this物件(不是鎖定一段程式碼哦

)。
那麼如果synchronized關鍵字寫在靜態方法的宣告中,情況又會怎樣呢?看如下程式碼:

public class T004 {
    private static int count = 0;
    //這裡等同於 synchronized(T004.class)
    public synchronized static void m() {
        count++;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void mm() {
        synchronized (T004.class) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

實際上ynchronized關鍵字寫在靜態方法的宣告中,等同於鎖定的是T004.class這個物件,T004.class是Class類的物件。靜態的方法,不需要建立物件來訪問,所以這時候是不需要this的,所以m方法等同於mm方法。
synchronized宣告的方法程式碼塊,相當於一個原子操作:

public class T005 implements Runnable {
    private static int count = 10;
    @Override
    public synchronized void run() {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void main(String[] args) {
        T005 t005 = new T005();
        for(int i = 0; i < 5; i++) {
            new Thread(t005, "thread_" + i).start();
        }
    }
}

上面程式碼中,因為run方法由synchronized關鍵字,所以該方法的程式碼塊,就相當於一個原子操作,儘管在main方法中啟動了5個執行緒都對t005物件的count進行了修改,但列印的結果順序完好。
假如同時存在同步方法(方法宣告中有synchronized關鍵字)和非同步方法,那麼在執行同步方法的過程中,非同步方法能否被其他執行緒執行呢?看下面這段程式碼:

public class T006 {
    private static int count = 10;

    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start...");
        try {
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end.");
    }

    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2");
    }

    public static void main(String[] args) {
        T006 t006 = new T006();

        new Thread(()->t006.m1()).start();
        new Thread(()->t006.m2()).start();
    }
}

上面程式碼啟動了兩個執行緒,執行緒1執行m1方法,執行緒2執行m2方法,在m1執行的過程中睡眠了10秒,在此期間,m2被執行了,執行結果如下:

Thread-0 m1 start...
Thread-1 m2
Thread-0 m1 end.

這說明,在同步方法在被執行的過程中,非同步方法是可以被其他執行緒執行的。有一個經典的問題,那就是寫方法同步,讀方法非同步的時候,容易產生髒讀,下面是一個例子:

public class T007 {
    private String name;
    private double balance;
    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }
    
    public double getBalance() {
        return this.balance;
    }

    public static void main(String[] args) {
        T007 t007 = new T007();
        new Thread(()->t007.set("zhangsan", 100.0)).start();
	try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("balance:" + t007.getBalance());
    }
}

顯然打印出來的並不會是set進去的100.0,而是0,這就是髒讀問題。業務程式碼只對寫加鎖,而讀不加鎖,寫方法在被執行的時候,讀方法可以被其他執行緒執行。因此,在做業務的時候要考慮清楚,是否允許髒讀。
synchronized獲得的鎖,是可重入的。即一個同步方法,可以呼叫另一個同步方法,一個執行緒已經擁有某個物件的鎖,再次申請時,仍然會得到該物件的鎖。下面是一個小例子:

public class T008 {
    public synchronized void m1() {
        System.out.println("m1 start...");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end.");
    }
    public synchronized void m2() {
        System.out.println("m2 start...");
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("m2 end.");
    }
    public static void main(String[] args) {
        T008 t008 = new T008();
        new Thread(t008::m1).start();
    }
}

列印結果如下:

m1 start...
m2 start...
m2 end.
m1 end.

用一句話歸納,就是,同一個執行緒,同一把鎖,則可重入。另一種情形與此相似,子類呼叫父類的方法:

public class T009 {
    public synchronized void m() {
        System.out.println("m start...");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("m end.");
    }

    public static void main(String[] args) {
        TT009 tt009 = new TT009();
        new Thread(tt009::m).start();
    }
}

class TT009 extends T009 {
    @Override
    public synchronized void m() {
        System.out.println("child m start...");
        super.m();
        System.out.println("child m start...");
    }
}

列印結果如下:

child m start...
m start...
m end.
child m start...

在程式執行過程中,如果出現異常,預設情況鎖會被釋放,所以,在併發處理過程中,有異常要多加小心,不然可能發生不一致的情況。比如,在一個web app處理過程中,多個servlet執行緒共同訪問同一個資源,這是如果異常處理不合適,在第一個執行緒丟擲異常,其他執行緒就會進入同步程式碼區,有可能會訪問到異常產生時的資料。因此要非常小心地處理同步業務邏輯中的異常。

public class T010 {
    private int count = 0;
    public synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start...");
        while (true) {
            count++;
            System.out.println("m start...");
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            if(5 == count) {
                //此處必定會丟擲異常,要想鎖不被釋放,需要進行catch,讓迴圈繼續
                int i = 1/0;
            }
        }
    }

    public static void main(String[] args) {
        T010 t010 = new T010();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t010.m();
            }
        };
        new Thread(r, "thread_1").start();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(r, "thread_2").start();
    }
}

在上面這段程式碼中,thread_1丟擲異常時,thread_2馬上開始執行,這說明,thread_1丟擲異常的時候,也釋放了鎖。