Java 并發(fā) 學習筆記

并發(fā)

最近重新復習了一邊并發(fā)的知識撼港,發(fā)現(xiàn)自己之前對于并發(fā)的了解只是皮毛茫蛹。這里總結(jié)以下Java并發(fā)需要掌握的點猖吴。

使用并發(fā)的一個重要原因是提高執(zhí)行效率憔恳。由于I/O等情況阻塞瓤荔,單個任務并不能充分利用CPU時間。所以在單處理器的機器上也應該使用并發(fā)钥组。
為了實現(xiàn)并發(fā)输硝,操作系統(tǒng)層面提供了多進程。但是進程的數(shù)量和開銷都有限制程梦,并且多個進程之間的數(shù)據(jù)共享比較麻煩点把。另一種比較輕量的并發(fā)實現(xiàn)是使用線程,一個進程可以包含多個線程屿附。線程在進程中沒有數(shù)量限制, 數(shù)據(jù)共享相對簡單郎逃。線程的支持跟語言是有關(guān)系的。Java 語言中支持多線程拿撩。

Java 中的多線程是搶占式的衣厘。這意味著一個任務隨時可能中斷并切換到其它任務。所以我們需要在代碼中足夠的謹慎压恒,防范好這種切換帶來的副作用影暴。

基礎

Runnable 它可以理解成一個任務。它的run()方法就是任務的邏輯探赫,執(zhí)行順序型宙。

Thread 它是一個任務的載體,虛擬機通過它來分配任務執(zhí)行的時間片伦吠。
Thread中的start方法可以作為一個并發(fā)任務的入口妆兑。不通過start方法來執(zhí)行任務,那么run方法就只是一個普通的方法

線程的狀態(tài)有四種:

  1. NEW 線程創(chuàng)建的時候短暫的處于這種狀態(tài)毛仪。這種狀態(tài)下已經(jīng)可以獲得CPU時間了搁嗓,隨后可能進入RUNNABLE,BLOCKED狀態(tài)箱靴。
  2. RUNNABLE 此狀態(tài)下只要CPU將時間分配給線程腺逛,線程中的任務就可以執(zhí)行。隨后可能進入BLOCKED衡怀,DEAD狀態(tài)棍矛。
  3. BLOCKED 線程可以運行安疗,但是有某個條件阻止著它。當線程處于阻塞狀態(tài)時够委,CPU不會分配時間片給它荐类,直到它重新進入RUNNABLE狀態(tài)。
  4. DEAD 此狀態(tài)的線程將永遠不會獲得CPU時間片茁帽。通常是因為run()方法返回才會到達此狀態(tài)玉罐。此時任務還是可以被中斷的。

Callable<T> 它是一個帶返回的異步任務脐雪,返回的結(jié)果放到一個Future對象中厌小。

Future<T> 它可以接受Callable任務的返回結(jié)果。在任務沒有返回的時候調(diào)用get方法會阻塞當前線程战秋。cancel方法會嘗試取消未完成的任務(未執(zhí)行->直接不執(zhí)行璧亚,已經(jīng)完成->返回false,正在執(zhí)行->嘗試中斷)。

FutureTask<T> 同時繼承了Runnable, Callable 接口脂信。

Java 1.5之后癣蟋,不再推薦直接使用Thread對象作為任務的入口。推薦使用Executor管理Thread對象狰闪。Executor是線程與任務之間的的一個中間層疯搅,它屏蔽了線程的生命周期,不再需要顯式的管理線程埋泵。并且ThreadPoolExecutor 實現(xiàn)了此接口幔欧,我們可以通過它來利用線程池的優(yōu)點。

線程池涉及到的類有:Executor, ExecutorService, ThreadExecutorPool, Executors, FixedThreadPool, CachedThreadPool, SingleThreadPool丽声。

Executor 只有一個方法礁蔗,execute來提交一個任務

ExecutorService 提供了管理異步任務的方法,也可以產(chǎn)生一個Future對象來跟蹤一個異步任務雁社。

主要的方法如下:

  • submit 可以提交一個任務
  • shutdown 可以拒絕接受新任務
  • shutdownNow 可以拒絕新任務并向正在執(zhí)行的任務發(fā)出中斷信號
  • invokeXXX 批量執(zhí)行任務

ThreadPoolExecutor 線程池的具體實現(xiàn)類浴井。線程池的好處在于提高效率,能避免頻繁申請/回收線程帶來的開銷霉撵。

