Java Threads 多線程10分鐘參考手冊

來源:互聯網
上載者:User
1         同步

                如何同步多個線程對共用資源的訪問是多線程編程中最基本的問題之一。當多個線程並發訪問共用資料時會出現資料處於計算中間狀態或者不一致的問題,從而影響到程式的正確運行。我們通常把這種情況叫做競爭條件(race condition),把並發訪問共用資料的代碼叫做關鍵地區(critical section)。同步就是使得多個線程順序進入關鍵地區從而避免競爭條件的發生。

1.1       Synchronized關鍵字

Synchronized是Java多線程編程中最常用的關鍵字。所有的Java 對象都有自己唯一的隱式同步鎖。該鎖只能同時被一個線程獲得,其他試圖獲得該鎖的線程都會被阻塞在對象的等待隊列中直到獲得該鎖的線程釋放鎖才能繼續工作。Synchronized關鍵字通常有兩種用法。當Synchronized關鍵字用於類方法定義中時,表示所有調用該方法的線程都必須獲得當前對象的鎖。這種方式比較簡單,但是同步的粒度比較大,當一個線程要執行某個對象的同步方法的時候,必須同時沒有任何其他線程在執行該對象的任一同步方法。此外,同步方法中的所有代碼均在同步塊中,獲得鎖的線程必須在執行完所有的代碼離開該方法後才會釋放鎖,這些代碼中可能只有一部分涉及到對共用資源(例如成員變數)的訪問需要同步,其餘則不需要,那麼這樣粗粒度的同步顯然增加了其他線程的等待時間。Synchronized的另一種
用法允許作用在某個對象上,並且只同步一段代碼而不是整個方法。

 

synchronized (object)  {

 // 需要同步的代碼

}

 

這裡synchronized所作用的對象可以是類的某個成員變數,也可以是這個類對象(用this表示)。這種用法使得程式員可以根據需要同步不同的成員變數,而不總是當前類對象,提高了靈活性。

 值得一提的是,並不是只有對象才有鎖,類本身也有自己的鎖,這使得static方法同樣可以用synchronized來修飾。訪問同步static方法的線程需要獲得類的同步鎖才能繼續執行。

1.2       Volatile關鍵字

                在Java記憶體模型中每個線程擁有自己的本機存放區(例如寄存器),並且允許線程擁有變數值的拷貝。這使得本來不需要同步的一些原子操作,例如boolean成員變數儲存和讀取也變得不安全。設想我們有個叫做done的boolean成員變數和一個當done為true時才會停止的迴圈,該迴圈由後台線程執行,另一個UI線程等待使用者輸入,使用者按下某個按鈕以後會把done設成true從而終止迴圈。由於UI線程自己本地擁有done的拷貝,使用者在按下按鈕時只是把自己本地的done設成了true而沒有及時更新主記憶體中的done,所以後台線程由於看不到done的改變而不會終止。即使主記憶體中的done變化了,後台線程也會因為自己本地的變數值沒有及時更新而沒有察覺到done的變化。解決這一問題的方法之一是為done提供synchronized的setter和getter方法,這是因為獲得同步鎖會迫使所有變數的值從臨時儲存(寄存器)寫會主記憶體。除此之外,Java提供了一個解決這個問題更為優雅的方法:Volatile關鍵字。每次使用volatile變數,JVM都會保證從主記憶體中讀取它的值;同樣每次修改volatile變數,JVM都會把值寫回到主記憶體中。

                Volatile適用的情境比較嚴格,必須很清楚地看到volatile只是告訴JVM對於該變數的讀寫必須每次都在主記憶體中進行而禁止使用臨時的拷貝來最佳化,它只是出於JVM特殊的記憶體模型的需要,並沒有同步的功能。因此只有對volatile變數進行的原子操作(讀取和賦值)才是安全執行緒的,像自增++自減--這樣包含多個命令的操作仍然需要其它的同步措施。

                另一個需要注意的的地方是當用volatile修飾數組的時候,它只是說數組的引用是volatile的,而數組中的元素還是和普通變數一樣,可能被JVM最佳化,我們無法為數組中的元素加上volatile修飾。解決上述問題的方法是使用Atomic變數。作為使用volatile修飾數組的一個例子,可以參考java.util.concurrent.CopyOnWriteArrayList。它的add操作是通過複製原來的數組並把新元素添加到新數組末尾然後再把內部數組引用變數指向新數組來實現的,因此陣列變數經常會被修改,需要使用volatile。

