java synchronized 關鍵字

這一章主要講述線程之間數據的共享,數據共享最大的難點就是資源競爭


3.1 Synchronized關鍵字的使用 (The Synchronized Keyword)

書中例子太繁瑣了,我找了一個簡單的例子


package com.yellow.chapteThree;
public class Test implements Runnable {
    public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()
                        + " synchronized loop " + i);
            }
    }
    public static void main(String[] args) {
        Test t = new Test();
        Thread ta = new Thread(t, "A");
        Thread tb = new Thread(t, "B");
        ta.start();
        tb.start();
    }
}


我們讓兩個線程從1打印到5,結果我們會發現,在A打印的過程中B也在打印,兩個線程都進入了這個方法,那怎麼辦呢~


第一個辦法,使用synchronized代碼塊


package com.yellow.chapteOne;
public class Test implements Runnable {
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()
                        + " synchronized loop " + i);
            }
        }
    }
    public static void main(String[] args) {
        Test t = new Test();
        Thread ta = new Thread(t, "A");
        Thread tb = new Thread(t, "B");
        ta.start();
        tb.start();
    }
}


synchronized是由監聽器(monitors)實現的,每個對象都有一個monitors,所以synchronized()的括號裏面可以填任意對象,當一個線程試圖進入一個synchronized代碼塊的時候,必須得到這個代碼塊的monitors,一旦有一個線程得到了這個代碼塊的monitors後,其他所有在同一monitors下的線程都必須等待,獲得monitors的線程執行完代碼後會自動釋放monitors的所有權,好讓其他線程進入


我們讓synchronized使用t對象的monitors,很明顯,A,B兩個線程處於同一個monitor下面,當A搶到資源執行run方法,B只能等待


如果我們稍作變化如下:


package com.yellow.chapteOne;
public class Test implements Runnable {
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()
                        + " synchronized loop " + i);
            }
        }
    }
    public static void main(String[] args) {
        Test t1 = new Test();
        Test t2 = new Test();
        Thread ta = new Thread(t1, "A");
        Thread tb = new Thread(t2, "B");
        ta.start();
        tb.start();
    }
}


我們會發現結果並沒有還是會交替打印,沒有起到同步的作用,因爲,A,B兩個線程處於不同對象的monitor,可以同時進入synchronized代碼塊


第二種方式就是使用synchronized方法,如下


package com.yellow.chapteOne;
public class Test implements Runnable {
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()
                    + " synchronized loop " + i);
        }
    }
    public static void main(String[] args) {
        Test t = new Test();
        Thread ta = new Thread(t, "A");
        Thread tb = new Thread(t, "B");
        ta.start();
        tb.start();
    }
}


非static的synchronized方法用的是this的monitor,static的synchronized方法用類的class對象的monitor


3.2 Volatile 關鍵字(The Volatile Keyword)

把一個變量聲明成volatile意味着這個變量的值將不會換成在線程的本地空間,而是直接操作main memory


直接上個例子來解釋:


public class VolatileObjectTest {
    /**
     * 相信絕大多數使用JAVA的人都沒試出volatile變量的區別。獻給那些一直想知道volatile是如何工作的而又試驗不出區別的人。
     * 成員變量boolValue使用volatile和不使用volatile會有明顯區別的。 本程序需要多試幾次,就能知道兩者之間的區別的。
     *
     * @param args
     */
    public static void main(String[] args) {
        final VolatileObjectTest volObj = new VolatileObjectTest();
        Thread t2 = new Thread() {
            public void run() {
                System.out.println("t1 start");
                for (;;) {
                    volObj.waitToExit();
                }
            }
        };
        t2.start();
        Thread t1 = new Thread() {
            public void run() {
                System.out.println("t2 start");
                for (;;) {
                    volObj.swap();
                }
            }
        };
        t1.start();
    }
     boolean boolValue;// 加上volatile 修飾的是時候,程序會很快退出,因爲volatile
                        // 保證各個線程工作內存的變量值和主存一致。所以boolValue == !boolValue就成爲了可能。
    public void waitToExit() {
        if (boolValue == !boolValue)
            System.exit(0);// 非原子操作,理論上應該很快會被打斷。實際不是,因爲此時的boolValue在線程自己內部的工作內存的拷貝,因爲它不會強制和主存區域同步,線程2修改了boolValue很少有機會傳遞到線程一的工作內存中。所以照成了假的“原子現象”。
    }
    public void swap() {// 不斷反覆修改boolValue,以期打斷線程1.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolValue = !boolValue;
        System.out.println(boolValue);
    }
}


我們用volatile用的最廣泛的地方是 作爲停止請求的標誌( "stop request" flag ),好吧,我也知道翻譯的不像人話,看代碼


public class StoppableTask extends Thread {
  private volatile boolean pleaseStop;
  public void run() {
    while (!pleaseStop) {
      // do some stuff...
    }
  }
  public void tellMeToStop() {
    pleaseStop = true;
  }
}


如果pleaseStop變量沒有被聲明爲volatile的話,別的線程改變了pleaseStop的值,而這個線程不知道(因爲它不會從主存裏面同步,會讀自己本地空間),就會一直循環下去


volatile和synchronized的區別:


1)volatile可以修飾primitive變量,synchronized不能

2)線程進入synchronized的時候會獲得鎖,但是volatile不會獲得鎖

3)因爲進入volatile沒有獲得鎖,所以不要試圖用volatile實現原子操作

4)volatile可以修飾null


然後我查資料的時候發現了一個有意思的東西,可以用 volatile和雙重檢查加鎖可以實現多線程單例模式


1

/**
 * 雙重檢查加鎖 單例模式,據說JDK1.5之後可以用  volatile 和 雙重檢查加速來實現單例模式
 * @author yellowbaby
 *
 */
