JAVA多線程編程陷阱總結一

來源:互聯網
上載者:User

陷阱1:在建構函式中啟動線程

在建構函式中啟動一個線程,類似這樣:

        public class A
        {
           public A()
           {
              this.x=1;
              this.y=2;
              this.thread=new MyThread();
              this.thread.start();
           }
        } 

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

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

陷阱2:不完全的同步

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

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

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

陷阱3:在使用某個對象當鎖時,改變了對象的引用,導致同步失效。

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

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

陷阱4:沒有在迴圈中調用wait()。

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

        synchronized(lock)
        {
           if(isEmpty()
             lock.wait();
           
        }

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

        synchronized(lock)
        {
           while(isEmpty()
             lock.wait();
           
        }

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

陷阱5:同步的範圍過小或者過大。

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

        Map map=Collections.synchronizedMap(new HashMap());
        if(!map.containsKey("a"))
        {
        map.put("a", value);
        }

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

        Map map = Collections.synchronizedMap(new HashMap());
        synchronized (map) 
        {
            if (!map.containsKey("a")) 
            {
                map.put("a", value);
            }
        }

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

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

陷阱6:正確使用volatile

在jdk5修正了volatile的語義後,volatile作為一種輕量級的同步策略就得到了大量的使用。volatile的嚴格定義參考jvm spec,這裡只從volatile能做什麼,和不能用來做什麼出發做個探討。
volatile可以用來做什嗎?
1)狀態標誌,類比控制機制。常見用途如控制線程是否停止:

        private volatile boolean stopped;
        public void close()
        {
           stopped=true;
        }
        
        public void run()
        {
           while(!stopped){
              //do something
           } 
        } 

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

 2)安全發布,如修複DLC問題,具體參考http://www.javaeye.com/topic/260515和http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html

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

3)開銷較低的讀寫鎖

        public class CheesyCounter 
        {
            private volatile int value;
            public int getValue() 
            { return value; }
        
            public synchronized int increment() 
            {
                return value++;
            }
        }

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

volatile不能用於做什嗎?

1)不能用於做計數器

  public class CheesyCounter 
{
        private volatile int value;
        public int getValue() 
{ return value; }

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

 因為value++其實是有三個操作組成的:讀取、修改、寫入,volatile不能保證這個序列是原子的。對value的修改操作依賴於value的最新值。解決這個問題的方法可以將increment方法同步,或者使用AtomicInteger原子類。
 2)與其他變數構成不變式
一個典型的例子是定義一個資料範圍,需要保證約束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並不是安全執行緒方法。假設初始狀態為(0,5),同時調用setLower(4)和setUpper(3),兩個線程交叉進行,最後結果可能是(4,3),違反了約束條件。修改這個問題的辦法就是將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;
            }
        }


引文:http://java.chinaitlab.com/line/782806_3.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.