天天看點

線程同步:解決線程安全問題

    多線程安全問題,是由于多個線程在通路共享的資料(共享的資源),并且操作共享資料的語句不止一條。那麼這樣在多條操作共享資料的之間線程就可能發生切換,進而引發線程安全問題。

例如如下情況:

public class ThreadDemo {
  public static void main(String[] args){
    Apple apple = new Apple();
    Thread t1 = new Thread(apple, "小明");
    Thread t2 = new Thread(apple, "二明");
    Thread t3 = new Thread(apple, "大明");
    Thread t4 = new Thread(apple, "小蘭");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
  }

}

class Apple implements Runnable{
  /*有6個蘋果用于配置設定*/
  int num = 6;
  @Override
  public void run() {

    while (true){
      if (num > 0) {
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "拿了第" + num-- + "個蘋果");
      }else {
        break;
      }
    }
  }
}
           

運作結果:

小蘭拿了第5個蘋果
	小明拿了第6個蘋果
	大明拿了第4個蘋果
	二明拿了第3個蘋果
	小明拿了第2個蘋果
	大明拿了第1個蘋果
	小蘭拿了第0個蘋果
	二明拿了第-1個蘋果
	小明拿了第-2個蘋果
           

    這個問題中,為什麼會出現有人拿到了0,-1,-2個蘋果的情況呢?因為當二明進入run()方法時,還沒來得及做num-- 操作時,此時num=1,另外兩個線程也進入到run()方法,執行拿蘋果的動作,是以就出現了出現-1,-2的結果。

    要解決上述多線程并發通路臨界資源出現的安全性問,就要保證sleep()和拿蘋果的操作同步完成。也即是說,在第一個線程拿完蘋果,進行num--之前,其他線程不能進行 num>0 的判。

    想要知道你的多線程程式有沒有安全問題,隻要看線程任務中是否有多條代碼在處理共享資料。

    解決多線程并發通路資源的安全問題,有三種方式:同步代碼塊、同步函數、鎖機制(Lock)。

方法一、同步代碼塊

文法:

synchronized(同步鎖){

    //需要同步操作的代碼

}

同步鎖:

為了保證每個線程都能正常執行原子操作,Java引入了線程同步機制.也稱為同步監聽對象/同步鎖/同步監聽器/互斥鎖。

實際上,對象的同步鎖隻是一個概念,可以想象為在對象上标記了一個鎖,誰拿到鎖,誰就可以進入代碼塊,其他線程隻能在代碼塊外面等着,而且注意,在任何時候,最多允許一個線程擁有同步鎖.

Java程式運作可以使用任何對象作為同步監聽對象,但是一般的,我們把目前并發通路的共同資源作為同步監聽對象。

public class ThreadDemo {
  public static void main(String[] args){
    Apple apple = new Apple();
    Thread t1 = new Thread(apple, "小明");
    Thread t2 = new Thread(apple, "二明");
    Thread t3 = new Thread(apple, "大明");
    Thread t4 = new Thread(apple, "小蘭");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
  }

}

class Apple implements Runnable{
  /*有6個蘋果用于配置設定*/
  int num = 6;
//  Object obj = new Object();
  @Override
  public void run() {
    while (true){
      synchronized (Apple.class){
        if (num > 0) {
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "拿了第" + num-- + "個蘋果");
        }else {
          break;
        }
      }
    }
  }
}
           

運作結果:

小蘭拿了第5個蘋果
小明拿了第6個蘋果
大明拿了第4個蘋果
二明拿了第3個蘋果
小明拿了第2個蘋果
大明拿了第1個蘋果
           

方法二、同步函數;

使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程隻能在方法外等着.

Synchronized public void doFunction(){

    //需要同步的内容

}

同步鎖是誰:

      對于非static方法,同步鎖就是this.  

      對于static方法,我們使用目前方法所在類的位元組碼對象(xxx.class).

public class ThreadDemo {
  public static void main(String[] args){
    Apple apple = new Apple();
    Thread t1 = new Thread(apple, "小明");
    Thread t2 = new Thread(apple, "二明");
    Thread t3 = new Thread(apple, "大明");
    Thread t4 = new Thread(apple, "小蘭");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
  }

}

class Apple implements Runnable{
  /*有6個蘋果用于配置設定*/
  int num = 6;
//  Object obj = new Object();
  @Override
  public void run() {
    while (true){
      doRun();
    }
  }

  public synchronized void doRun(){
    synchronized (Apple.class){
      if (num > 0) {
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "拿了一個蘋果,還剩" + num-- + "個");
      }
    }
  }
}
           

運作結果:

小明拿了一個蘋果,還剩6個
小明拿了一個蘋果,還剩5個
小明拿了一個蘋果,還剩4個
小明拿了一個蘋果,還剩3個
小明拿了一個蘋果,還剩2個
小明拿了一個蘋果,還剩1個
           

方法三、鎖機(同步鎖)

    Lock鎖代替了同步代碼塊。在多線程中能夠使用同步代碼塊的地方都可以使用Lock接口來代替,在java.util.concurrent.locks包下有接口Lock它是專門放負責描述鎖這個事務。

    當我們使用Lock接口代替同步代碼塊的時候,就需要程式員手動的來擷取鎖和釋放鎖,如果在程式中出現了擷取鎖之後,沒有釋放鎖,導緻其他程式無法進入這個同步語句中。需要使用try-finally語句在finally中書寫釋放鎖的代碼。

    在Lock接口中lock方法和unLock方法分别是擷取鎖和釋放鎖。

class AppleAllot implements Runnable{
  int num = 6;
  Lock lock = new ReentrantLock();
  @Override
  public void run() {
    while (true){
      //擷取鎖
      lock.lock();
      try{
        if (num > 0){
          System.out.println(Thread.currentThread().getName() + "拿了第" + num+ "個蘋果");
          num--;
        }
      }finally {
        //釋放鎖
        lock.unlock();
      }
    }
  }
}

public class LockDemo{
  public static void main(String[] args){
    AppleAllot apple = new AppleAllot();
    Thread t1 = new Thread(apple, "小明");
    Thread t2 = new Thread(apple, "二明");
    Thread t3 = new Thread(apple, "大明");
    Thread t4 = new Thread(apple, "小蘭");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
  }
}
           

運作結果:

小明拿了第6個蘋果
小明拿了第5個蘋果
小明拿了第4個蘋果
小明拿了第3個蘋果
小明拿了第2個蘋果
小明拿了第1個蘋果
           

說明:

Synchronized:

使用synchronized關鍵字修飾方法,會導緻加鎖,雖然可以使該方法線程安全,但是會極大的降低該方法的執行效率,故要慎用該關鍵字。

它無法中斷一個正在等候獲得鎖的線程;

也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖;

同步還要求鎖的釋放隻能在與獲得鎖所在的堆棧幀相同的堆棧幀中進行,多數情況下,這沒問題(而且與異常處理互動得很好),但是,确實存在一些非塊結構的鎖定更合适的情況。

鎖機制:

Java.util.concurrent.lock 中的Lock 架構是鎖定的一個抽象,它允許把鎖定的實作作為 Java 類,而不是作為語言的特性來實作。這就為Lock 的多種實作留下了空間,各種實作可能有不同的排程算法、性能特性或者鎖定語義。

ReentrantLock類實作了Lock ,它擁有與synchronized 相同的并發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈争用情況下更佳的性能。(換句話說,當許多線程都想通路共享資源時,JVM 可以花更少的時候來排程線程,把更多時間用在執行線程上。

參考:http://blog.csdn.net/caidie_huang/article/details/52748973