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

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

上一篇文章講了synchronized的部分關鍵要點,詳見:Java高併發程式設計之synchronized關鍵字(一)
本篇文章接著講synchronized的其他關鍵點。
在使用synchronized關鍵字的時候,不要以字串常量作為鎖定物件。看下面的例子:

public class T012 {
    public String s1 = "hello";
    public String s2 = "hello";

    public void m1() {
        synchronized (s1) {
            System.out.println("m1 start...");
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("m1 end.");
        }
    }

    public void m2() {
        synchronized (s2) {
            System.out.println("m2 start...");
            System.out.println("m2 end.");
        }
    }

    public static void main(String[] args) {
        T012 t012 = new T012();
        new Thread(t012::m1, "Thread_1").start();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(t012::m2, "Thread_2").start();
    }
}

thread_1執行m1方法,thread_2執行m2方法,然而m2方法卻要等到m1執行結束才開始,這說明,m1和m2其實鎖定的是同一個物件。這種情況還會發生比較詭異的現象,比如你用到了一個類庫,在該類庫中程式碼鎖定了字串“hello”,但是因為你讀不到原始碼,所以你在自己的程式碼中也鎖定了“hello”,這時候就有可能發生非常詭異的死鎖阻塞,因為你的程式和你的類庫不經意間使用了同一把鎖。
鎖定某個物件o,如果o的屬性發生改變,不影響鎖的使用。但是如果o變成了另一個物件,則鎖定的物件發生改變,應該避免 將鎖定物件的應用變成另外的物件。

public class T013 {
    Object o = new Object();
    void m() {
        synchronized (o) {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        T013 t013 = new T013();
        new Thread(t013::m, "thread_1").start();

        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        Thread t2 = new Thread(t013::m, "thread_2");
        //鎖物件發生改變
        t013.o = new Object();
        t2.start();
    }
}

上面的程式碼中,如果物件o所引用的物件不發生改變,t2執行緒是不會執行的,然而執行程式可發現t2執行緒也運行了,這就證明,synchronized鎖定的是堆記憶體中的物件,而不是物件的應用,所以要避免改變鎖定的物件。
如何對synchronized進行優化呢?同步程式碼塊中的語句越少越好!
觀察下面程式:

public class T014 {
     int count = 0;
     synchronized void m1() {
         try {
             Thread.sleep(1000);
         } catch (Exception e) {
             e.printStackTrace();
         }
         
         count++;
         try {
             Thread.sleep(2000);
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
    
     void m2() {
         try {
             Thread.sleep(1000);
         } catch (Exception e) {
             e.printStackTrace();
         }
         //業務邏輯中只有count++需要sync,這時不應該給整個方法上鎖
         //採用細粒度的鎖,可以使執行緒爭用時間變短,從而提高效率
         synchronized (this) {
             count++;
         }
         try {
             Thread.sleep(2000);
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
}

上述程式碼中,m2方法的效率,要遠高於m1方法(具體可以自行測試下)