1. 程式人生 > >Java多執行緒-----執行緒安全及解決機制

Java多執行緒-----執行緒安全及解決機制

   1.什麼是執行緒安全問題?

     從某個執行緒開始訪問到訪問結束的整個過程,如果有一個訪問物件被其他執行緒修改,那麼對於當前執行緒而言就發生了執行緒安全問題;

如果在整個訪問過程中,無一物件被其他執行緒修改,就是執行緒安全的,即存在兩個或者兩個以上的執行緒物件共享同一個資源

   2.執行緒安全問題產生的根本原因

     首先是多執行緒環境,即同時存在有多個操作者,單執行緒環境不存線上程安全問題。在單執行緒環境下,任何操作包括修改操作都是操作者自己發出的,

 操作者發出操作時不僅有明確的目的,而且意識到操作的影響。

     多個操作者(執行緒)必須操作同一個物件,只有多個操作者同時操作一個物件,行為的影響才能立即傳遞到其他操作者。

     多個操作者(執行緒)對同一物件的操作必須包含修改操作,共同讀取不存線上程安全問題,因為物件不被修改,未發生變化,不能產生影響。

綜上可知,執行緒安全問題產生的根本原因是共享資料存在被併發修改的可能,即一個執行緒讀取時,允許另一個執行緒修改

   3.有執行緒安全的例項

模擬火車站售票視窗,開啟三個視窗售票,總票數為20張

例項一:

package com.practise.threadsafe;

//模擬火車站售票視窗,開啟三個視窗售票,總票數為100張 //存線上程的安全問題 class Window extends Thread { static int ticket = 20; public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()
+ "售票,票號為:" + ticket--); } else { break; } } } } public class TestWindow { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("視窗1"); w2.setName("視窗2"); w3.setName("視窗3"); w1.start(); w2.start(); w3.start(); } }
執行結果的一種:出現重複售票及負數票

視窗3售票,票號為:20
視窗2售票,票號為:18
視窗1售票,票號為:19
視窗1售票,票號為:17
視窗3售票,票號為:16
視窗2售票,票號為:17
視窗1售票,票號為:15
視窗3售票,票號為:14
視窗2售票,票號為:13
視窗2售票,票號為:12
視窗3售票,票號為:11
視窗1售票,票號為:10
視窗3售票,票號為:8
視窗2售票,票號為:9
視窗1售票,票號為:7
視窗1售票,票號為:6
視窗2售票,票號為:6
視窗3售票,票號為:5
視窗1售票,票號為:4
視窗3售票,票號為:4
視窗2售票,票號為:3
視窗2售票,票號為:2
視窗1售票,票號為:2
視窗3售票,票號為:2
視窗2售票,票號為:1
視窗1售票,票號為:-1
視窗3售票,票號為:0

 例項二:

package com.practise.threadsafe;

//使用實現Runnable介面的方式,售票
/*
* 此程式存線上程的安全問題
*/

class Window1 implements Runnable {
  int ticket = 20;

  public void run() {
      while (true) {
          if (ticket > 0) {
              try {
                  Thread.sleep(200);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--);
          } else {
              break;
          }
      }
  }
}

public class TestWindow1 {
  public static void main(String[] args) {
      Window1 w = new Window1();
      Thread t1 = new Thread(w);
      Thread t2 = new Thread(w);
      Thread t3 = new Thread(w);

      t1.setName("視窗1");
      t2.setName("視窗2");
      t3.setName("視窗3");

      t1.start();
      t2.start();
      t3.start();
  }
}
執行結果的一種:出現重複售票

視窗2售票,票號為:20
視窗1售票,票號為:20
視窗3售票,票號為:20
視窗1售票,票號為:19
視窗3售票,票號為:18
視窗2售票,票號為:17
視窗2售票,票號為:16
視窗3售票,票號為:14
視窗1售票,票號為:15
視窗1售票,票號為:13
視窗2售票,票號為:12
視窗3售票,票號為:11
視窗2售票,票號為:10
視窗1售票,票號為:10
視窗3售票,票號為:10
視窗1售票,票號為:9
視窗3售票,票號為:7
視窗2售票,票號為:8
視窗2售票,票號為:6
視窗3售票,票號為:4
視窗1售票,票號為:5
視窗3售票,票號為:3
視窗1售票,票號為:3
視窗2售票,票號為:3
視窗1售票,票號為:2
視窗3售票,票號為:0
視窗2售票,票號為:1

   4.執行緒安全解決機制Lock和synchronized

4.1  同步程式碼塊synchronized

package com.practise.threadsafe;

/*      同步程式碼塊
 *         synchronized(同步監視器){
 *             //需要被同步的程式碼塊(即為操作共享資料的程式碼)
 *         }
 *         1.共享資料:多個執行緒共同操作的同一個資料(變數)
 *         2.同步監視器:由一個類的物件來充當。哪個執行緒獲取此監視器,誰就執行大括號裡被同步的程式碼。俗稱:鎖
 *         要求:所有的執行緒必須共用同一把鎖!
 *         注:在實現的方式中,考慮同步的話,可以使用this來充當鎖。但是在繼承的方式中,慎用this!
 */
class Window implements Runnable {
    int ticket = 20;// 共享資料

    public void run() {
        while (true) {
            // this表示當前物件,本題中即為w
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--);
                }
            }
        }
    }
}

