原創不易,如需轉載,請註明出處 "https://www.cnblogs.com/baixianlong/p/10739579.html" ,希望大家多多支持!!! 一、線程基礎 1、線程與進程 線程是指進程中的一個執行流程,一個進程中可以運行多個線程。 進程是指一個記憶體中運行的應用程式,每個進程都 ...
原創不易,如需轉載,請註明出處https://www.cnblogs.com/baixianlong/p/10739579.html,希望大家多多支持!!!
一、線程基礎
1、線程與進程
- 線程是指進程中的一個執行流程,一個進程中可以運行多個線程。
- 進程是指一個記憶體中運行的應用程式,每個進程都有自己獨立的一塊記憶體空間,即進程空間或(虛空間),比如一個qq.exe就是一個進程。
2、線程的特點
- 線程共用分配給該進程的所有資源
- 線程之間實際上輪換執行(也就是線程切換)
- 一個程式至少有一個進程,一個進程至少有一個線程
- 線程不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個線程執行控制
- 線程有自己的堆棧和局部變數,但線程之間沒有單獨的地址空間,一個線程包含以下內容:
- 一個指向當前被執行指令的指令指針
- 一個棧
- 一個寄存器值的集合,定義了一部分描述正在執行線程的處理器狀態的值
- 一個私有的數據區
3、線程的作用
- 進程在執行過程中擁有獨立的記憶體單元,而多個線程共用記憶體,從而極大地提高了程式的運行效率(併發執行)
4、線程的創建
- 繼承Thread類
- 實現Runnable介面
- 通過ThreadPool獲取
5、線程的狀態(生命周期)
- 創建:當用new操作符創建一個線程時。此時程式還沒有開始運行線程中的代碼
- 就緒:當start()方法返回後,線程就處於就緒狀態
- 運行:當線程獲得CPU時間後,它才進入運行狀態,真正開始執行run()方法
- 阻塞:所謂阻塞狀態是正在運行的線程沒有運行結束,暫時讓出CPU,這時其他處於就緒狀態的線程就可以獲得CPU時間,進入運行狀態
死亡:1、run方法正常退出而自然死亡;2、一個未捕獲的異常終止了run方法而使線程猝死
6、線程的優先順序、線程讓步yield、線程合併join、線程睡眠sleep
- 優先順序:線程總是存在優先順序,優先順序範圍在1~10之間(數值越大優先順序越高),線程預設優先順序是5,優先順序高的理論上先執行,但實際不一定。
- 線程讓步:yield方法調用後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被調度到運行狀態(使用yield()的目的是讓相同優先順序的線程之間能適當的輪轉執行)。
- 線程合併:保證當前線程停止執行,直到該線程所加入的線程完成為止。然而,如果它加入的線程沒有存活,則當前線程不需要停止。
- 線程睡眠:sleep方法暫停當前線程後,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。
7、線程的分類(以下兩者的唯一區別之處就在虛擬機的離開)
- 守護線程: thread.setDaemon(true),必須在thread.start()之前設置,GC線程就是一個守護線程。
- 普通線程
8、正確結束線程(給出一些方案)
- 使用Thread.stop()方法,但是該方法已經被廢棄了,使用它是極端不安全的,會造成數據不一致的問題。
- 使用interrupt()方法停止一個線程,直接調用該方法不會終止一個正在運行的線程,需要加入一個判斷語句才可以完成線程的停止。
- 使用共用變數的方式,在這種方式中,之所以引入共用變數,是因為該變數可以被多個執行相同任務的線程用來作為是否中斷的信號,通知中斷線程的執行。
二、線程同步
1、線程同步的意義
- 線程的同步是為了防止多個線程訪問一個數據對象時,對數據造成的破壞
2、鎖的原理
- Java中每個對象都有一個內置鎖,內置鎖是一個互斥鎖,這就是意味著最多只有一個線程能夠獲得該鎖。
- 當程式運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
- 當程式運行到synchronized同步方法或代碼塊時才該對象鎖才起作用。
- 一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
- 釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
3、鎖和同步的理解
- 只能同步方法,而不能同步變數和類。
- 每個對象只有一個鎖,當提到同步時,應該清楚在什麼上同步,也就是說,在哪個對象上同步。
- 不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
- 如果兩個線程要執行一個類中的synchronized方法,並且兩個線程使用相同的實例來調用方法,那麼一次只能有一個線程能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個線程在對象上獲得一個鎖,就沒有任何其他線程可以進入(該對象的)類中的任何一個同步方法。
- 如果線程擁有同步和非同步方法,則非同步方法可以被多個線程自由訪問而不受鎖的限制。
- 線程睡眠時,它所持的任何鎖都不會釋放。
- 線程可以獲得多個鎖。比如,在一個對象的同步方法裡面調用另外一個對象的同步方法,則獲取了兩個對象的同步鎖。
- 同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分代碼塊。
- 在使用同步代碼塊時候,應該指定在哪個對象上同步,也就是說要獲取哪個對象的鎖。
4、對象鎖和類鎖的區別
- 對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。
- 類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不幹擾的,但是每個類只有一個類鎖。
5、線程的死鎖及規避
- 死鎖是線程間相互等待鎖鎖造成的,一旦程式發生死鎖,程式將死掉。
- 如果我們能夠避免在對象的同步方法中調用其它對象的同步方法,那麼就可以避免死鎖產生的可能性。
6、volatile關鍵字(推薦大家一片文章)
- 正確使用 volatile變數
- 與鎖相比,Volatile 變數是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優於鎖的性能和伸縮性。
- 如果嚴格遵循 volatile 的使用條件 —— 即變數真正獨立於其他變數和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化代碼。
三、線程的交互
1、線程交互的基礎知識
- void notify()——喚醒在此對象監視器上等待的單個線程。
- void notifyAll()——喚醒在此對象監視器上等待的所有線程。
- void wait()——導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法。
- void wait(longtimeout)——導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
- void wait(longtimeout, int nanos)——導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量。
2、註意點
- 必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。
- 當在對象上調用wait()方法時,執行該代碼的線程立即放棄它在對象上的鎖。然而調用notify()時,並不意味著這時線程會放棄其鎖。如果線程仍然在完成同步代碼,則線程在移出之前不會放棄鎖。因此,調用notify()並不意味著這時該鎖變得可用。
四、線程池
1、好處
- 降低資源消耗。通過重覆利用已創建的線程降低線程創建和銷毀造成的消耗。
- 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
- 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
2、線程池的創建使用
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
}
- 參數介紹:
- corePoolSize:核心池的大小
- maximumPoolSize:線程池最大線程數
- keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止
- unit:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小時
- TimeUnit.MINUTES; //分鐘
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //納秒
- workQueue:一個阻塞隊列,用來存儲等待執行的任務
- threadFactory:線程工廠,主要用來創建線程
- handler:表示當拒絕處理任務時的策略,有以下四種取值:
- ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
- ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
- ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重覆此過程)
- ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
3、java預設實現的4中線程池(不建議使用)
- newCachedThreadPool:創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。線程池為無限大,當執行第二個任務時第一個任務已經完成,會復用執行第一個任務的線程,而不用每次新建線程。
- newFixedThreadPool:創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()。
- newScheduledThreadPool:創建一個定長線程池,支持定時及周期性任務執行。
- newSingleThreadExecutor:創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
五、併發編程相關內容
1、synchronized 的局限性 與 Lock 的優點
- 首先synchronized是java內置關鍵字,在jvm層面,Lock是個java類;
- synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
- synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;
- 用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了;
- synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)
- Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。
2、Lock 和 ReadWriteLock
Lock介面,ReentrantLock(可重入鎖)是唯一的實現類。
public interface Lock { void lock(); //lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。 //也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。 void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
ReadWriteLock介面,ReentrantReadWriteLock實現了ReadWriteLock介面。
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
- 讀讀共用
- 寫寫互斥
- 讀寫互斥
- 寫讀互斥
3、信號量(Semaphore)
- 介紹:Java的信號量實際上是一個功能完畢的計數器,對控制一定資源的消費與回收有著很重要的意義,信號量常常用於多線程的代碼中,並能監控有多少數目的線程等待獲取資源,並且通過信號量可以得知可用資源的數目等等。
- 特點:
- 以控制某個資源可被同時訪問的個數,通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。
- 單個信號量的Semaphore對象可以實現互斥鎖的功能,並且可以是由一個線程獲得了“鎖”,再由另一個線程釋放“鎖”,這可應用於死鎖恢復的一些場合。
- 信號量解決了鎖一次只能讓一個線程訪問資源的問題,信號量可以指定多個線程,同時訪問一個資源。
分為公平模式和非公平模式(預設非公平)
public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
區別在於:公平模式會考慮是否已經有線程在等待,如果有則直接返回-1表示獲取失敗;而非公平模式不會關心有沒有線程在等待,會去快速競爭資源的使用權。
說到競爭就得提到AbstractQueuedSynchronizer同步框架,一個僅僅需要簡單繼承就可以實現複雜線程的同步方案,建議大家去研究一下。
4、閉鎖(CountDownLatch)
介紹:閉鎖是一種同步工具,可以延遲線程的進度直到終止狀態。可以把它理解為一扇門,當閉鎖到達結束狀態之前,這扇門一直是關閉的,沒有任何線程可以通過。當閉鎖到達結束狀態時,這扇門會打開並允許所有線程通過,並且閉鎖打開後不可再改變狀態。
閉鎖可以確保某些任務直到其他任務完成後才繼續往下執行。- 使用介紹
- 構造器中的計數值(count)實際上就是閉鎖需要等待的線程數量。這個值只能被設置一次,而且CountDownLatch沒有提供任何機制去重新設置這個計數值。
- 與CountDownLatch的第一次交互是主線程等待其他線程。主線程必須在啟動其他線程後立即調用CountDownLatch.await()方法。這樣主線程的操作就會在這個方法上阻塞,直到其他線程完成各自的任務。
- 其他N 個線程必須引用閉鎖對象,因為他們需要通知CountDownLatch對象,他們已經完成了各自的任務。這種通知機制是通過 CountDownLatch.countDown()方法來完成的;每調用一次這個方法,在構造函數中初始化的count值就減1。所以當N個線程都調 用了這個方法,count的值等於0,然後主線程就能通過await()方法,恢復執行自己的任務。
4、柵欄(CyclicBarrier)
介紹:柵欄類似於閉鎖,它能阻塞一組線程直到某個事件的發生。柵欄與閉鎖的關鍵區別在於,所有的線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他線程。CyclicBarrier可以使一定數量的線程反覆地在柵欄位置處彙集。當線程到達柵欄位置時將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達柵欄位置,那麼柵欄將打開,此時所有的線程都將被釋放,而柵欄將被重置以便下次使用。
- CyclicBarrier和CountDownLatch的區別:
- CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能夠處理更為複雜的場景;
- CyclicBarrier還提供了一些其他有用的方法,比如getNumberWaiting()方法可以獲得CyclicBarrier阻塞的線程數量,isBroken()方法用來瞭解阻塞的線程是否被中斷;
- CountDownLatch允許一個或多個線程等待一組事件的產生,而CyclicBarrier用於等待其他線程運行到柵欄位置。
5、原子量 (Atomic)
介紹:Atomic一詞跟原子有點關係,後者曾被人認為是最小物質的單位。電腦中的Atomic是指不能分割成若幹部分的意思。如果一段代碼被認為是Atomic,則表示這段代碼在執行過程中,是不能被中斷的。通常來說,原子指令由硬體提供,供軟體來實現原子方法(某個線程進入該方法後,就不會被中斷,直到其執行完成)
特性:在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具有排他性,即當某個線程進入方法,執行其中的指令時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另一個線程進入,這隻是一種邏輯上的理解。實際上是藉助硬體的相關指令來實現的,不會阻塞線程(或者說只是在硬體級別上阻塞了)。
註意:原子量雖然可以保證單個變數在某一個操作過程的安全,但無法保證你整個代碼塊,或者整個程式的安全性。因此,通常還應該使用鎖等同步機制來控制整個程式的安全性。
6、Condition
- 介紹:在Java程式中,任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object類上),主要包括wait()、wait(long)、notify()、notifyAll()方法,這些方法與synchronized關鍵字配合,可以實現等待/通知模式。Condition介面也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有區別的。
- Condition與Object中的wati,notify,notifyAll區別:
- Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
不同的是,Object中的這些方法是和同步鎖捆綁使用的;而Condition是需要與互斥鎖/共用鎖捆綁使用的。 - Condition它更強大的地方在於:能夠更加精細的控制多線程的休眠與喚醒。對於同一個鎖,我們可以創建多個Condition,在不同的情況下使用不同的Condition。如果採用Object類中的wait(),notify(),notifyAll()實現該緩衝區,當向緩衝區寫入數據之後需要喚醒"讀線程"時,不可能通過notify()或notifyAll()明確的指定喚醒"讀線程",而只能通過notifyAll喚醒所有線程(但是notifyAll無法區分喚醒的線程是讀線程,還是寫線程)。 但是,通過Condition,就能明確的指定喚醒讀線程。
- Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
7、併發編程概覽
六、總結
- 到此線程的基本內容介紹就差不多了,這篇文章偏理論一些,每個知識點的介紹並不全面。
- 大家可以以此篇文章為索引,來展開對併發編程的深入學習,細細咀嚼每個知識點,相信你會有巨大的收穫!!!
個人博客地址:
cnblogs:https://www.cnblogs.com/baixianlong
csdn:https://blog.csdn.net/tiantuo6513
segmentfault:https://segmentfault.com/u/baixianlong
github:https://github.com/xianlongbai