public class SingletonOne {
    /**
     *  volatile 的作用是讓變量不再緩存在當前線程的本地空間,而是直接去操作main memory
     *  我的問題是,這裏爲什麼要使用 volatile?
     */
    private volatile static SingletonOne singleton = null;
    public SingletonOne() {
    }
    public static SingletonOne getInstance() {
        if (null == singleton) {// 1 檢查實例,如果不存在就進入同步區塊
            synchronized (SingletonOne.class) {// 2  注意,只有第一次才徹底執行這裏的代碼
                if (null != singleton) {// 3
                    singleton = new SingletonOne();
                }
            }
        }
        return singleton;
    }
}


假如不用 volatile會發生什麼呢?


假設同時有兩個線程(A和B)進入了1的if塊,A進入2,B等待,A出了Syn塊,B進入,B判斷3這個時候它會直接從自己的線程內存中讀取singleton的值,發現爲空然後就會又new一個出來


然後分享另一種多線程單例模式~


/**
 * 使用內部靜態類來得到實例,因爲只有在調用InnerSingleFactory.SINGLETON的時候纔會加載SingletonTwo,所以也是懶漢型
 * @author yellowbaby
 *
 */
public class SingletonTwo {
        private volatile static SingletonTwo singleton = null;
        private SingletonTwo() {
        }
        public static SingletonTwo getInstance() {
                if(singleton == null){
                        singleton = InnerSingleFactory.SINGLETON;
                }
                return singleton;
        }
                                                                                                                                                                                                                                                                                                                                                                                                                                   
        private final static class InnerSingleFactory {
                final static SingletonTwo SINGLETON = new SingletonTwo();
        }
}


用static來解決同步是個好辦法,但是常規的使用static的寫法是餓漢型的,但是如果丟在內部類裏面就可以解決這個麻煩的問題了


3.3 資源競爭(More on Race Conditions)


什麼是資源競爭?


race condition 發送在 兩個線程共享同一個數據,並且試圖同時修改它,因爲線程調度算法讓線程執行的先後不是固定的,數據最終改變的結果取決於線程的執行順序


問題往外出現在 “check-then-act”的操作中


舉個例子


if (x == 5) // The "Check"檢查 1
{
   y = x * 2; // The "Act" 操作 2
                                                                                                                                                                                                                                                                                                                           
   //  如果另一個線程在 1 和 2 直接修改了 x 的值,那結果就不會是 10 了
}



爲了解決這個問題,我們需要在合適的地方加上鎖來確保只有一個線程修改這個數據


// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released.
              // Therefore y = 10
}
// release lock for x


一般都是使用synchronized代碼塊或者synchronized方法來同步,前面我面提到過的~


3.4 顯示鎖(Explicit Locking)


上面說了synchronized的一些用法,synchronized是好,但是並不是完美的,比如你不能中斷一個正在等待的線程,有可能一個線程得不到鎖就一直傻傻的等下去,JDK 5 之後出現了一個新東西既可以實現synchronized的作用也可以實現這些它做不到的東西,這就是 顯示鎖


看例子


Lock lock = new ReentrantLock();
        lock.lock();
        try {
          // 等價於加上了synchronized代碼塊
        }
        finally {
          lock.unlock();
        }


顯示鎖在性能上面比synchronized強,而且可以實現一些synchronized不能實現的,但是也有缺點


1)必須要手動釋放鎖,使用synchronized會自動的釋放鎖,一旦忘記就能難查出問題出現在什麼地方

2)還有,顯示鎖不兼容 JDK 1.5 以前的版本


那什麼時候用顯示鎖呢,答案就是,在你真正需要某些synchronized無法實現的功能的時候,大部分時候synchronized是可以足夠的


3.8 死鎖(Deadlock)

死鎖就是兩個線程都在等待對方釋放釋放資源,導致兩個線程一直堵塞

看一個例子

public class MyDeadLock{
    public static void main(String[] args) {
                                                                        
        final Robber robber = new Robber();
        final Victim victim = new Victim();
        robber.setVictim(victim);
        victim.setRobber(robber);
                                                                        
        new Thread(new Runnable() {
                                                                            
            @Override
            public void run() {
                robber.rob();
            }
        }).start();
                                                                        
        new Thread(new Runnable() {
                                                                            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                victim.beRobbed();
            }
        }).start();
    }
                                                                    
}
class Robber  {
                                                                    
    Victim victim;
                                                                    
    public Robber() {
                                                                        
    }
                                                                    
    synchronized void rob(){
        System.out.println("我是劫匪");
        victim.giveYouMoney();//給錢
        letYouGo();//讓你走
    }
                                                                    
    synchronized void letYouGo(){
        System.out.println("放人");
    }
                                                                    
    public void setVictim(Victim victim) {
        this.victim = victim;
    }
}
class Victim  {
                                                                    
    Robber robber;
                                                                    
    public Victim() {
                                                                    
    }
                                                                    
    synchronized void beRobbed(){
        System.out.println("我是被搶劫的人");
        robber.letYouGo();//讓我走
        giveYouMoney();//給錢
    }
                                                                    
    synchronized void giveYouMoney(){
        System.out.println("給你錢");
    }
                                                                    
    public void setRobber(Robber robber) {
        this.robber = robber;
    }
}


劫匪和受害者一個想要對方先給錢,一個想讓對方先放人,一直在等待,然後就死鎖了


當第一個robber對象進入rob方法時,得到了robber的對象鎖,試圖調用victim的方法,因爲giveYouMoney是synchronized的,所以需要等待得到victim的對象鎖,但是victim的對象鎖被在調用beRobbed的時候被victim獲得了,而也想得到robber的對象鎖,兩邊互不相讓,然後就死鎖了~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章