標籤:雜湊 知識點 volatile 拋出異常 相關 adl 加鎖 ldo 方便
用多線程只有一個目的,那就是更好的利用cpu的資源,因為所有的多線程代碼都可以用單線程來實現。說這個話其實只有一半對,因為反應“多角色”的程式碼,最起碼每個角色要給他一個線程吧,否則連實際情境都無法類比,當然也沒法說能用單線程來實現:比如最常見的“生產者,消費者模型”。很多人都對其中的一些概念不夠明確,如同步、並發等等,讓我們先建立一個資料字典,以免產生誤會。多線程:指的是這個程式(一個進程)運行時產生了不止一個線程並行與並發:並行:多個cpu執行個體或者多台機器同時執行一段處理邏輯,是真正的同時。並發:通過cpu調度演算法,讓使用者看上去同時執行,實際上從cpu操作層面不是真正的同時。並發往往在情境中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用TPS或者QPS來反應這個系統的處理能力。並發與並行安全執行緒:經常用來描繪一段代碼。指在並發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的記憶體,cpu是不是夠用即可。反過來,線程不安全就意味著線程的調度順序會影響最終結果,如不加事務的轉賬代碼:void transferMoney(User from, User to, float amount){ to.setMoney(to.getBalance() + amount); from.setMoney(from.getBalance() - amount);}同步:Java中的同步指的是通過人為的控制和調度,保證共用資源的多線程訪問成為安全執行緒,來保證結果的準確。如上面的代碼簡單加入@synchronized關鍵字。在保證結果準確的同時,提高效能,才是優秀的程式。安全執行緒的優先順序高於效能。好了,讓我們開始吧。我準備分成幾部分來總結涉及到多線程的內容:紮好馬步:線程的狀態內功心法:每個對象都有的方法(機制)太祖長拳:基本線程類九陰真經:進階多線程式控制制類紮好馬步:線程的狀態先來兩張圖:線程狀態線程狀態轉換各種狀態一目瞭然,值得一提的是"blocked"這個狀態:線程在Running的過程中可能會遇到阻塞(Blocked)情況調用join()和sleep()方法,sleep()時間結束或被打斷,join()中斷,IO完成都會回到Runnable狀態,等待JVM的調度。調用wait(),使該線程處於等待池(wait blocked pool),直到notify()/notifyAll(),線程被喚醒被放到鎖定池(lock blocked pool ),釋放同步鎖使線程回到可運行狀態(Runnable)對Running狀態的線程加同步鎖(Synchronized)使其進入(lock blocked pool ),同步鎖被釋放進入可運行狀態(Runnable)。此外,在runnable狀態的線程是處於被調度的線程,此時的調度順序是不一定的。Thread類中的yield方法可以讓一個running狀態的線程轉入runnable。內功心法:每個對象都有的方法(機制)synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來瞭解他們monitor他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確monitor的概念,Java中的每個對象都有一個監視器,來監測並發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在synchronized 範圍內,監視器發揮作用。wait/notify必須存在於synchronized塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味著wait之後,其他線程可以進入同步塊執行。當某代碼並不持有監視器的使用權時(中5的狀態,即脫離同步塊)去wait或notify,會拋出java.lang.IllegalMonitorStateException。也包括在synchronized塊中去調用另一個對象的wait/notify,因為不同對象的監視器不同,同樣會拋出此異常。再講用法:synchronized單獨使用:代碼塊:如下,在多線程環境下,synchronized塊中的方法擷取了lock執行個體的monitor,如果執行個體相同,那麼只有一個線程能執行該塊內容public class Thread1 implements Runnable { Object lock; public void run() { synchronized(lock){ ..do something } }}直接用於方法: 相當於上面代碼中用lock來鎖定的效果,實際擷取的是Thread1類的monitor。更進一步,如果修飾的是static方法,則鎖定該類所有執行個體。public class Thread1 implements Runnable { public synchronized void run() { ..do something }}synchronized, wait, notify結合:典型情境生產者消費者問題/** * 生產者生產出來的產品交給店員 */ public synchronized void produce() { if(this.product >= MAX_PRODUCT) { try { wait(); System.out.println("產品已滿,請稍候再生產"); } catch(InterruptedException e) { e.printStackTrace(); } return; } this.product++; System.out.println("生產者生產第" + this.product + "個產品."); notifyAll(); //通知等待區的消費者可以取出產品了 } /** * 消費者從店員取產品 */ public synchronized void consume() { if(this.product <= MIN_PRODUCT) { try { wait(); System.out.println("缺貨,稍候再取"); } catch (InterruptedException e) { e.printStackTrace(); } return; } System.out.println("消費者取走了第" + this.product + "個產品."); this.product--; notifyAll(); //通知等待去的生產者可以生產產品了 }volatile多線程的記憶體模型:main memory(主存)、working memory(線程棧),在處理資料時,線程會把值從主存load到本地棧,完成操作後再save回去(volatile關鍵詞的作用:每次針對該變數的操作都激發一次load and save)。volatile針對多線程使用的變數如果不是volatile或者final修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一執行個體的同一屬性本身只有一個副本。但是多線程是會緩衝值的,本質上,volatile就是不去緩衝,直接取值。線上程安全的情況下加volatile會犧牲效能。太祖長拳:基本線程類基本線程類指的是Thread類,Runnable介面,Callable介面Thread 類實現了Runnable介面,啟動一個線程的方法: MyThread my = new MyThread(); my.start();Thread類相關方法://當前線程可轉讓cpu控制權,讓別的就緒狀態線程運行(切換)public static Thread.yield() //暫停一段時間public static Thread.sleep() //在一個線程中調用other.join(),將等待other執行完後才繼續本線程。 public join()//後兩個函數皆可以被打斷public interrupte()中斷關於中斷:它並不像stop方法那樣會中斷一個正在啟動並執行線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否為true)。終端只會影響到wait狀態、sleep狀態和join狀態。被打斷的線程會拋出InterruptedException。Thread.interrupted()檢查當前線程是否發生中斷,返回booleansynchronized在獲鎖的過程中是不能被中斷的。中斷是一個狀態!interrupt()方法只是將這個狀態置為true而已。所以說正常啟動並執行程式不去檢測狀態,就不會終止,而wait等阻塞方法會去檢查並拋出異常。如果在正常啟動並執行程式中添加while(!Thread.interrupted()) ,則同樣可以在中斷後離開代碼體Thread類最佳實務:寫的時候最好要設定線程名稱 Thread.name,並設定線程組 ThreadGroup,目的是方便管理。在出現問題的時候,列印線程棧 (jstack -pid) 一眼就可以看出是哪個線程出的問題,這個線程是幹什麼的。如何擷取線程中的異常不能用try,catch來擷取線程中的異常Runnable與Thread類似Callablefuture模式:併發模式的一種,可以有兩種形式,即無阻塞和阻塞,分別是isDone和get。其中Future對象用來存放該線程的傳回值以及狀態ExecutorService e = Executors.newFixedThreadPool(3); //submit方法有多重參數版本,及支援callable也能夠支援runnable介面類型.Future future = e.submit(new myCallable());future.isDone() //return true,false 無阻塞future.get() // return 傳回值,阻塞直到該線程運行結束九陰真經:進階多線程式控制制類以上都屬於內功心法,接下來是實際項目中常用到的工具了,Java1.5提供了一個非常高效實用的多線程包:java.util.concurrent, 提供了大量進階工具,可以協助開發人員編寫高效、易維護、結構清晰的Java多線程程式。1.ThreadLocal類用處:儲存線程的獨立變數。對一個線程類(繼承自Thread)當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的線程提供獨立的變數副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於使用者登入控制,如記錄session資訊。實現:每個Thread都持有一個TreadLocalMap類型的變數(該類是一個輕量級的Map,功能與map一樣,區別是桶裡放的是entry而不是entry的鏈表。功能還是一個map。)以本身為key,以目標為value。主要方法是get()和set(T a),set之後在map裡維護一個threadLocal -> a,get時將a返回。ThreadLocal是一個特殊的容器。2.原子類(AtomicInteger、AtomicBoolean……)如果使用atomic wrapper class(原子封裝類)如atomicInteger(原子整數),或者使用自己保證原子的操作,則等同於synchronized//傳回值為booleanAtomicInteger.compareAndSet(int expect,int update)該方法可用於實現樂觀鎖,考慮文中最初提到的如下情境:a給b付款10元,a扣了10元,b要加10元。此時c給b2元,但是b的加十元代碼約為:if(b.value.compareAndSet(old, value)){ return ;}else{ //try again // if that fails, rollback and log}AtomicReference(原子引用)對於AtomicReference 來講,也許對象會出現,屬性丟失的情況,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。這時候,AtomicStampedReference就派上用場了。這也是一個很常用的思路,即加上版本號碼3.Lock類 lock: 在java.util.concurrent包內。共有三個實現:ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock主要目的是和synchronized一樣, 兩者都是為瞭解決同步問題,處理資源爭端而產生的技術。功能類似但有一些區別。區別如下:lock更靈活,可以自由定義多把鎖的枷鎖解鎖順序(synchronized要按照先加的後解順序)提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有trylock的帶逾時時間版本。本質上和監視器鎖(即synchronized是一樣的)能力越大,責任越大,必須控制好加鎖和解鎖,否則會導致災難。和Condition類的結合。效能更高,對比如:synchronized和Lock效能對比ReentrantLock 可重新進入的意義在於持有鎖的線程可以繼續持有,並且要釋放對等的次數後才真正釋放該鎖。使用方法是:1.先new一個執行個體static ReentrantLock r=new ReentrantLock();2.加鎖 r.lock()或r.lo ckInterruptibly(); 此處也是個不同,後者可被打斷。當a線程lock後,b線程阻塞,此時如果是lockInterruptibly,那麼在調用b.interrupt()之後,b線程退出阻塞,並放棄對資源的爭搶,進入catch塊。(如果使用後者,必須throw interruptable exception 或catch) 3.釋放鎖 r.unlock()必須做!何為必須做呢,要放在finally裡面。以防止異常跳出了正常流程,導致災難。這裡補充一個小知識點,finally是可以信任的:經過測試,哪怕是發生了OutofMemoryError,finally塊中的語句執行也能夠得到保證。ReentrantReadWriteLock可重新進入讀寫鎖(讀寫鎖的一個實現) ReentrantReadWriteLock lock = new ReentrantReadWriteLock() ReadLock r = lock.readLock(); WriteLock w = lock.writeLock();兩者都有lock,unlock方法。寫寫,寫讀互斥;讀讀不互斥。可以實現並發讀的高效安全執行緒代碼4.容器類這裡就討論比較常用的兩個:BlockingQueueConcurrentHashMapBlockingQueue阻塞隊列。該類是java.util.concurrent包下的重要類,通過對Queue的學習可以得知,這個queue是單向隊列,可以在隊列頭添加元素和在隊尾刪除或取出元素。類似於一個管 道,特別適用於先進先出策略的一些應用情境。普通的queue介面主要實現有PriorityQueue(優先隊列),有興趣可以研究BlockingQueue在隊列的基礎上添加了多線程協作的功能:BlockingQueue除了傳統的queue功能(表格左邊的兩列)之外,還提供了阻塞介面put和take,帶逾時功能的阻塞介面offer和poll。put會在隊列滿的時候阻塞,直到有空間時被喚醒;take在隊 列空的時候阻塞,直到有東西拿的時候才被喚醒。用於生產者-消費者模型尤其好用,堪稱神器。常見的阻塞隊列有:ArrayListBlockingQueueLinkedListBlockingQueueDelayQueueSynchronousQueueConcurrentHashMap高效的安全執行緒雜湊map。請對比hashTable , concurrentHashMap, HashMap5.管理類管理類的概念比較泛,用於管理線程,本身不是多線程的,但提供了一些機制來利用上述的工具做一些封裝。瞭解到的值得一提的管理類:ThreadPoolExecutor和 JMX架構下的系統級管理類 ThreadMXBeanThreadPoolExecutor如果不瞭解這個類,應該瞭解前面提到的ExecutorService,開一個自己的線程池非常方便:ExecutorService e = Executors.newCachedThreadPool(); ExecutorService e = Executors.newSingleThreadExecutor(); ExecutorService e = Executors.newFixedThreadPool(3); // 第一種是可變大小線程池,按照任務數來分配線程, // 第二種是單線程池,相當於FixedThreadPool(1) // 第三種是固定大小線程池。 // 然後運行 e.execute(new MyRunnableImpl());該類內部是通過ThreadPoolExecutor實現的,掌握該類有助於理解線程池的管理,本質上,他們都是ThreadPoolExecutor類的各種實現版本。請參見javadoc:ThreadPoolExecutor參數解釋翻譯一下:corePoolSize:池內線程初始值與最小值,就算是空閑狀態,也會保持該數量線程。maximumPoolSize:線程最大值,線程的增長始終不會超過該值。keepAliveTime:當池內線程數高於corePoolSize時,經過多少時間多餘的空閑線程才會被回收。回收前處於wait狀態unit:時間單位,可以使用TimeUnit的執行個體,如TimeUnit.MILLISECONDS workQueue:待入任務(Runnable)的等待場所,該參數主要影響調度策略,如公平與否,是否產生餓死(starving)threadFactory:線程工廠類,有預設實現,如果有自訂的需要則需要自己實現ThreadFactory介面並作為參數傳入。
Java多線程學習