理解Java多線程之并發(fā)編程
今天深度學(xué)習(xí)一下《Java并發(fā)編程的藝術(shù)》的第1章并發(fā)編程的挑戰(zhàn),深入理解Java多線程,看看多線程中的坑。
注意,哈肯的程序員讀書筆記并不是抄書,而是將書中精華內(nèi)容和哈肯的開發(fā)經(jīng)驗(yàn)相結(jié)合,給你系統(tǒng)地講述我對(duì)相關(guān)知識(shí)理解。如果你發(fā)現(xiàn)哈肯講的內(nèi)容跟書中內(nèi)容差異很大也不足為奇。
1 多線程的使用場(chǎng)景
在實(shí)際的商業(yè)系統(tǒng)中,為了提升程序的性能,我們經(jīng)常會(huì)使用到多線程。java多線程也是后臺(tái)開發(fā)崗、Android開發(fā)崗招聘面試和筆試時(shí)的熱門問題。至少,到目前為止我面試過的200多位開發(fā)同學(xué)時(shí),以及我曾經(jīng)的幾次求職被面試時(shí),多半都會(huì)問java多線程并發(fā)相關(guān)的問題。
2 多線程的缺點(diǎn)
多線程能充分利用多核CPU的特性,但并不意味著多線程就一定比單線程更快,并發(fā)編程也存在許多限制和挑戰(zhàn),例如多線程間的上下文切換會(huì)有開銷、多線程中的數(shù)據(jù)一致性問題、線程死鎖問題、系統(tǒng)資源限制。
2.1 上下文切換的開銷
(1)上下文切換的開銷
CPU使用時(shí)間片算法,將處理時(shí)間輪著分配給不同的線程,所以即使是單核CPU也支持多線程,這個(gè)時(shí)間片非常短,一般是幾十毫秒(ms),CPU不停的切換線程執(zhí)行,讓我們感覺多個(gè)線程是同時(shí)執(zhí)行的。CPU在切換線程前會(huì)保存上一個(gè)任務(wù)的狀態(tài),以便下次切換回這個(gè)線程時(shí),可以正常繼續(xù)執(zhí)行。我們把線程的狀態(tài)保存到再加載的過程稱為一次上下文切換。
《Java并發(fā)編程的藝術(shù)》作者寫了一段程序,讓2個(gè)線程同時(shí)不斷地做變量自增操作,結(jié)果證明用2個(gè)線程并行甚至比單線程串行更慢一些。通過使用Lmbench3測(cè)量上下文切換的時(shí)長(zhǎng),發(fā)現(xiàn)上述多線程代碼中每秒高達(dá)1000多次的線程上下文切換。因此不是多線程就一定更快,還要看在線程中干了什么。
(2)如何減少上下文切換
減少上下文切換的方法有:無鎖并發(fā)編程、CAS算法、減少不必要的線程、使用協(xié)程。
- 無鎖并發(fā)編程。多線程競(jìng)爭(zhēng)鎖時(shí),會(huì)引起上下文切換,通過避免使用鎖,可以減少線程的切換,例如將數(shù)據(jù)用ID分段,不同的線程處理不同段的數(shù)據(jù)。
- CAS算法。Java的Atomic包使用CAS算法來更新數(shù)據(jù),而不需要加鎖。
- 減少不必要的線程。當(dāng)任務(wù)很少時(shí),盡量減少不必要的線程,避免造成大量線程都處于等待狀態(tài)。
- 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換。
2.2 多線程中的數(shù)據(jù)一致性問題
(1)線程中訪問外部數(shù)據(jù)的過程
每個(gè)線程都有自己的棧,保存線程中創(chuàng)建的局部變量,如果線程中使用到外部的變量,則線程通常會(huì)把改外部變量復(fù)制一份到線程棧中,當(dāng)修改完后,再將數(shù)據(jù)同步回外部。
(2)線程內(nèi)操作的原子性問題
一個(gè)操作會(huì)由多個(gè)cpu指令構(gòu)造。例如,創(chuàng)建對(duì)象操作大致分為幾步:為對(duì)象分配內(nèi)存、成員變量的值初始化、調(diào)用構(gòu)造方法、返回對(duì)象的引用;又例如,線程中對(duì)一個(gè)外部變量的賦值(修改)操作大致分為幾步:在當(dāng)前線程棧創(chuàng)建變量副本,修改變量值,將變量值的修改同步回外部,由于CPU的時(shí)間片機(jī)制,每個(gè)線程獲得時(shí)間片后,能執(zhí)行的指令數(shù)量是有限的,可能一個(gè)操作還未完成,而時(shí)間片到了,需要保存上下文并切換到下一個(gè)線程。因此,無法保證操作的原子性。
(3)共享數(shù)據(jù)的可見性問題
觀察上述的原子性問題中的例子,線程中對(duì)一個(gè)外部變量的賦值,可能線程A中剛創(chuàng)建了外部變量的副本,而線程B已經(jīng)對(duì)該外部變量進(jìn)行了修改,但線程A中是不知道的。即,一個(gè)線程對(duì)共享數(shù)據(jù)的修改,不能立刻被其他線程所看見,這就引起了數(shù)據(jù)一致性的問題。
(4)有序性問題
CPU單個(gè)核中,包含多個(gè)ALU單元(Arithmetic Logic Unit,即算術(shù)邏輯單元),用來執(zhí)行算數(shù)運(yùn)算和邏輯運(yùn)算。目前Intel的Haswell架構(gòu)的CPU(第四代酷睿處理器開始)有4個(gè)ALU單元。所以單核CPU在同一時(shí)刻是同時(shí)執(zhí)行多個(gè)指令的。CPU會(huì)把多條指令進(jìn)行重排序,把沒有依賴關(guān)系的指令同時(shí)放到各ALU中執(zhí)行。例如3個(gè)操作按如下順序?qū)懘a a=1; b=2; c=a+b; 由于a=1和b=2不存在依賴關(guān)系,2個(gè)指令可能會(huì)被重排序,而c=a+b存在依賴關(guān)系,這個(gè)指令一定會(huì)在前2個(gè)指令執(zhí)行完后再執(zhí)行,最終一定是c=3。請(qǐng)看如下代碼:
public class VolatileTest { private int a = 1; private boolean status = false; public void setStatus() { a = 2; status = true; } public void test() { if (status) { int b = a + 1; System.out.print(b); } } }
(5)如何解決多線程的數(shù)據(jù)一致性問題
由于以上4點(diǎn),引發(fā)了多線程中數(shù)據(jù)的一致性問題。解決辦法主要有2個(gè):
- 使用volatile關(guān)鍵字。相對(duì)synchronized來說,volatile是一種輕量級(jí)的同步機(jī)制,有2個(gè)作用:一是保證共享變量對(duì)所有線程的可見性,即本地副本修改后會(huì)強(qiáng)制刷新回外部;二是禁止指令重排序優(yōu)化。但要注意volatile只對(duì)單個(gè)變量讀/寫操作具有原子性,對(duì)i++這種復(fù)合操作(取值、加1、賦值)是無法保證其原子性的。要保證多線程中i++這種復(fù)合操作的原子性,可以改用CAS的實(shí)現(xiàn)類,例如用AtomicInteger代替int。
- 加鎖。通過鎖來實(shí)現(xiàn)同一時(shí)間只有1個(gè)線程可以訪問共享變量。
2.3 線程死鎖問題
死鎖產(chǎn)生需要同時(shí)滿足4個(gè)條件:
- 資源的互斥使用,即當(dāng)資源被一個(gè)線程占有時(shí),別的線程不能用。
- 不可搶占,即資源請(qǐng)的求者不能從使用者手上強(qiáng)制奪取資源,只能等待對(duì)方釋放。
- 請(qǐng)求和保持,即在請(qǐng)求其他資源的同時(shí)還保持對(duì)現(xiàn)有資源的占有。
- 循環(huán)等待,例如線程1持有資源A,請(qǐng)求資源B,而線程2持有資源B,請(qǐng)求資源A。
下面的代碼演示對(duì)資源的請(qǐng)求和保持,持有res1不釋放,同時(shí)對(duì)res2進(jìn)行請(qǐng)求:
synchronized (res1) { ... // 執(zhí)行一些操作 synchronized (res2) { ... } }
解決死鎖的方法
只要讓產(chǎn)生死鎖的4個(gè)條件中任何一個(gè)不成立,就可以避免死鎖。具體做法通常有:
- 使用Lock接口的tryLock()。
- 避免一個(gè)線程中同時(shí)獲得多個(gè)鎖。例如上面代碼所示,同時(shí)獲得res1和res2的鎖才能完成執(zhí)行。
- 避免一個(gè)鎖中占用多個(gè)資源,即要縮小鎖的范圍。
Lock接口包含4個(gè)給對(duì)象加鎖的方法,如下所示:
public interface Lock { /**阻塞,直到另外一個(gè)線程釋放鎖*/ void lock(); /**以可被中斷的方式獲取鎖。調(diào)用正在等待獲得鎖的線程的interrupt()方法可中斷*/ void lockInterruptibly() throws InterruptedException; /**非阻塞,嘗試的獲取鎖,如果獲取到則返回true,否則返回false*/ boolean tryLock(); /**阻塞,試圖獲取鎖,最多阻塞等待指定時(shí)間,如果獲取到則返回true,否則返回false*/ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); ... }
Lock lock = new ReentrantLock(); if(lock.tryLock()) { //成功獲得鎖 try{ ... } catch(Exception ex){ } finally{ lock.unlock(); //釋放鎖 } } else { //不能獲取鎖 ... }
class Account { private int money; synchronized boolean transfer(Account target, int money){ if (this.money > money) { this.money -= money; target.money += money; return true; } return false; } }
class Account { private int money; boolean transfer(Account target, int money){ synchronized(Account.class) { if (this.money > money) { this.money -= money; target.money += money; return true; } } return false; } }
2.4 系統(tǒng)資源限制
系統(tǒng)的資源總是有限的,無節(jié)制地創(chuàng)建大量的線程,只會(huì)造成大量的線程上下文切換的開銷,并不能實(shí)際提高程序的效率。按照大家的常規(guī)經(jīng)驗(yàn),如果是IO密集型,則線程池的核心線程數(shù)宜為2N+1;如果是
CPU密集型,則線程池核心線程數(shù)宜為N+1;其中N為CPU核數(shù)。
到此這篇關(guān)于理解Java多線程之并發(fā)編程的文章就介紹到這了,更多相關(guān)Java并發(fā)編程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)幸運(yùn)抽獎(jiǎng)系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)幸運(yùn)抽獎(jiǎng)系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07深入解析Jdk8中Stream流的使用讓你脫離for循環(huán)
這篇文章主要介紹了Jdk8中Stream流的使用,讓你脫離for循環(huán),本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Java中的static靜態(tài)變量、靜態(tài)方法超詳細(xì)講解
Java中的static關(guān)鍵字用于表示靜態(tài)變量和靜態(tài)方法,靜態(tài)變量是類的成員變量,它們屬于類本身,而不是類的實(shí)例,這篇文章主要給大家介紹了關(guān)于Java中static靜態(tài)變量、靜態(tài)方法詳細(xì)講解的相關(guān)資料,需要的朋友可以參考下2024-06-06MapStruct實(shí)體轉(zhuǎn)換及List轉(zhuǎn)換的方法講解
今天小編就為大家分享一篇關(guān)于MapStruct實(shí)體轉(zhuǎn)換及List轉(zhuǎn)換的方法講解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03Spring中@Import的各種用法以及ImportAware接口詳解
這篇文章主要介紹了Spring中@Import的各種用法以及ImportAware接口詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10java并發(fā)編程專題(八)----(JUC)實(shí)例講解CountDownLatch
這篇文章主要介紹了java CountDownLatch的相關(guān)資料,文中示例代碼非常詳細(xì),幫助大家理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07apollo與springboot集成實(shí)現(xiàn)動(dòng)態(tài)刷新配置的教程詳解
這篇文章主要介紹了apollo與springboot集成實(shí)現(xiàn)動(dòng)態(tài)刷新配置,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06使用lombok@Data啟動(dòng)項(xiàng)目報(bào)錯(cuò)問題及解決
在使用Lombok時(shí),可能會(huì)遇到實(shí)體類中的@Data注解不生效,導(dǎo)致get方法找不到的問題,解決這一問題通常需要三個(gè)步驟:首先,檢查項(xiàng)目設(shè)置中編譯規(guī)則是否勾選;其次,確認(rèn)IDE中是否安裝了Lombok插件2024-10-10new出來的對(duì)象中無法使用@autowired進(jìn)行對(duì)象bean注入問題
這篇文章主要介紹了基于new出來的對(duì)象中無法使用@autowired進(jìn)行對(duì)象bean注入問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02