本篇內(nèi)容主要講解“為什么要學(xué)習(xí)Java并發(fā)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“為什么要學(xué)習(xí)Java并發(fā)”吧!
因?yàn)榕驼嬲\(chéng),有更多的客戶和我們聚集在一起,為了共同目標(biāo),成都創(chuàng)新互聯(lián)在工作上密切配合,從創(chuàng)業(yè)型企業(yè)到如今不斷成長(zhǎng),要感謝客戶對(duì)我們的高要求,讓我們敢于面對(duì)挑戰(zhàn),才有今天的進(jìn)步與發(fā)展。從網(wǎng)站到重慶小程序開發(fā),軟件開發(fā),成都App定制開發(fā),十余年企業(yè)網(wǎng)站建設(shè)服務(wù)經(jīng)驗(yàn),為企業(yè)提供網(wǎng)站設(shè)計(jì),網(wǎng)站托管運(yùn)營(yíng)一條龍服務(wù).為企業(yè)提供營(yíng)銷型網(wǎng)站,專業(yè)公司,原創(chuàng)設(shè)計(jì),十余年品質(zhì),值得您的信賴.
01
初識(shí)并發(fā)
什么是并發(fā),什么是并行?
用個(gè)JVM的例子來講解,在垃圾回收器做并發(fā)標(biāo)記的時(shí)候,這個(gè)時(shí)候JVM不僅可以做垃圾標(biāo)記,還可以處理程序的一些需求,這個(gè)叫并發(fā)。在做垃圾回收時(shí),JVM多個(gè)線程同時(shí)做回收,這叫并行。
02
為什么要學(xué)習(xí)并發(fā)編程
直觀原因
1)JD的強(qiáng)制性要求
隨著互聯(lián)網(wǎng)行業(yè)的飛速發(fā)展,并發(fā)編程已經(jīng)成為非常熱門的領(lǐng)域,也是各大企業(yè)服務(wù)端崗位招聘的必備技能。
2)從小牛通往大牛的必經(jīng)之路
架構(gòu)師是軟件開發(fā)團(tuán)隊(duì)中非常重要的角色,成為一名架構(gòu)師是許多搞技術(shù)人奮斗的目標(biāo),衡量一個(gè)架構(gòu)師的能力指標(biāo)就是設(shè)計(jì)出一套解決高并發(fā)的系統(tǒng),由此可見高并發(fā)技術(shù)的重要性,而并發(fā)編程是底層的基礎(chǔ)。無論游戲還是互聯(lián)網(wǎng)行業(yè),無論軟件開發(fā)還是大型網(wǎng)站,都對(duì)高并發(fā)技術(shù)人才存在巨大需求,因此,為了工作為了提升自己,學(xué)習(xí)高并發(fā)技術(shù)刻不容緩。
3)面試過程中極容易踩坑
面試的時(shí)候?yàn)榱丝疾鞂?duì)并發(fā)編程的掌握情況,經(jīng)常會(huì)考察并發(fā)安全相關(guān)的知識(shí)和線程交互的知識(shí)。例如在并發(fā)情況下如何實(shí)現(xiàn)一個(gè)線程安全的單例模式,如何完成兩個(gè)線程中的功能交互執(zhí)行。
以下是使用雙檢索實(shí)現(xiàn)一個(gè)線程安全的單例懶漢模式,當(dāng)然也可以使用枚舉或者單例餓漢模式。
private static volatile Singleton singleton; private Singleton(){}; public Singleton getSingleton(){ if(null == singleton){ synchronized(Singleton.class){ if(null == singleton){ singleton = new Singleton(); } } } return singleton; }
在這里第一層空判斷是為了減少鎖控制的粒度,使用volatile修飾是因?yàn)樵趈vm中new Singleton()會(huì)出現(xiàn)指令重排,volatile避免happens before,避免空指針的問題。從一個(gè)線程安全的單例模式可以引申出很多,volatile和synchronized的實(shí)現(xiàn)原理,JMM模型,MESI協(xié)議,指令重排,關(guān)于JMM模型后序會(huì)給出更詳細(xì)的圖解。
除了線程安全問題,還會(huì)考察線程間的交互。 例如使用兩個(gè)線程交替打印出A1B2C3…Z26
考察的重點(diǎn)并不是要簡(jiǎn)單的實(shí)現(xiàn)這個(gè)功能,通過此面試題,可以考察知識(shí)的整體掌握情況,多種方案實(shí)現(xiàn),可以使用Atomicinteger、ReentrantLock、CountDownLat ch。下圖是使用LockSupport控制兩個(gè)線程交替打印的示例,LockSupport內(nèi)部實(shí)現(xiàn)的原理是使用UNSAFE控制一個(gè)信號(hào)量在0和1之間變動(dòng),從而可以控制兩個(gè)線程的交替打印。
4)并發(fā)在我們工作使用的框架中處處可見,tom cat,netty,jvm,Disruptor
熟悉JAVA并發(fā)編程基礎(chǔ)是掌握這些框架底層知識(shí)的基石,這里簡(jiǎn)單介紹下高并發(fā)框架Disruptor的底層實(shí)現(xiàn)原理,做一個(gè)勾勒的作用:
Martin Fowler在一篇LMAX文章中介紹,這一個(gè)高性能異步處理框架,其單線程一秒的吞吐量可達(dá)六百萬
Disruptor核心概念
Disruptor特征
基于事件驅(qū)動(dòng)
基于"觀察者"模式、"生產(chǎn)者-消費(fèi)者"模型
可以在無鎖的情況下實(shí)現(xiàn)網(wǎng)絡(luò)的隊(duì)列操作
RingBuffer執(zhí)行流程
Disruptor底層組件,RingBuffer密切相關(guān)的對(duì)象:Sequ enceBarrier和Sequencer;
SequenceBarrier是消費(fèi)者和RingBuffer之間的橋梁。在Disruptor中,消費(fèi)者直接訪問的是SequenceBarrier,由SequenceBarrier減少RingBuffer的隊(duì)列沖突。
SequenceBarrier 通過waitFor方法當(dāng)消費(fèi)者速度大于生產(chǎn)者的生產(chǎn)速度時(shí),消費(fèi)者可通過waitFor方法給予生產(chǎn)者一定的緩沖時(shí)間,協(xié)調(diào)生產(chǎn)者和消費(fèi)者的速度問題,waitFor執(zhí)行時(shí)機(jī):
Sequencer是生產(chǎn)者和緩沖區(qū)RingBuffer之間的橋梁,生產(chǎn)者通過Sequencer向RingBuffer申請(qǐng)數(shù)據(jù)存放空間,通過WaitStrategy使用publish方法通知消費(fèi)者,WaitStrategy是消費(fèi)者沒有數(shù)據(jù)可以消費(fèi)時(shí)的等待策略。每個(gè)生產(chǎn)者或者消費(fèi)者線程,會(huì)先申請(qǐng)可以操作的元素在數(shù)組中的位置,申請(qǐng)到之后,直接在該位置寫入或者讀取數(shù)據(jù),整個(gè)過程通過原子變量CAS,保證操作的線程安全,這就是Disruptor的無鎖設(shè)計(jì)。
以下是五大常用等待策略:
BlockingWaitStrategy:Disruptor的默認(rèn)策略是BlockingWaitStrategy。在BlockingWaitStrategy內(nèi)部是使用鎖和condition來控制線程的喚醒。BlockingWaitStrategy是最低效的策略,但其對(duì)CPU的消耗最小并且在各種不同部署環(huán)境中能提供更加一致的性能表現(xiàn)。
SleepingWaitStrategy:SleepingWaitStrategy 的性能表現(xiàn)跟 BlockingWaitStrategy 差不多,對(duì) CPU 的消耗也類似,但其對(duì)生產(chǎn)者線程的影響最小,通過使用LockSupport.parkNanos(1)來實(shí)現(xiàn)循環(huán)等待。
YieldingWaitStrategy:YieldingWaitStrategy是可以使用在低延遲系統(tǒng)的策略之一。YieldingWaitStrategy將自旋以等待序列增加到適當(dāng)?shù)闹怠T谘h(huán)體內(nèi),將調(diào)用Thread.yield()以允許其他排隊(duì)的線程運(yùn)行。在要求極高性能且事件處理線數(shù)小于 CPU 邏輯核心數(shù)的場(chǎng)景中,推薦使用此策略;例如,CPU開啟超線程的特性。
BusySpinWaitStrategy:性能最好,適合用于低延遲的系統(tǒng)。在要求極高性能且事件處理線程數(shù)小于CPU邏輯核心數(shù)的場(chǎng)景中,推薦使用此策略;例如,CPU開啟超線程的特性。
目前,包括Apache Storm、Camel、Log4j2在內(nèi)的很多知名項(xiàng)目都應(yīng)用了Disruptor以獲取高性能。
5)JUC是并發(fā)大神Doug Lea靈魂力作,堪稱典范(第一個(gè)主流嘗試,它將線程,鎖和事件之外的抽象層次提升到更平易近人的方式:并發(fā)集合, fork/join 等等)
通過并發(fā)編程設(shè)計(jì)思維的學(xué)習(xí),發(fā)揮使用多線程的優(yōu)勢(shì)
發(fā)揮多處理器的強(qiáng)大能力
建模的簡(jiǎn)單性
異步事件的簡(jiǎn)化處理
響應(yīng)更靈敏的用戶界面
那么學(xué)不好并發(fā)編程基礎(chǔ)會(huì)帶來什么問題呢
1)多線程在日常開發(fā)中運(yùn)用中處處都是,jvm、tomcat、netty,學(xué)好java并發(fā)編程是更深層次理解和掌握此類工具和框架的前提由于計(jì)算機(jī)的cpu運(yùn)算速度和內(nèi)存io速度有幾個(gè)數(shù)量級(jí)的差距,因此現(xiàn)代計(jì)算機(jī)都不得不加入一層盡可能接近處理器運(yùn)算速度的高速緩存來做緩沖:將內(nèi)存中運(yùn)算需要使用的數(shù)據(jù)先復(fù)制到緩存中,當(dāng)運(yùn)算結(jié)束后再同步回內(nèi)存。如下圖:
因?yàn)閖vm要實(shí)現(xiàn)跨硬件平臺(tái),因此jvm定義了自己的內(nèi)存模型,但是因?yàn)閖vm的內(nèi)存模型最終還是要映射到硬件上,因此jvm內(nèi)存模型幾乎與硬件的模型一樣:
操作系統(tǒng)底層數(shù)據(jù)結(jié)構(gòu),每個(gè)CPU對(duì)應(yīng)的高速緩存中的數(shù)據(jù)結(jié)構(gòu)是一個(gè)個(gè)bucket存儲(chǔ)的鏈表,其中tag代表的是主存中的地址,cache line是偏移量,flag對(duì)應(yīng)的MESI緩存一致性協(xié)議中的各個(gè)狀態(tài)。
MESI緩存一致性狀態(tài)分別為:
M:Modify,代表修改
E:Exclusive,代表獨(dú)占
S:Share,代表共享
I:Invalidate,代表失效
以下是一次cpu0數(shù)據(jù)寫入的流程:
在CPU0執(zhí)行一次load,read和write時(shí),在做write之前flag的狀態(tài)會(huì)是S,然后發(fā)出invalidate消息到總線;
其他cpu會(huì)監(jiān)聽總線消息,將各cpu對(duì)應(yīng)的cache entry中的flag狀態(tài)由S修改為I,并且發(fā)送invalidate ack給總線
cpu0收到所有cpu返回的invalidate ack后,cpu0將flag變?yōu)镋,執(zhí)行數(shù)據(jù)寫入,狀態(tài)修改為M,類似于一個(gè)加鎖過程
考慮到性能問題,這樣寫入修改數(shù)據(jù)的效率太過漫長(zhǎng),因此引入了寫緩沖器和無效隊(duì)列,所有的修改操作會(huì)先寫入寫緩沖器,其他cpu接收到消息后會(huì)先寫入無效隊(duì)列,并返回ack消息,之后再?gòu)臒o效隊(duì)列消費(fèi)消息,采用異步的形式。當(dāng)然,這樣就會(huì)產(chǎn)生有序性問題,例如某些entry中的flag還是S,但實(shí)際上應(yīng)該標(biāo)識(shí)為I,這樣訪問到的數(shù)據(jù)就會(huì)有問題。運(yùn)用volitale是為了解決指令重排帶來的無序性問題,volitale是jvm層面的關(guān)鍵字,MESI是cpu層面的,兩者是差了幾個(gè)層次的。
2)性能不達(dá)標(biāo),找不到解決思路。
3)工作中可能會(huì)寫出線程不安全的方法
以下是一個(gè)多線程打印時(shí)間的逐步優(yōu)化案例
new Thread(new Runnable() { @Override public void run() { System.out.println(new ThreadLocalDemo01().date(10)); }}).start();new Thread(new Runnable() { @Override public void run() { System.out.println(new ThreadLocalDemo01().date(1007)); }}).start();
優(yōu)化1,多個(gè)線程運(yùn)用線程池復(fù)用
for(int i = 0; i < 1000; i++){ int finalI = i; executorService.submit(new Runnable() { @Override public void run() { System.out.println(new ThreadLocalDemo01().date2(finalI)); } });}executorService.shutdown();public String date2(int seconds){ Date date = new Date(1000 * seconds); String s = null;// synchronized (ThreadLocalDemo01.class){// s = simpleDateFormat.format(date);// } s = simpleDateFormat.format(date); return s;}
優(yōu)化2,線程池結(jié)合ThreadLocal
public String date2(int seconds){ Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return simpleDateFormat.format(date);}
在多線程服用一個(gè)SimpleDateFormat時(shí)會(huì)出現(xiàn)線程安全問題,執(zhí)行結(jié)果會(huì)打印出相同的時(shí)間,在優(yōu)化2中使用線程池結(jié)合ThreadLocal實(shí)現(xiàn)資源隔離,線程安全。
4)許多問題無法正確定位
踩坑:crm仿真定時(shí)任務(wù)阻塞,無法繼續(xù)執(zhí)行
問題:crm仿真運(yùn)用schedule配置的定時(shí)任務(wù)在某個(gè)時(shí)間節(jié)點(diǎn)后的所有定時(shí)任務(wù)均未執(zhí)行
原因:定時(shí)任務(wù)配置導(dǎo)致的問題,@Schedule配置的定時(shí)任務(wù)如果未配置線程池,在啟動(dòng)類使用@EnableScheduling啟用定時(shí)任務(wù)時(shí)會(huì)默認(rèn)使用單線程,后端配置了多定時(shí)任務(wù),會(huì)出現(xiàn)問題.配置了兩定時(shí)任務(wù)A和B,在A先占用資源后如果一直未釋放,B會(huì)一直處于等待狀態(tài),直到A任務(wù)釋放資源后,B開始執(zhí)行,若要避免多任務(wù)執(zhí)行帶來的問題,需要使用以下方法配置:
@Bean public ThreadPoolTaskScheduler taskScheduler(){ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(10); return scheduler; }
crm服務(wù)由于定時(shí)任務(wù)配置的不多,并且在資源足夠的情況下,任務(wù)執(zhí)行速度相對(duì)較快,并未設(shè)置定時(shí)任務(wù)的線程池
定時(shí)任務(wù)里程序方法如何造成線程一直未釋放,導(dǎo)致阻塞。
在問題定位時(shí),產(chǎn)生的問題來自CountDownLatch無法歸零,導(dǎo)致整個(gè)主線程hang在那里,無法釋放。
在api中當(dāng)調(diào)用await時(shí)候,調(diào)用線程處于等待掛起狀態(tài),直至count變成0再繼續(xù),大致原理如下:
因此將目光焦點(diǎn)轉(zhuǎn)移至await方法,使當(dāng)前線程在鎖存器倒計(jì)數(shù)至零之前一直等待,除非線程被中斷或超出了指定的等待時(shí)間。如果當(dāng)前計(jì)數(shù)為零,則此方法立刻返回true 值。如果當(dāng)前計(jì)數(shù)大于零,則出于線程調(diào)度目的,將禁用當(dāng)前線程,且在發(fā)生以下三種情況之一前,該線程將一直處于休眠狀態(tài):由于調(diào)用 countDown() 方法,計(jì)數(shù)到達(dá)零;或者其他某個(gè)線程中斷當(dāng)前線程;或者已超出指定的等待時(shí)間。
Executors.newFixedThreadPool這是個(gè)有固定活動(dòng)線程數(shù)。當(dāng)提交到池中的任務(wù)數(shù)大于固定活動(dòng)線程數(shù)時(shí),任務(wù)就會(huì)放到阻塞隊(duì)列中等待。CRM該定時(shí)任務(wù)里為了加快任務(wù)處理,運(yùn)用多線程處理,設(shè)置的CountDownLatch的count大于ThreadPoolExecutor的固定活動(dòng)線程數(shù)導(dǎo)致任務(wù)一直處于等待狀態(tài),計(jì)數(shù)無法歸零,導(dǎo)致主線程一直無法釋放,從而導(dǎo)致crm一臺(tái)仿真服務(wù)的定時(shí)任務(wù)處于癱瘓狀態(tài)。
03
如何學(xué)習(xí)java并發(fā)編程
為了學(xué)習(xí)好并發(fā)編程基礎(chǔ),我們需要有一個(gè)上帝視角,一個(gè)宏觀的概念,然后由點(diǎn)及深,掌握必備的知識(shí)點(diǎn)。我們可以從以下兩張思維導(dǎo)圖列舉出來的逐步進(jìn)行學(xué)習(xí)。
必備知識(shí)點(diǎn)
04
線程
列舉了如此多的案例都是圍繞線程展開的,所以我們需要更深地掌握線程,它的概念,它的原則,它是如何實(shí)現(xiàn)交互通信的。
以下的一張圖可以更通俗地解釋進(jìn)程、線程的區(qū)別
進(jìn)程: 一個(gè)進(jìn)程好比是一個(gè)程序,它是 資源分配的最小單位 。同一時(shí)刻執(zhí)行的進(jìn)程數(shù)不會(huì)超過核心數(shù)。不過如果問單核CPU能否運(yùn)行多進(jìn)程?答案又是肯定的。單核CPU也可以運(yùn)行多進(jìn)程,只不過不是同時(shí)的,而是極快地在進(jìn)程間來回切換實(shí)現(xiàn)的多進(jìn)程。電腦中有許多進(jìn)程需要處于「同時(shí)」開啟的狀態(tài),而利用CPU在進(jìn)程間的快速切換,可以實(shí)現(xiàn)「同時(shí)」運(yùn)行多個(gè)程序。而進(jìn)程切換則意味著需要保留進(jìn)程切換前的狀態(tài),以備切換回去的時(shí)候能夠繼續(xù)接著工作。所以進(jìn)程擁有自己的地址空間,全局變量,文件描述符,各種硬件等等資源。操作系統(tǒng)通過調(diào)度CPU去執(zhí)行進(jìn)程的記錄、回復(fù)、切換等等。
線程:線程是獨(dú)立運(yùn)行和獨(dú)立調(diào)度的基本單位(CPU上真正運(yùn)行的是線程),線程相當(dāng)于一個(gè)進(jìn)程中不同的執(zhí)行路徑。
單線程:單線程就是一個(gè)叫做“進(jìn)程”的房子里面,只住了你一個(gè)人,你可以在這個(gè)房子里面任何時(shí)間去做任何的事情。你是看電視、還是玩電腦,全都有你自己說的算。想干什么干什么,想什么時(shí)間做什么就什么時(shí)間做什么。
多線程:但是如果你處在一個(gè)“多人”的房子里面,每個(gè)房子里面都有叫做“線程”的住戶:線程1、線程2、線程3、線程4,情況就不得不發(fā)生變化了。
在多線程編程中有”鎖”的概念,在你的房子里面也有鎖。如果你的老婆在上廁所并鎖上門,她就是在獨(dú)享這個(gè)“房子(進(jìn)程)”里面的公共資源“衛(wèi)生間”,如果你的家里只有這一個(gè)衛(wèi)生間,你作為另外一個(gè)線程就只能先等待。
線程最為重要也是最為麻煩的就是線程間的交互通信過程,下圖是線程狀態(tài)的變化過程:
為了闡述線程間的通信,簡(jiǎn)單模擬一個(gè)生產(chǎn)者消費(fèi)者模型:
生產(chǎn)者
CarStock carStock;public CarProducter(CarStock carStock){ this.carStock = carStock;}@Overridepublic void run() { while (true){ carStock.produceCar(); }}public synchronized void produceCar(){ try { if(cars < 20){ System.out.println("生產(chǎn)者..." + cars); Thread.sleep(100); cars++; notifyAll(); }else { wait(); } } catch (InterruptedException e) { e.printStackTrace(); }}
消費(fèi)者
CarStock carStock;public CarConsumer(CarStock carStock){ this.carStock = carStock;}@Overridepublic void run() { while (true){ carStock.consumeCar(); }}public synchronized void consumeCar(){ try { if(cars > 0){ System.out.println("銷售車..." + cars); Thread.sleep(100); cars--; notifyAll(); }else { wait(); } } catch (InterruptedException e) { e.printStackTrace(); }}
消費(fèi)過程
通信過程
對(duì)于此簡(jiǎn)單的生產(chǎn)者消費(fèi)者模式可以運(yùn)用隊(duì)列、線程池等技術(shù)對(duì)程序進(jìn)行改進(jìn),運(yùn)用BolckingQueue隊(duì)列共享數(shù)據(jù),改進(jìn)后的消費(fèi)過程
05
并發(fā)編程三大特性
并發(fā)編程實(shí)現(xiàn)機(jī)制大多都是圍繞以下三點(diǎn):原子性、可見性、有序性
1)原子性問題
for(int i = 0; i < 20; i++){ Thread thread = new Thread(() -> { for (int j = 0; j < 10000; j++) { res++; normal++; atomicInteger.incrementAndGet(); } }); thread.start();}
運(yùn)行結(jié)果:
volatile: 170797
atomicInteger:200000
normal:182406
這就是原子性問題,原子性是指在一個(gè)操作中就是cpu不可以在中途暫停然后再調(diào)度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。
如果一個(gè)操作是原子性的,那么多線程并發(fā)的情況下,就不會(huì)出現(xiàn)變量被修改的情況。
2)可見性問題
class MyThread extends Thread{ public int index = 0; @Override public void run() { System.out.println("MyThread Start"); while (true) { if (index == -1) { break; } } System.out.println("MyThread End"); }}
main線程將index修改為-1,myThread線程并不可見,這就是可見性問題導(dǎo)致的線程安全,可見性就是指當(dāng)一個(gè)線程修改了線程共享變量的值,其它線程能夠立即得知這個(gè)修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方法來實(shí)現(xiàn)可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區(qū)別是volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每使用前立即從內(nèi)存刷新。因?yàn)槲覀兛梢哉fvolatile保證了線程操作時(shí)變量的可見性,而普通變量則不能保證這一點(diǎn)。
3)有序性問題
雙檢索單例懶漢模式
有序性: Java內(nèi)存模型中的程序天然有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行語(yǔ)義”,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存中主內(nèi)存同步延遲”現(xiàn)象。
到此,相信大家對(duì)“為什么要學(xué)習(xí)Java并發(fā)”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!