它的使用方法復雜一些磺浙,構(gòu)造線程池的可選參數(shù)有:

  1. corePoolSize : int 工作的Worker的數(shù)量。
  2. maximumPoolSize : int 線程池中持有的Worker的最大數(shù)量
  3. keepAliveTime : long 當超過Workder的數(shù)量corePoolSize的時候徒坡,如果沒有新的任務提交撕氧,超過corePoolSize的Worker的最長等待時間。超過這個時間之后喇完,一部分Worker將被回收呵曹。
  4. unit : TimeUnit keepAliveTime的單位
  5. workQueue : BlockingQueue 緩存任務的隊列, 這個隊列只緩存提交的Runnable任務。
  6. threadFactory : ThreadFactory 產(chǎn)生線程的“工廠”
  7. handler : RejectedExecutionHandler 當一個任務被提交的時候何暮,如果所有Worker都在工作并且超過了緩存隊列的容量的時候奄喂。會交給這個Handler處理。Java 中提供了幾種默認的實現(xiàn)海洼,AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy, DiscardPolicy跨新。

這里的Worker可以理解為一個線程。

這里之前想不通坏逢,覺得線程不可能重新利用綁定新任務域帐。看了下源碼發(fā)現(xiàn)原來確實不是重新綁定任務是整。每一個Worker的核心部分只是一個循環(huán)肖揣,不斷從緩存隊列中取任務執(zhí)行。這樣達到了重用的效果浮入。

final void runWorker(Worker w) {
    Runnable task = w.firstTask;
    // ...
    try {
        while(task != null || (task=getTask())!=null) {
            try{
                task.run();
            } catch(Exception e){
            }
            // ...
        }
    } finally {
        // ...
    }
    // ...
}

Executors類提供了幾種默認線程池的實現(xiàn)方式龙优。

  1. CachedThreadExecutor 工作線程的數(shù)量沒有上限(Integer的最大值), 有需要就創(chuàng)建新線程。
  2. FixedThreadExecutor 預先一次分配固定數(shù)量的線程事秀,之后不再需要創(chuàng)建新線程彤断。
  3. SingleThreadExecutor 只有一個線程的線程池。如果提交了多個任務易迹,那么這些人物將排隊宰衙,每個任務都在上一個人物執(zhí)行完之后執(zhí)行。所有任務都是按照它們的提交順序執(zhí)行的睹欲。

sleep(long) 當前線程 中止 一段時間供炼。它不會釋放鎖。Java1.5之后提供了更加靈活的版本窘疮。

TimeUnit 可以指定睡眠的時間單位袋哼。

優(yōu)先級 絕大多數(shù)情況下我們都應該使用默認的優(yōu)先級。不同的虛擬機中對應的優(yōu)先級級別的總數(shù)考余,一般用三個就可以了 MAX_PRIORITY, NORM_PRIORITY, MIN_PRIORITY先嬉。

讓步 Thread.yield()建議相同優(yōu)先級的其它線程先運行,但是不保證一定運行其它線程楚堤。

后臺線程 一個進程中的所有非后臺線程都終止的時候整個進程也就終止疫蔓,同時殺死所有后臺線程。與優(yōu)先級沒有什么關(guān)系身冬。

join() 線程 A 持有線程T衅胀,當在線程T調(diào)用T.join()之后,A會阻塞酥筝,直到T的任務結(jié)束滚躯。可以加一個超時參數(shù),這樣在超時之后線程A可以放棄等待繼續(xù)執(zhí)行任務掸掏。

捕獲異常 不能跨線程捕獲異常茁影。比如說不能在main線程中添加try-catch塊來捕獲其它線程中拋出的異常。每一個Thread對象都可以設置一個UncaughtExceptionHandler對象來處理本線程中拋出的異常丧凤。線程池中可以通過參數(shù)ThreadFactory來為每一個線程設置一個UncaughtExceptionHandler對象募闲。

訪問共享資源

在處理并發(fā)的時候,將變量設置為private非常的重要愿待,這可以防止其它線程直接訪問變量浩螺。

synchronized 修飾方法在不加參數(shù)情況下,使用對象本身作為鎖仍侥。靜態(tài)方法使用Class對象作為鎖要出。同一個任務可以多次獲得對象鎖。

顯式鎖 Lock农渊,相比synchronized更加靈活患蹂。但是需要的代碼更多,編寫出錯的可能性也更高腿时。只有在解決特殊問題或者提高效率的時候才用它况脆。

原子性 原子操作就是永遠不會被線程切換中斷的操作。很多看似原子的操作都是非原子的批糟,比如說long,double是由兩個byte表示的格了,它們的所有操作都是非原子的。所以徽鼎,涉及到并發(fā)異常的地方都加上同步吧盛末。除非你對虛擬機十分的了解。

volatile 這個關(guān)鍵字的作用在于防止多線程環(huán)境下讀取變量的臟數(shù)據(jù)否淤。這個關(guān)鍵字在c語言中也有悄但,作用是相同的。

