Java線程安全和死鎖問題

線程安全

  • 如果你的代碼在多線程下執行和在單線程下執行永遠都能獲得一樣的結果,那麼你的代碼就是線程安全的

線程安全級別

  • 1、不可變

    String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新創建一個,因此這些不可變對象不需要任何同步手段就可以直接在多線程環境下使用

  • 2、絕對線程安全

    不管運行時環境如何,調用者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java中標註自己是線程安全的類,實際上絕大多數都不是線程安全的。不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayListCopyOnWriteArraySet

  • 3、相對線程安全

    相對線程安全也就是我們通常意義上所說的線程安全,像Vector這種,add、remove方法都是原子操作不會被打斷,但也僅限於此,如果有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制

  • 4、 線程非安全

    ArrayList、LinkedList、HashMap等都是線程非安全的類

常見的線程安全類

線程安全類 線程不安全類
Vector ArrayList
StringBuffer StringBuilder
Hashtable HashMap
LinkedList

- StringBuffer 線程安全(其append方法中加了synchronized修飾
- vector add、remove方法都是原子操作,加了synchronized修飾
- 但是Collections集合工具類中提供了靜態方法synchronizedXXX(XXX),分別對應着線程不安全的那些集合類,可以讓他們轉換成線程安全的集合,所以Vector類淘汰了…

方法摘要 方法說明
static <T> Collection<T> synchronizedCollection(Collection<T> c) 返回指定 collection 支持的同步(線程安全的)collection。
static <T> List<T> synchronizedList(List<T> list) 返回指定列表支持的同步(線程安全的)列表。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(線程安全的)映射。
static <T> Set<T> synchronizedSet(Set<T> s) 返回指定 set 支持的同步(線程安全的)set。
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) 返回指定有序映射支持的同步(線程安全的)有序映射。
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s) 返回指定有序 set 支持的同步(線程安全的)有序 set。

多線程中的線程安全問題

  • 多線程併發操作同一共享數據時,就會可能出現線程安全問題。

  • 使用同步技術可以解決這種問題, 把操作數據的代碼進行同步, 就不會多個線程同時執行

多窗口賣票問題

  • 如果不開啓鎖同步 ,就會出現賣出票號爲負數的現象
  • 在循環中使用鎖同步,讓各個線程進入循環之後進行同步,一個線程把ticketNum–後,其他線程再執行
  • 使用Runnable方式實現:

    public class SynchronizeTicketTest {
    
      public static void main(String[] args) {
    
          new Thread(new TicketSeller()).start();
          new Thread(new TicketSeller()).start();
          new Thread(new TicketSeller()).start();
          new Thread(new TicketSeller()).start();
    
      }
    }
    
    class TicketSeller implements Runnable{
      private static int tikcetNum = 10000;//總共10000張票,放到靜態池中共享
      @Override
      public void run() {
          while(true){
              //在循環中使用鎖同步,讓各個線程進入循環之後進行同步,一個線程把ticketNum--後,其他線程再執行
              synchronized(TicketSeller.class){
                  if(tikcetNum <= 0)
                      break;
                  try {
                      //讓線程睡10ms 如果不開啓鎖同步 就會出現票號爲負數的現象
                      Thread.sleep(10);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "...這是第" + tikcetNum-- + "號票");
    
              }
          }
      }
    }
    

死鎖問題

  • 線程A和線程B相互等待對方持有的鎖導致程序無限死循環下去
  • 線程A持有鎖H並且想獲取鎖W,此時線程B持有鎖W並且想獲取鎖H,那麼這兩個線程AB就會永遠等待下去,產生最簡單的死鎖。
  • 一個類可能發生死鎖,並不意味着每次都會發生,往往在高併發、高負載的情況下,死鎖出現概率高很多。

  • 多線程同步的時候, 如果同步代碼嵌套, 使用相同鎖, 就有可能出現死鎖