1.3       顯式鎖Lock

                儘管synchronized關鍵字可以解決大多數同步問題,J2SE5.0還是引入了Lock介面。相比使用synchronized關鍵字擷取對象隱式的同步鎖,我們稱Lock為顯式鎖。使用顯式鎖的一個顯而易見的好處是它不再屬於某個對象,從而可以在多個對象可以共用它。Lock介面有lock()和unlock()兩個方法,使用它們和使用synchronized關鍵字類似,在進入需要同步的代碼之前調用lock,在離開同步代碼塊時調用unlock。通常unlock會被放在finally中以保證即使同步代碼塊中有異常發生,鎖仍然可以被釋放。

                和使用synchronized關鍵字和lock()方法總是把未能獲得鎖的線程阻塞不同,Lock介面還提供了非阻塞的tryLock()方法。調用tryLock方法的線程如果未能獲得鎖會立刻返回false,線程可以繼續執行其他代碼而避免等待,這為程式員提供了更多自由。

                Lock介面還提供了一個newCondition () 方法,它返回一個Condition對象。Condition對象的作用和Object用於線程通知的wait-notify機制相同。

1.4       訊號量Semaphore

                有時候我們有多個相同的共用資源可以同時被多個線程使用。我們希望在鎖的基礎上加上一個計數器,根據資源的個數來初始化這個計數器,每次成功的lock操作都會使計數器的值減去1,只要計數器的值不為零就表示還有資源可以使用,lock操作就能成功。每次unlock操作都會給這個計數器加1。只有當計數器的值為0的時候lock操作才會阻塞當前線程。這就是Java中的訊號量Semaphore。

                Semaphore類提供的方法和Lock介面非常類似,當把訊號量的資源個數設定成1時,訊號量就退化為普通的鎖。

1.5       讀寫鎖ReadWriteLock

                對共用資源的訪問通常可以分為讀取和寫入。在有些應用情境中讀取可能需要花費較長時間,我們需要使用互斥鎖來阻止並發的寫入操作以保證資料的一致性。但是對於並發的讀取線程其實並不需要使用同步。事實上只有使資料發生變化的操作才需要同步,我們希望有一種方法可以把讀取和寫入區分開來,讀取和寫入的操作之間是互斥的,但是多個讀取操作可以同時進行,這樣可以有效提高讀取密集型程式的效能。J2SE5.0提供了ReadWriteLock介面並提供了實現該介面的ReentrantReadWriteLock類:

 

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();

}

 

從介面方法中不難看出讀寫鎖中包含讀鎖和寫鎖。實作類別ReentrantReadWriteLock為我們提供了更多便捷的方法來使用讀寫鎖,例如isWriteLocked可以用來檢測是否被寫鎖定。

 

2         線程通知

                除了同步鎖,Java Object還有兩個可用於線程間通知的同步方法wait和notify。調用對象wait方法的線程會被阻塞在該對象的等待隊列中直到其他線程調用notify方法來喚醒它。每次notify調用只能喚醒一個在等待隊列中的線程,notifyAll方法可以喚醒所有在該對象等待隊列中的線程。

 

3         最小化同步

                線程同步通過讓線程順序進入同步代碼塊解決了多個線程競爭同一資源而引起的不確定性,但是犧牲了效率,因此為了取得更好地效能,我們需要儘可能少地使用同步。事實上並不是所有的競爭條件都是需要避免的,只有當競爭條件出現在非安全執行緒的程式碼片段時才會引起問題。

3.1       Atomic 變數

           如果一個操作是原子操作,例如給一個boolean 變數賦值,我們就不需要同步。Java提供了一些Atomic類,使得一些本來不是原子操作(例如自增操作 ++,它包含了取值、加1、賦值三個原子操作)也能夠原子執行,從而不需要使用同步。

                Java提供了4個基本的原子類,AtomicInteger, AtomicLong, AtomicBoolean和AtomicReference分別提供針對int,long,boolean,object的原子操作。有意思的是如果你開啟JDK的原始碼想看看這些原子操作是如何?的,你會失望地發現代碼裡面沒有使用任何同步或其它技術。如果你在自己的程式中寫下同樣地代碼,那麼它們並不是原子的。

3.2       Thread Local 變數

                如果每個線程都有自己私人的成員變數,那麼我們也不需要同步。ThreadLocal就是線程的私人變數,每個使用ThreadLocal變數的線程都會有自己獨立的ThreadLocal對象,因此就不存在多個線程訪問同一個變數的問題。當然由於ThreadLocal變數為線程私人,它也就不可以用於在多個線程間共用狀態。

                ThreadLocal類並不神秘,它的實現原理比較簡單:每個Thread對象有自己用來儲存私人ThreadLocal對象的容器ThreadLocalMap,當某個線程調用ThreadLocal對象的get()方法來 取值的時候,get方法首先會取得當前線程對象,然後取出該線程的ThreadLocalMap,然後檢查自己是否已經在map中,如果自己已經存在,直接返回map中的value。如果不存在,把自己作key並初始化一個value加入到當前線程的map中。

 

    public T get() {

        Thread t = Thread.currentThread();

        ThreadLocalMap map = getMap(t);

        if (map != null) {

            ThreadLocalMap.Entry e = map.getEntry(this);

            if (e != null)

                return (T)e.value;

        }

        return setInitialValue();

}

 