原子類 AtomicXXX類石抡,它們能夠保證對數(shù)據(jù)的操作是滿足原子性的檐嚣。這些類可以用來優(yōu)化多線程的執(zhí)行效率,減少鎖的使用啰扛。然而嚎京,使用難度還是比較高的。

臨界區(qū) synchronized關(guān)鍵字的用法隐解。不是修飾整個方法鞍帝,而是修飾一個代碼塊。它的作用在于盡量利用并發(fā)的效率煞茫,減少同步控制的區(qū)域帕涌。

ThreadLocal 這個概念與同步的概念不同摄凡。它是給每一個線程都創(chuàng)建一個變量的副本,并保持副本之間相互獨立蚓曼,互不干擾亲澡。所以各個線程操作自己的副本,不會產(chǎn)生沖突辟躏。

終結(jié)任務

這里我講一下自己當前的理解谷扣。

一個線程不是可以隨便中斷的。即使我們給線程設置了中斷狀態(tài)捎琐,它也還是可以獲得CPU時間片的。只有因為sleep()方法而阻塞的線程可以立即收到InterruptedException異常裹匙,所以在sleep中斷任務的情況下可以直接使用try-catch跳出任務瑞凑。其它情況下,均需要通過判斷線程狀態(tài)來判斷是否需要跳出任務(Thread.interrupted()方法)概页。

synchronized方法修飾的代碼不會在收到中斷信號后立即中斷籽御。ReentrantLock鎖控制的同步代碼可以通過InterruptException中斷。

Thread.interrupted方法調(diào)用一次之后會立即清空中斷狀態(tài)惰匙〖继停可以自己用變量保存狀態(tài)。

線程協(xié)作

wait/notifyAll wait/notifyAll是Object類中的方法项鬼。調(diào)用wait/notifyAll方法的對象是互斥對象哑梳。因為Java中所有的Object都可以做互斥量(synchronized關(guān)鍵字的參數(shù)),所以wait/notify方法是在Object類中的。

wait與sleep 不同在于sleep方法是Thread類中的方法绘盟,調(diào)用它的時候不會釋放鎖鸠真;wait方法是Object類中的方法,調(diào)用它的時候會釋放鎖龄毡。

調(diào)用wait方法之前吠卷,當前線程必須持有這段邏輯的鎖。否則會拋出異常沦零,不能繼續(xù)執(zhí)行祭隔。

wait方法可以將當前線程放入等待集合中,并釋放當前線程持有的鎖路操。此后疾渴,該線程不會接收到CPU的調(diào)度,并進入休眠狀態(tài)寻拂。有四種情況肯能打破這種狀態(tài):

  1. 有其它線程在此互斥對象上調(diào)用了notify方法程奠,并且剛好選中了這個線程被喚醒;
  2. 有其它線程在此互斥對象上調(diào)用了notifyAll方法祭钉;
  3. 其它線程向此線程發(fā)出了中斷信號瞄沙;
  4. 等待時間超過了參數(shù)設置的時間。

線程一旦被喚醒之后,它會像正常線程一樣等待之前持有的所有鎖距境。直到恢復到wait方法調(diào)用之前的狀態(tài)申尼。

還有一種不常見的情況,spurious wakeup(虛假喚醒)垫桂。就是在沒有notify师幕,notifyAll,interrupt的時候線程自動醒來诬滩。查了一些資料并沒有弄清楚是為什么霹粥。不過為了防止這種現(xiàn)象,我們要在wait的條件上加一層循環(huán)疼鸟。

當一個線程調(diào)用wait方法之后后控,其它線程調(diào)用該線程的interrupt方法空镜。該線程會喚醒浩淘,并嘗試恢復之前的狀態(tài)。當狀態(tài)恢復之后吴攒,該線程會拋出一個異常张抄。

notify 喚醒一個等待此對象的線程。
notifyAll 喚醒所有等待此對象的線程洼怔。

錯失的信號

當兩個線程使用notify/wait或者notifyAll/wait進行協(xié)作的時候署惯,不恰當?shù)氖褂盟鼈兛赡軙е乱恍┬盘杹G失。例子:

T1:
synchronized(shareMonitor){
    // set up condition for T2
    shareMonitor.notify();
}

T2:
while(someCondition){
    // Point 1
    synchronized(shareMonitor){
        shareMonitor.wait();
    }
}

信號丟失是這樣發(fā)生的:

當T2執(zhí)行到Point1的時候茴厉,線程調(diào)度器將工作線程從T2切換到T1泽台。T1完成T2條件的設置工作之后,線程調(diào)度器將工作線程從T1切換回T2矾缓。雖然T2線程等待的條件已經(jīng)滿足怀酷,但還是會被掛起。

