熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

Java多線程編程的常見陷阱

2022-06-13   來源: Java高級技術 

  在構造函數中啟動線程

  我在很多代碼中都看到這樣的問題在構造函數中啟動一個線程類似這樣

   public class A{
   public A(){
      thisx=;
      thisy=;
      thisthread=new MyThread();
      thisthreadstart();
   }
   
}

  這個會引起什麼問題呢?如果有個類B繼承了類A依據java類初始化的順序A的構造函數一定會在B的構造函數調用前被調用那麼thread線程也將在B被完全初始化之前啟動當thread運行時使用到了類A中的某些變量那麼就可能使用的不是你預期中的值因為在B的構造函數中你可能賦給這些變量新的值也就是說此時將有兩個線程在使用這些變量而這些變量卻沒有同步

  解決這個問題有兩個辦法將A設置為final不可繼承或者提供單獨的start方法用來啟動線程而不是放在構造函數中

  不完全的同步

  都知道對一個變量同步的有效方式是用synchronized保護起來synchronized可能是對象鎖也可能是類鎖看你是類方法還是實例方法但是當你將某個變量在A方法中同步那麼在變量出現的其他地方你也需要同步除非你允許弱可見性甚至產生錯誤值類似這樣的代碼

   class A{
  int x;
  public int getX(){
     return x;
  }
  public synchronized void setX(int x)
  {
     thisx=x;
  }
}

  x的setter方法有同步然而getter方法卻沒有那麼就無法保證其他線程通過getX得到的x是最新的值事實上這裡的setX的同步是沒有必要的因為對int的寫入是原子的這一點JVM規范已經保證多個同步沒有任何意義當然如果這裡不是int而是double或者long那麼getX和setX都將需要同步因為double和long都是寫入和讀取都是分成兩個位來進行(這一點取決於jvm的實現有的jvm實現可能保證對long和double的readwrite是原子的)沒有保證原子性類似上面這樣的代碼其實都可以通過聲明變量為volatile來解決

  在使用某個對象當鎖時改變了對象的引用導致同步失效

  這也是很常見的錯誤類似下面的代碼

   synchronized(array[])
{
  
   array[]=new A();
  
}

  同步塊使用array[]作為鎖然而在同步塊中卻改變了array[]指向的引用分析下這個場景第一個線程獲取了array[]的鎖第二個線程因為無法獲取array[]而等待在改變了array[]的引用後第三個線程獲取了新的array[]的鎖第一和第三兩個線程持有的鎖是不一樣的同步互斥的目的就完全沒有達到了這樣代碼的修改通常是將鎖聲明為final變量或者引入業務無關的鎖對象保證在同步塊內不會被修改引用

  沒有在循環中調用wait()

  wait和notify用於實現條件變量你可能知道需要在同步塊中調用wait和notify為了保證條件的改變能做到原子性和可見性常常看見很多代碼做到了同步卻沒有在循環中調用wait而是使用if甚至沒有條件判斷

   synchronized(lock)
{
   if(isEmpty()
     lockwait();
   
}

  對條件的判斷是使用if這會造成什麼問題呢?在判斷條件之前可能調用notify或者notifyAll那麼條件已經滿足不會等待這沒什麼問題在條件沒有滿足調用了wait()方法釋放lock鎖並進入等待休眠狀態如果線程是在正常情況下也就是條件被改變之後被喚醒那麼沒有任何問題條件滿足繼續執行下面的邏輯操作問題在於線程可能被意外甚至惡意喚醒由於沒有再次進行條件判斷在條件沒有被滿足的情況下線程執行了後續的操作意外喚醒的情況可能是調用了notifyAll可能是有人惡意喚醒也可能是很少情況下的自動蘇醒(稱為偽喚醒因此為了防止這種條件沒有滿足就執行後續操作的情況需要在被喚醒後再次判斷條件如果條件不滿足繼續進入等待狀態條件滿足才進行後續操作

   synchronized(lock)
{
   while(isEmpty()
     lockwait();
   
}

  沒有進行條件判斷就調用wait的情況更嚴重因為在等待之前可能notify已經被調用那麼在調用了wait之後進入等待休眠狀態後就無法保證線程蘇醒過來

  同步的范圍過小或者過大

  同步的范圍過小可能完全沒有達到同步的目的同步的范圍過大可能會影響性能同步范圍過小的一個常見例子是誤認為兩個同步的方法一起調用也是將同步的需要記住的是Atomic+Atomic!=Atomic

      Map map=CollectionssynchronizedMap(new HashMap());
   if(!ntainsKey(a)){
            mapput(a value);
   }

  這是一個很典型的錯誤map是線程安全的containskey和put方法也是線程安全的然而兩個線程安全的方法被組合調用就不一定是線程安全的了因為在containsKey和put之間可能有其他線程搶先put進了a那麼就可能覆蓋了其他線程設置的值導致值的丟失解決這一問題的方法就是擴大同步范圍因為對象鎖是可重入的因此在線程安全方法之上再同步相同的鎖對象不會有問題

          Map map = CollectionssynchronizedMap(new HashMap());
       synchronized (map) {
            if (!ntainsKey(a)) {
                mapput(a value);
            }
        }

  注意加大鎖的范圍也要保證使用的是同一個鎖不然很可能造成死鎖 CollectionssynchronizedMap(new HashMap())使用的鎖是map本身因此沒有問題當然上面的情況現在更推薦使用ConcurrentHashMap它有putIfAbsent方法來達到同樣的目的並且滿足線程安全性

  同步范圍過大的例子也很多比如在同步塊中new大對象或者調用費時的IO操作(操作數據庫webservice等)不得不調用費時操作的時候一定要指定超時時間例如通過URLConnection去invoke某個URL時就要設置connect timeout和read timeout防止鎖被獨占不釋放同步范圍過大的情況下要在保證線程安全的前提下將不必要同步的操作從同步塊中移出

  正確使用volatile

  在jdk修正了volatile的語義後volatile作為一種輕量級的同步策略就得到了大量的使用volatile的嚴格定義參考jvm spec這裡只從volatile能做什麼和不能用來做什麼出發做個探討

  volatile可以用來做什麼?

  )狀態標志模擬控制機制常見用途如控制線程是否停止

   private volatile boolean stopped;
public void close(){
   stopped=true;
}

public void run(){

   while(!stopped){
      //do something
   }
   
}

  前提是do something中不會有阻塞調用之類volatile保證stopped變量的可見性run方法中讀取stopped變量總是main memory中的最新值

  )安全發布如修復DLC問題具體參考和/l

       private volatile IoBufferAllocator instance;
    public IoBufferAllocator getInsntace(){
        if(instance==null){
            synchronized (IoBufferAllocatorclass) {
                if(instance==null)
                    instance=new IoBufferAllocator();
            }
        }
        return instance;
    }

  )開銷較低的讀寫鎖

   public class CheesyCounter {
    private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

  synchronized保證更新的原子性volatile保證線程間的可見性

  volatile不能用於做什麼?

  )不能用於做計數器

       public class CheesyCounter {
        private volatile int value;

        public int getValue() { return value; }

        public int increment() {
            return value++;
        }
    }

  因為value++其實是有三個操作組成的讀取修改寫入volatile不能保證這個序列是原子的對value的修改操作依賴於value的最新值解決這個問題的方法可以將increment方法同步或者使用AtomicInteger原子類

  )與其他變量構成不變式

  一個典型的例子是定義一個數據范圍需要保證約束lower<upper

   public class NumberRange {
    private volatile int lower upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException();
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException();
        upper = value;
    }
}

  盡管講lower和upper聲明為volatile但是setLower和setUpper並不是線程安全方法假設初始狀態為(同時調用setLower()和setUpper(兩個線程交叉進行最後結果可能是(違反了約束條件修改這個問題的辦法就是將setLower和setUpper同步

   public class NumberRange {
    private volatile int lower upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public synchronized void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException();
        lower = value;
    }

    public synchronized void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException();
        upper = value;
    }
}


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27292.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.