1、前言
一直以來,用過多線程,但是,在某些細節方面總是不注意,現特將一些基本知識點進行歸納彙總,以備後面的使用。
2、Java多線程的實現方式
JAVA多線程實現方式主要有三種:繼承Thread類、實現Runnable接口、使用ExecutorService、Callable、Future實現有返回結果的多線程。其中前兩種方式線程執行完後都沒有返回值,只有最後一種是帶返回值的。
2.1 繼承Thread類實現多線程
繼承Thread類的方法儘管被我列爲一種多線程實現方式,但Thread本質上也是實現了Runnable接口的一個實例,它代表一個線程的實例,並且,啓動線程的唯一方法就是通過Thread類的start()實例方法。
- public class MyThread extends Thread {
- public void run() {
- System.out.println("MyThread.run()");
- }
- }
- MyThread myThread1 = new MyThread();
- myThread1.start();
如果自己的類已經extends另一個類,就無法直接extends Thread,此時,必須實現一個Runnable接口,如下:
- public class MyThread extends OtherClass implements Runnable {
- public void run() {
- System.out.println("MyThread.run()");
- }
- }
- MyThread myThread = new MyThread();
- Thread thread = new Thread(myThread);
- thread.start();
ExecutorService、Callable、Future這個對象實際上都是屬於Executor框架中的功能類。想要詳細瞭解Executor框架的可以訪問點擊打開鏈接,這裏面對該框架做了很詳細的解釋。返回結果的線程是在JDK1.5中引入的新特徵,確實很實用,有了這種特徵我就不需要再爲了得到返回值而大費周折了,而且即便實現了也可能漏洞百出。
可返回值的任務必須實現Callable接口,類似的,無返回值的任務必須Runnable接口。執行Callable任務後,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務返回的Object了,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。下面提供了一個完整的有返回結果的多線程測試例子,在JDK1.5下驗證過沒問題可以直接使用。
3、多線程的併發
3.1 volatile關鍵詞
用來對共享變量的訪問進行同步,上一次寫入操作的結果對下一次讀取操作是肯定可見的。(在寫入volatile變量值之後,CPU緩存中的內容會被寫回內存;在讀取volatile變量時,CPU緩存中的對應內容會被置爲失效,重新從主存中進行讀取),volatile不使用鎖,性能優於synchronized關鍵詞。
3.2、final關鍵詞
final關鍵詞聲明的域的值只能被初始化一次,一般在構造方法中初始化。。(在多線程開發中,final域通常用來實現不可變對象)
當對象中的共享變量的值不可能發生變化時,在多線程中也就不需要同步機制來進行處理,故在多線程開發中應儘可能使用不可變對象。
另外,在代碼執行時,final域的值可以被保存在寄存器中,而不用從主存中頻繁重新讀取。
3.3、java基本類型的原子操作
1)基本類型,引用類型的複製引用是原子操作;(即一條指令完成)
2)long與double的賦值,引用是可以分割的,非原子操作;
3)要在線程間共享long或double的字段時,必須在synchronized中操作,或是聲明成volatile
4 Java多線程的同步方式
4.1、synchronized關鍵字
方法或代碼塊的互斥性來完成實際上的一個原子操作。(方法或代碼塊在被一個線程調用時,其他線程處於等待狀態)
所有的Java對象都有一個與synchronzied關聯的監視器對象(monitor),允許線程在該監視器對象上進行加鎖和解鎖操作。
a、靜態方法:Java類對應的Class類的對象所關聯的監視器對象。
b、實例方法:當前對象實例所關聯的監視器對象。
c、代碼塊:代碼塊聲明中的對象所關聯的監視器對象。
注:當鎖被釋放,對共享變量的修改會寫入主存;當獲得鎖,CPU緩存中的內容被置爲無效。編譯器在處理synchronized方法或代碼塊,不會把其中包含的代碼移動到synchronized方法或代碼塊之外,從而避免了由於代碼重排而造成的問題。
4.2 Object類的wait、notify和notifyAll方法
生產者和消費者模式,判斷緩衝區是否滿來消費,緩衝區是否空來生產的邏輯。如果用while 和 volatile也可以做,不過本質上會讓線程處於忙等待,佔用CPU時間,對性能造成影響。
wait: 將當前線程放入,該對象的等待池中,線程A調用了B對象的wait()方法,線程A進入B對象的等待池,並且釋放B的鎖。(這裏,線程A必須持有B的鎖,所以調用的代碼必須在synchronized修飾下,否則直接拋出java.lang.IllegalMonitorStateException異常)。
notify:將該對象中等待池中的線程,隨機選取一個放入對象的鎖池,噹噹前線程結束後釋放掉鎖, 鎖池中的線程即可競爭對象的鎖來獲得執行機會。
notifyAll:將對象中等待池中的線程,全部放入鎖池。
(notify鎖喚醒的線程選擇由虛擬機實現來決定,不能保證一個對象鎖關聯的等待集合中的線程按照所期望的順序被喚醒,很可能一個線程被喚醒之後,發現他所要求的條件並沒有滿足,而重新進入等待池。因爲當等待池中包含多個線程時,一般使用notifyAll方法,不過該方法會導致線程在沒有必要的情況下被喚醒,之後又馬上進入等待池,對性能有影響,不過能保證程序的正確性)
工作流程:a、Consumer線程A 來 看產品,發現產品爲空,調用產品對象的wait(),線程A進入產品對象的等待池並釋放產品的鎖。
b、Producer線程B獲得產品的鎖,執行產品的notifyAll(),Consumer線程A從產品的等待池進入鎖池,Producer線程B生產產品,然後退出釋放鎖。
c、Consumer線程A獲得產品鎖,進入執行,發現有產品,消費產品,然後退出。
生產者消費者問題是研究多線程程序時繞不開的經典問題之一,它描述是有一塊緩衝區作爲倉庫,生產者可以將產品放入倉庫,消費者則可以從倉庫中取走產品。解決生產者/消費者問題的方法可分爲兩類:(1)採用某種機制保護生產者和消費者之間的同步;(2)在生產者和消費者之間建立一個管道。第一種方式有較高的效率,並且易於實現,代碼的可控制性較好,屬於常用的模式。第二種管道緩衝區不易控制,被傳輸數據對象不易於封裝等,實用性不強。因此本文只介紹同步機制實現的生產者/消費者問題。
同步問題核心在於:如何保證同一資源被多個線程併發訪問時的完整性。常用的同步方法是採用信號或加鎖機制,保證資源在任意時刻至多被一個線程訪問。Java語言在多線程編程上實現了完全對象化,提供了對同步機制的良好支持。在Java中一共有四種方法支持同步,其中前三個是同步方法,一個是管道方法。
(1)wait() / notify()方法
(2)await() / signal()方法
(3)BlockingQueue阻塞隊列方法
(4)PipedInputStream / PipedOutputStream
還有一點需要特別強調:調用notify和notifyAll方法後,當前線程並不會立即放棄鎖的持有權,而必須要等待當前同步代碼塊執行完纔會讓出鎖。
參考博客:
http://lavasoft.blog.51cto.com/62575/27069
http://blog.csdn.net/escaflone/article/details/10418651
http://blog.csdn.net/monkey_d_meng/article/details/6251879
http://www.cnblogs.com/riskyer/p/3263032.html
http://my.oschina.net/hanzhankang/blog/193917