解決的方法比較簡單:

T2:
synchronized(sharedMonitor) {
    while(someCondition) {
        sharedMonitor.wait();
    }
}

將競爭條件放到while循環(huán)的外面即可嗜闻。在進入while循環(huán)之后蜕依,在沒有調(diào)用wait方法釋放鎖之前,將不會進入到T1線程造成信號丟失琉雳。

notify & notifyAll 前面已經(jīng)提過這兩個方法的區(qū)別样眠。notify是隨機喚醒一個等待此鎖的線程,notifyAll是喚醒所有等待此鎖的線程翠肘。

Condition 他是concurrent類庫中顯式的掛起/喚醒任務的工具檐束。它是真正的鎖(Lock)對象產(chǎn)生的一個對象。其實用法跟wait/notify是一致的束倍。await掛起任務被丧,signalAll()喚醒任務盟戏。

生產(chǎn)者消費者隊列 Java中提供了一種非常簡便的容器,BlockingQueue甥桂。已經(jīng)幫你寫好了阻塞式的隊列柿究。

除了BlockingQueue,使用PipedWriter/PipedReader也可以方便的在線程之間傳遞數(shù)據(jù)黄选。

死鎖

死鎖有四個必要條件蝇摸,打破一個即可去除死鎖。

四個必要條件:

  1. 互斥條件办陷。 互斥條件:一個資源每次只能被一個進程使用貌夕。
  2. 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放懂诗。
  3. 不剝奪條件:線程已獲得的資源蜂嗽,在末使用完之前,不能強行剝奪殃恒。
  4. 循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

本來自己翻譯辱揭,但發(fā)現(xiàn)百度上描述的更好一些离唐,直接copy到這里來,并把進程換成了線程问窃。

其它工具

CountDownLatch 同步多個任務亥鬓,強制等待其它任務完成。它有兩個重要方法countDown,await以及構(gòu)造時傳入的參數(shù)SIZE域庇。當一個線程調(diào)用await方法的時候會掛起嵌戈,直到該對象收到SIZE次countDown。一個對象只能使用一次听皿。

CyclicBarrier 也是有一個SIZE參數(shù)熟呛。當有SIZE個線程調(diào)用await的時候,全部線程都會被喚醒尉姨♀殖可以理解為所有運動員就位后才能起跑,早就位的運動員只能掛起等待又厉。它可以重復利用九府。

DelayQueue 一個無界的BlockingQueue,用來放置實現(xiàn)了Delay接口的對象覆致,在隊列中的對象只有在到期之后才能被取走侄旬。如果沒有任何對象到期,就沒有頭元素煌妈。

PriorityBlockingQueue 一種自帶優(yōu)先級的阻塞式隊列儡羔。

ScheduledExecutor 可以把它想象成一種線程池式的Timer, TimerTask宣羊。

Semaphore 互斥鎖只允許一個線程訪問資源,但是Semaphore允許SIZE個線程同時訪問資源笔链。

Exchanger 生產(chǎn)者消費者問題的特殊版段只。兩個線程可以在都‘準備好了’之后交換一個對象的控制權(quán)。

ReadWriteLock 讀寫鎖鉴扫。 讀-讀不互斥赞枕,讀-寫互斥,寫-寫互斥坪创。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炕婶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子莱预,更是在濱河造成了極大的恐慌柠掂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件依沮,死亡現(xiàn)場離奇詭異涯贞,居然都是意外死亡,警方通過查閱死者的電腦和手機危喉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門宋渔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辜限,你說我怎么就攤上這事皇拣。” “怎么了薄嫡?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵氧急,是天一觀的道長。 經(jīng)常有香客問我毫深,道長吩坝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任费什,我火速辦了婚禮钾恢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸳址。我一直安慰自己瘩蚪,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布稿黍。 她就那樣靜靜地躺著疹瘦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪巡球。 梳的紋絲不亂的頭發(fā)上言沐,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天邓嘹,我揣著相機與錄音,去河邊找鬼险胰。 笑死汹押,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的起便。 我是一名探鬼主播棚贾,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼榆综!你這毒婦竟也來了妙痹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鼻疮,失蹤者是張志新(化名)和其女友劉穎怯伊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體判沟,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡耿芹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挪哄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猩系。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖中燥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塘偎,我是刑警寧澤疗涉,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站跷叉,受9級特大地震影響迎捺,放射性物質(zhì)發(fā)生泄漏堵腹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一闹伪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧壮池,春花似錦偏瓤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至橙依,卻和暖如春证舟,著一層夾襖步出監(jiān)牢的瞬間硕旗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工女责, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留漆枚,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓抵知,卻偏偏與公主長得像墙基,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辛藻,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容