public class TestWindow {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}
執行結果的一種:

視窗1售票,票號為:20
視窗3售票,票號為:19
視窗3售票,票號為:18
視窗2售票,票號為:17
視窗2售票,票號為:16
視窗2售票,票號為:15
視窗2售票,票號為:14
視窗2售票,票號為:13
視窗2售票,票號為:12
視窗3售票,票號為:11
視窗3售票,票號為:10
視窗1售票,票號為:9
視窗1售票,票號為:8
視窗3售票,票號為:7
視窗2售票,票號為:6
視窗2售票,票號為:5
視窗3售票,票號為:4
視窗3售票,票號為:3
視窗3售票,票號為:2
視窗3售票,票號為:1

4.2  同步方法synchronized

package com.practise.threadsafe;

/*    
 * 同步方法
 * 將操作共享資料的方法宣告為synchronized。即此方法為同步方法,能夠保證當其中一個執行緒執行
 * 此方法時,其它執行緒在外等待直至此執行緒執行完此方法 
 */

class Window1 implements Runnable {
    int ticket = 20;// 共享資料

    public void run() {
        while (true) {
            show();
        }
    }

    public synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--);
        }

    }
}

public class TestWindow1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}
執行結果的一種:

視窗2售票,票號為:20
視窗1售票,票號為:19
視窗3售票,票號為:18
視窗1售票,票號為:17
視窗2售票,票號為:16
視窗1售票,票號為:15
視窗3售票,票號為:14
視窗1售票,票號為:13
視窗2售票,票號為:12
視窗1售票,票號為:11
視窗3售票,票號為:10
視窗1售票,票號為:9
視窗2售票,票號為:8
視窗1售票,票號為:7
視窗3售票,票號為:6
視窗3售票,票號為:5
視窗1售票,票號為:4
視窗2售票,票號為:3
視窗1售票,票號為:2
視窗3售票,票號為:1

4.3  同步鎖Lock

package com.practise.threadsafe;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Window2 implements Runnable {
    int ticket = 20;// 共享資料
    Lock lock = new ReentrantLock();

    public void run() {
        while (true) {
            lock.lock(); // 獲取鎖
            try {
                if (ticket > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--);
                }
            } finally {
                lock.unlock(); // 釋放鎖
            }
        }
    }
}

public class TestWindow2 {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}
執行結果的一種:

視窗3售票,票號為:20
視窗3售票,票號為:19
視窗3售票,票號為:18
視窗3售票,票號為:17
視窗3售票,票號為:16
視窗3售票,票號為:15
視窗3售票,票號為:14
視窗3售票,票號為:13
視窗3售票,票號為:12
視窗1售票,票號為:11
視窗2售票,票號為:10
視窗3售票,票號為:9
視窗3售票,票號為:8
視窗3售票,票號為:7
視窗3售票,票號為:6
視窗3售票,票號為:5
視窗3售票,票號為:4
視窗3售票,票號為:3
視窗3售票,票號為:2
視窗1售票,票號為:1

   5.synchronized 的侷限性 與 Lock 的優點

   如果一個程式碼塊被synchronized關鍵字修飾,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待直至佔有鎖的執行緒釋放鎖。事實上,佔有鎖的執行緒釋放鎖一般會是以下三種情況之一:

  • 佔有鎖的執行緒執行完了該程式碼塊,然後釋放對鎖的佔有;
  • 佔有鎖執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖;
  • 佔有鎖執行緒進入 WAITING 狀態從而釋放鎖,例如在該執行緒中呼叫wait()方法等。

   synchronized 是Java語言的內建特性,可以輕鬆實現對臨界資源的同步互斥訪問。那麼,為什麼還會出現Lock呢?試考慮以下三種情況:

Case 1 :

    在使用synchronized關鍵字的情形下,假如佔有鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,那麼其他執行緒就只能一直等待,別無他法。這會極大影響程式執行效率。因此,就需要有一種機制可以不讓等待的執行緒一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit)) 或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。

Case 2 :

    我們知道,當多個執行緒讀寫檔案時,讀操作和寫操作會發生衝突現象,寫操作和寫操作也會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是如果採用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個執行緒都只是進行讀操作時,也只有一個執行緒在可以進行讀操作,其他執行緒只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個執行緒都只是進行讀操作時,執行緒之間不會發生衝突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。

Case 3 :

    我們可以通過Lock得知執行緒有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。

   上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能為力。事實上,Lock 是 java.util.concurrent.locks包 下的介面,Lock 實現提供了比 synchronized 關鍵字 更廣泛的鎖操作,它能以更優雅的方式處理執行緒同步問題。也就是說,Lock提供了比synchronized更多的功能。但是要注意以下幾點:

  • 1)synchronized是Java的關鍵字,因此是Java的內建特性,是基於JVM層面實現的。而Lock是一個Java介面,是基於JDK層面實現的,通過這個介面可以實現同步訪問;
  • 2)採用synchronized方式不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而 Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致死鎖現象

   6.Lock和synchronized的選擇

   總結來說,Lock和synchronized有以下幾點不同:

  •   1)Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現
  •   2)synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖
  •   3)Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷
  •   4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到
  •   5)Lock可以提高多個執行緒進行讀操作的效率

  在效能上來說,如果競爭資源不激烈,兩者的效能是差不多的,而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