寫一個死鎖程序

  • 哲學家進餐問題,使用同步代碼塊嵌套,互相先持有對方需要的鎖對象

  • 寫一個死鎖程序步驟:

    1. 定義兩個對象分別代表兩個線程一開始就持有的鎖對象
    2. 在run方法中使用 synchronized 同步代碼塊嵌套
    3. 外層synchronized鎖對象對方所需求的自己所持有的內層synchronized鎖對象對方所持有自己所需要的
    4. 當一個線程中的鎖對象是自己持有的,還未走出外層代碼塊,需要對方所持有的鎖對象時,cpu調度到了另一個線程,另一個線程正好也是這種情況,此時雙方都持有了對方所需要的鎖對象,發生了死鎖。
    public class DeadLockTest {
      private static String left = "left one";
      private static String right = "right one";
      public static void main(String[] args) {
    
          new Thread(() -> {
              while(true){
                  synchronized (right){
                      System.out.println(Thread.currentThread().getName()+"--持有了right,想得到left");
                      synchronized(left){
                          System.out.println(Thread.currentThread().getName()+"--得到了left,可以開吃了");
                      }
                  }
              }
          }).start();
    
          new Thread(() -> {
              while(true){
                  synchronized (left){
                      System.out.println(Thread.currentThread().getName()+"--持有了left,想得到right");
                      synchronized(right){
                          System.out.println(Thread.currentThread().getName()+"--得到了right,可以開吃了");
                      }
                  }
              }
          }).start();
    
          /*
              Thread-1--持有了left,想得到right
              Thread-0--持有了right,想得到left
              執行到此時,就會發現這兩個線程的鎖對象誰都不想放,就會產生死鎖。
           */
    
      }
    }

    結果:

    上方結果省略....
    Thread-1--持有了left,想得到right
    Thread-0--持有了right,想得到left
    

    執行到此時,就會發現這兩個線程的鎖對象誰都不想放,就會產生死鎖。

避免死鎖的方式

  • 注意和減少同步代碼塊嵌套問題
  • 設計時考慮清楚鎖的順序,儘量減少嵌套加鎖交互數量
  • 由於死鎖是因爲兩個或多個線程之間無限時間等待對方持有的鎖對象而形成的,那麼給同步代碼塊加個等待時間限制。
    • synchronized 關鍵字 不具備這個功能,使用Lock類中的tryLock方法,指定一個超時時限,在等待時,若超過該時限,就返回一個失敗信息結束阻塞。

單例模式的線程安全問題

單例模式

  • 單例設計模式:保證一個類在內存中只有一個對象,內存唯一。
  • 保證類在內存中只有一個對象:
    • 1、控制類的創建,不讓其他類來創建本類的對象,將本類的構造函數私有private
    • 2、在本類中定義一個本類的對象,並且外界無法修改。
    • 3、在本類中提供一個唯一的公共訪問方法,可獲取本類的對象。

餓漢式-線程安全

  • 在類中直接創建一個不可修改的對象引用,不管有沒有調用,都創建,空間換時間
  • 餓漢式在多線程環境下是線程安全的。
class Singleton {
    //1.將本類的構造函數私有private
    private  Singleton (){}
    //2. 在本類中定義一個本類的對象,並且外界無法修改。
    private  static Singleton s = new Singleton();

    //3. 在本類中提供一個唯一的公共訪問方法,可獲取本類的對象
    //餓漢式
    public static Singleton getInstance(){
        return s ;
    }
}

另一種餓漢式,利用final直接修飾

class Singleton {
    //1.將本類的構造函數私有private
    private  Singleton (){}
    //2. 在本類中定義一個本類的對象,並且外界無法修改。
    public final static Singleton s = new Singleton() ;

}

懶漢式-非線程安全

  • 在類中獲取對象時加以判斷,爲空時才創建,即用到該類對象時才創建,時間換空間。
  • 懶漢式單例模式在多線程下是非線程安全的。
    • 當線程A判斷爲null時,正準備new,此時,被另一個線程B搶佔了CPU資源,線程B也判斷爲null,new了之後,第一個線程A又搶回了CPU資源,此時線程A又new了。此時這兩個線程就new了兩次,就不是唯一的內存引用了。
class Singleton {
    //1.將本類的構造函數私有private
    private  Singleton (){}
    //2. 在本類中定義一個本類的對象,並且外界無法修改。
    private  static Singleton s ;
    //3. 在本類中提供一個唯一的公共訪問方法,可獲取本類的對象
    //懶漢式 對象引用爲空 才創建,
    public static Singleton getInstance(){
        //用到時創建,用不到時不創建
        if(s == null)
             s = new Singleton() ;
        return s;
    }
}

餓漢式和懶漢式的區別

  • 線程安全上:
    • 餓漢式線程安全,多線程下也不會創建多個對象
    • 懶漢式非線程安全,多線程下可能會創建多個對象
  • 執行效果:
    • 餓漢式是 空間換時間,執行速度快。
    • 懶漢式是 時間換空間,延遲加載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章