4         線程池Thread Pool

                線程雖然不像進程需要那麼多資源,但是它的建立也是有一定開銷的,頻繁地建立和銷毀線程會降低程式的效能;此外應用程式可以建立線程的數量是受機器物理條件制約的,過多的線程會耗盡機器的資源,因此我們在設計程式的時候需要限制並發線程的數量。解決這兩個問題的通常做法是使用線程池。線程池在啟動的時候一次性初始化若干個線程(也可以根據負載按需啟動,也有閑置一定時間的線程會被銷毀的策略),然後程式把任務交給線程池去執行而不是直接交給某個線程執行,由線程池給這些任務分配線程。當某個線程執行完一個任務後,線程池會把它設成空閑狀態以備下一個任務重用而不是銷毀它。線程池在初始化的時候需要指定線程數量上限,當並發任務數量超過線程數量的時候,線程池不會再建立新的線程而是讓新任務等待,這樣我們就不在需要擔心線程數量過多耗盡系統資源了。JDK1.5開始為我們提供了標準的線程池。

4.1       執行器Executor

                Java的線程池實現了以下Executor介面:

 

public interface Executor {

    void execute(Runnable command);

}

               

                在多線程編程中,執行器是一種常用的設計模式,它的好處在於提供了一種簡單有效編程模型,我們只需把需要並發處理的工作拆分成獨立的任務,然後交給執行器去執行即可而不必關心線程的建立,分配和調度。J2SE5.0主要提供了兩種功能的執行器:ThreadPoolExecutor和ScheduledThreadPoolExecutor。ThreadPoolExecutor是基本的線程池實現,ScheduledThreadPoolExecutor在前者基礎上增加了任務調度的功能,在把任務交給它時我們可以指定任務的執行時間,而不是立刻執行。

                java.util.concurrent.Executors是用來建立線程池的工廠類,通過它提供的Factory 方法,我們可以方便地建立不同特性的線程池。

4.2       Future介面

                Executor介面並沒有看起來那麼理想,有時候我們執行一個任務是要得到計算的結果,有時候我們需要對任務有更多控制,例如知道它是否完成,或者中途終止它。返回void的execute方法並不能滿足我們這些需求。當然我們可以在傳入的Runnable類上下功夫來提供類似的功能,但是這樣做繁瑣且容易出錯。既然J2SE為我們提供了線程池的標準實現把我們從多線程編程中解放出來,這些常見的需求當然也會很好地滿足。事實上線程池實現了一個更為豐富的ExecutorService介面,它定義了執行任務並返回代表該任務的Future對象的submit方法。

           通過Future介面,我們可以查看已經被提交給線程池執行的任務是否完成,擷取執行的結果或者終止任務。

4.3       Runnable 和Callable 介面

                實現了Runnable或Callable介面的類都可以作為任務提交給線程池執行,這兩個介面的主要區別在於Callable的call方法有結果返回並且可以拋出異常而Runnable的run方法返回void且不允許有可檢查的異常拋出(只能拋runtime exception)。因此如果我們的任務執行後有結果返回,應該使用Callable介面。

 

5         線程和集合類5.1       安全執行緒的集合類

·         java.util.Vector

·         java.util.Stack

·         java.util.HashTable

·         java.util.concurrent.ConcurrentHashMap

·         java.util.concurrent.CopyOnWriteArrayList

·         java.util.concurrent.CopyOnWriteArraySet

·         java.util.concurrent.ConcurrentLinkedQueue

5.2       非安全執行緒集合類

·         java.util.BitSet

·         java.util.HashSet (LinkedHashSet)

·         java.util.TreeSet

·         java.util.HashMap (WeekHashMap, TreeMap, LinkedHashMap, IdentityHashMap)

·         java.util.ArrayList (LinkedList)

·         java.util.PriorityQueue

                這些非安全執行緒的集合可以通過java.util.Collections.SynchronizedList、SynchronizedMap、SynchronizedSet等方法封裝成安全執行緒的集合。封裝器類簡單地給被封裝集合的各項操作加上了synchronized保護。值得注意的是在使用遊標遍曆這些封裝器集合的時候必須加上額外的synchronized保護,否則會出現問題。

     List list = Collections.synchronizedList(new ArrayList());

         ...

     synchronized(list) {

         Iterator i = list.iterator(); // Must be in synchronized block

         while (i.hasNext())

             foo(i.next());

     }

5.3       線程通知集合類

·         java.util.concurrent.ArrayBlockingQueue

·         java.util.concurrent.LinkedBlockingQueue

·         java.util.concurrent.SynchronousQueue

·         java.util.concurrent.PriorityBlockingQueue

·         java.util.concurrent.DelayQueue

                這些集合類都實現了BlockingQueue介面。阻塞隊列的特點是當從隊列中取出元素時如果隊列為空白,線程會被阻塞直到隊列中有元素被插入。當從隊列中插入元素時如果隊列已滿,線程會被阻塞直到隊列中有元素被取出出現空閑空間。阻塞隊列可以用來實現生產者消費者模式(Producer/Consumer Pattern) 。

原文地址:http://blog.csdn.net/ring0hx/article/details/6858582

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.