Java併發編程-知識前瞻(第一章)

前言:java

Java併發編程學習分享的目標:面試

  • Java併發編程中經常使用的工具用途與用法;編程

  • Java併發編程工具實現原理與設計思路;後端

  • 併發編程中遇到的常見問題與解決方案;api

  • 根據實際情景選擇更合適的工具完成高效的設計方案數組

學習分享團隊:
學而思培優-運營研發團隊
Java併發編程分享小組:
@沈健 @曹偉偉 @張俊勇 @田新文 @張晨
本章分享人:@張晨緩存

學習分享大綱:tomcat

01安全

初識併發微信

什麼是併發,什麼是並行? 

用個JVM的例子來說解,在垃圾回收器作併發標記的時候,這個時候JVM不只能夠作垃圾標記,還能夠處理程序的一些需求,這個叫併發。在作垃圾回收時,JVM多個線程同時作回收,這叫並行。

02

爲何要學習併發編程

直觀緣由
1)JD的強制性要求
隨着互聯網行業的飛速發展,併發編程已經成爲很是熱門的領域,也是各大企業服務端崗位招聘的必備技能。

2)從小牛通往大牛的必經之路
架構師是軟件開發團隊中很是重要的角色,成爲一名架構師是許多搞技術人奮鬥的目標,衡量一個架構師的能力指標就是設計出一套解決高併發的系統,因而可知高併發技術的重要性,而併發編程是底層的基礎。不管遊戲仍是互聯網行業,不管軟件開發仍是大型網站,都對高併發技術人才存在巨大需求,所以,爲了工做爲了提高本身,學習高併發技術刻不容緩。

3)面試過程當中極容易踩坑
面試的時候爲了考察對併發編程的掌握狀況,常常會考察併發安全相關的知識和線程交互的知識。例如在併發狀況下如何實現一個線程安全的單例模式,如何完成兩個線程中的功能交互執行。

如下是使用雙檢索實現一個線程安全的單例懶漢模式,固然也可使用枚舉或者單例餓漢模式。

 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修飾是由於在jvm中new Singleton()會出現指令重排,volatile避免happens before,避免空指針的問題。從一個線程安全的單例模式能夠引伸出不少,volatile和synchronized的實現原理,JMM模型,MESI協議,指令重排,關於JMM模型後序會給出更詳細的圖解。

除了線程安全問題,還會考察線程間的交互。 例如使用兩個線程交替打印出A1B2C3…Z26

考察的重點並非要簡單的實現這個功能,經過此面試題,能夠考察知識的總體掌握狀況,多種方案實現,可使用Atomicinteger、ReentrantLock、CountDownLat ch。下圖是使用LockSupport控制兩個線程交替打印的示例,LockSupport內部實現的原理是使用UNSAFE控制一個信號量在0和1之間變更,從而能夠控制兩個線程的交替打印。

4)併發在咱們工做使用的框架中到處可見,tom cat,netty,jvm,Disruptor

熟悉JAVA併發編程基礎是掌握這些框架底層知識的基石,這裏簡單介紹下高併發框架Disruptor的底層實現原理,作一個勾勒的做用:
Martin Fowler在一篇LMAX文章中介紹,這一個高性能異步處理框架,其單線程一秒的吞吐量可達六百萬

Disruptor核心概念

Disruptor特徵

  • 基於事件驅動

  • 基於"觀察者"模式、"生產者-消費者"模型

  • 能夠在無鎖的狀況下實現網絡的隊列操做

RingBuffer執行流程

Disruptor底層組件,RingBuffer密切相關的對象:Sequ enceBarrier和Sequencer;

SequenceBarrier是消費者和RingBuffer之間的橋樑。在Disruptor中,消費者直接訪問的是SequenceBarrier,由SequenceBarrier減小RingBuffer的隊列衝突。

SequenceBarrier 經過waitFor方法當消費者速度大於生產者的生產速度時,消費者可經過waitFor方法給予生產者必定的緩衝時間,協調生產者和消費者的速度問題,waitFor執行時機:

Sequencer是生產者和緩衝區RingBuffer之間的橋樑,生產者經過Sequencer向RingBuffer申請數據存放空間,經過WaitStrategy使用publish方法通知消費者,WaitStrategy是消費者沒有數據能夠消費時的等待策略。每一個生產者或者消費者線程,會先申請能夠操做的元素在數組中的位置,申請到以後,直接在該位置寫入或者讀取數據,整個過程經過原子變量CAS,保證操做的線程安全,這就是Disruptor的無鎖設計。

如下是五大經常使用等待策略:
BlockingWaitStrategy:Disruptor的默認策略是BlockingWaitStrategy。在BlockingWaitStrategy內部是使用鎖和condition來控制線程的喚醒。BlockingWaitStrategy是最低效的策略,但其對CPU的消耗最小而且在各類不一樣部署環境中能提供更加一致的性能表現。

SleepingWaitStrategy:SleepingWaitStrategy 的性能表現跟 BlockingWaitStrategy 差很少,對 CPU 的消耗也相似,但其對生產者線程的影響最小,經過使用LockSupport.parkNanos(1)來實現循環等待。

YieldingWaitStrategy:YieldingWaitStrategy是可使用在低延遲系統的策略之一。YieldingWaitStrategy將自旋以等待序列增長到適當的值。在循環體內,將調用Thread.yield()以容許其餘排隊的線程運行。在要求極高性能且事件處理線數小於 CPU 邏輯核心數的場景中,推薦使用此策略;例如,CPU開啓超線程的特性。

BusySpinWaitStrategy:性能最好,適合用於低延遲的系統。在要求極高性能且事件處理線程數小於CPU邏輯核心數的場景中,推薦使用此策略;例如,CPU開啓超線程的特性。

目前,包括Apache Storm、Camel、Log4j2在內的不少知名項目都應用了Disruptor以獲取高性能。

5)JUC是併發大神Doug Lea靈魂力做,堪稱典範(第一個主流嘗試,它將線程,鎖和事件以外的抽象層次提高到更平易近人的方式:併發集合, fork/join 等等)

經過併發編程設計思惟的學習,發揮使用多線程的優點

  • 發揮多處理器的強大能力

  • 建模的簡單性

  • 異步事件的簡化處理

  • 響應更靈敏的用戶界面

那麼學很差併發編程基礎會帶來什麼問題呢

1)多線程在平常開發中運用中到處都是,jvm、tomcat、netty,學好java併發編程是更深層次理解和掌握此類工具和框架的前提因爲計算機的cpu運算速度和內存io速度有幾個數量級的差距,所以現代計算機都不得不加入一層儘量接近處理器運算速度的高速緩存來作緩衝:將內存中運算須要使用的數據先複製到緩存中,當運算結束後再同步回內存。以下圖:

由於jvm要實現跨硬件平臺,所以jvm定義了本身的內存模型,可是由於jvm的內存模型最終仍是要映射到硬件上,所以jvm內存模型幾乎與硬件的模型同樣:

操做系統底層數據結構,每一個CPU對應的高速緩存中的數據結構是一個個bucket存儲的鏈表,其中tag表明的是主存中的地址,cache line是偏移量,flag對應的MESI緩存一致性協議中的各個狀態。

MESI緩存一致性狀態分別爲:

M:Modify,表明修改

E:Exclusive,表明獨佔

S:Share,表明共享

I:Invalidate,表明失效

如下是一次cpu0數據寫入的流程:

  • 在CPU0執行一次load,read和write時,在作write以前flag的狀態會是S,而後發出invalidate消息到總線;

  • 其餘cpu會監聽總線消息,將各cpu對應的cache entry中的flag狀態由S修改成I,而且發送invalidate ack給總線

  • cpu0收到全部cpu返回的invalidate ack後,cpu0將flag變爲E,執行數據寫入,狀態修改成M,相似於一個加鎖過程

考慮到性能問題,這樣寫入修改數據的效率太過漫長,所以引入了寫緩衝器和無效隊列,全部的修改操做會先寫入寫緩衝器,其餘cpu接收到消息後會先寫入無效隊列,並返回ack消息,以後再從無效隊列消費消息,採用異步的形式。固然,這樣就會產生有序性問題,例如某些entry中的flag仍是S,但實際上應該標識爲I,這樣訪問到的數據就會有問題。運用volitale是爲了解決指令重排帶來的無序性問題,volitale是jvm層面的關鍵字,MESI是cpu層面的,二者是差了幾個層次的。

2)性能不達標,找不到解決思路。

3)工做中可能會寫出線程不安全的方法
如下是一個多線程打印時間的逐步優化案例

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();

優化1,多個線程運用線程池複用


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;}

優化2,線程池結合ThreadLocal

public String date2(int seconds){ Date date = new Date(1000 * seconds); SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return simpleDateFormat.format(date);}

在多線程服用一個SimpleDateFormat時會出現線程安全問題,執行結果會打印出相同的時間,在優化2中使用線程池結合ThreadLocal實現資源隔離,線程安全。

4)許多問題沒法正肯定位
踩坑:crm仿真定時任務阻塞,沒法繼續執行
問題:crm仿真運用schedule配置的定時任務在某個時間節點後的全部定時任務均未執行
緣由:定時任務配置致使的問題,@Schedule配置的定時任務若是未配置線程池,在啓動類使用@EnableScheduling啓用定時任務時會默認使用單線程,後端配置了多定時任務,會出現問題.配置了兩定時任務A和B,在A先佔用資源後若是一直未釋放,B會一直處於等待狀態,直到A任務釋放資源後,B開始執行,若要避免多任務執行帶來的問題,須要使用如下方法配置:

@Bean public ThreadPoolTaskScheduler taskScheduler(){  ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();  scheduler.setPoolSize(10);  return scheduler; }

crm服務因爲定時任務配置的很少,而且在資源足夠的狀況下,任務執行速度相對較快,並未設置定時任務的線程池

定時任務里程序方法如何形成線程一直未釋放,致使阻塞。

 

在問題定位時,產生的問題來自CountDownLatch沒法歸零,致使整個主線程hang在那裏,沒法釋放。

在api中當調用await時候,調用線程處於等待掛起狀態,直至count變成0再繼續,大體原理以下:

所以將目光焦點轉移至await方法,使當前線程在鎖存器倒計數至零以前一直等待,除非線程被中斷或超出了指定的等待時間。若是當前計數爲零,則此方法馬上返回true 值。若是當前計數大於零,則出於線程調度目的,將禁用當前線程,且在發生如下三種狀況之一前,該線程將一直處於休眠狀態:因爲調用 countDown() 方法,計數到達零;或者其餘某個線程中斷當前線程;或者已超出指定的等待時間。

Executors.newFixedThreadPool這是個有固定活動線程數。當提交到池中的任務數大於固定活動線程數時,任務就會放到阻塞隊列中等待。CRM該定時任務裏爲了加快任務處理,運用多線程處理,設置的CountDownLatch的count大於ThreadPoolExecutor的固定活動線程數致使任務一直處於等待狀態,計數沒法歸零,致使主線程一直沒法釋放,從而致使crm一臺仿真服務的定時任務處於癱瘓狀態。

03

如何學習java併發編程

爲了學習好併發編程基礎,咱們須要有一個上帝視角,一個宏觀的概念,而後由點及深,掌握必備的知識點。咱們能夠從如下兩張思惟導圖列舉出來的逐步進行學習。

必備知識點

04

線程

列舉了如此多的案例都是圍繞線程展開的,因此咱們須要更深地掌握線程,它的概念,它的原則,它是如何實現交互通訊的。

如下的一張圖能夠更通俗地解釋進程、線程的區別

進程: 一個進程比如是一個程序,它是 資源分配的最小單位 。同一時刻執行的進程數不會超過核心數。不過若是問單核CPU可否運行多進程?答案又是確定的。單核CPU也能夠運行多進程,只不過不是同時的,而是極快地在進程間來回切換實現的多進程。電腦中有許多進程須要處於「同時」開啓的狀態,而利用CPU在進程間的快速切換,能夠實現「同時」運行多個程序。而進程切換則意味着須要保留進程切換前的狀態,以備切換回去的時候可以繼續接着工做。因此進程擁有本身的地址空間,全局變量,文件描述符,各類硬件等等資源。操做系統經過調度CPU去執行進程的記錄、回覆、切換等等。

線程:線程是獨立運行和獨立調度的基本單位(CPU上真正運行的是線程),線程至關於一個進程中不一樣的執行路徑。

單線程:單線程就是一個叫作「進程」的房子裏面,只住了你一我的,你能夠在這個房子裏面任什麼時候間去作任何的事情。你是看電視、仍是玩電腦,全都有你本身說的算。想幹什麼幹什麼,想什麼時間作什麼就什麼時間作什麼。

多線程:可是若是你處在一個「多人」的房子裏面,每一個房子裏面都有叫作「線程」的住戶:線程一、線程二、線程三、線程4,狀況就不得不發生變化了。

在多線程編程中有」鎖」的概念,在你的房子裏面也有鎖。若是你的老婆在上廁所並鎖上門,她就是在獨享這個「房子(進程)」裏面的公共資源「衛生間」,若是你的家裏只有這一個衛生間,你做爲另一個線程就只能先等待。

線程最爲重要也是最爲麻煩的就是線程間的交互通訊過程,下圖是線程狀態的變化過程:

爲了闡述線程間的通訊,簡單模擬一個生產者消費者模型:

生產者​​​​​​​


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("生產者..." + cars); Thread.sleep(100); cars++; notifyAll(); }else { wait(); } } catch (InterruptedException e) { e.printStackTrace(); }}

消費者



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(); }}

消費過程

通訊過程

對於此簡單的生產者消費者模式能夠運用隊列、線程池等技術對程序進行改進,運用BolckingQueue隊列共享數據,改進後的消費過程

05

併發編程三大特性

併發編程實現機制大多都是圍繞如下三點:原子性、可見性、有序性

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();}

運行結果:

volatile: 170797
atomicInteger:200000
normal:182406

這就是原子性問題,原子性是指在一個操做中就是cpu不能夠在中途暫停而後再調度,既不被中斷操做,要不執行完成,要不就不執行。
若是一個操做是原子性的,那麼多線程併發的狀況下,就不會出現變量被修改的狀況。

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線程並不可見,這就是可見性問題致使的線程安全,可見性就是指當一個線程修改了線程共享變量的值,其它線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方法來實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是volatile的特殊規則保證了新值能當即同步到主內存,以及每使用前當即從內存刷新。由於咱們能夠說volatile保證了線程操做時變量的可見性,而普通變量則不能保證這一點。

3)有序性問題

雙檢索單例懶漢模式

有序性: Java內存模型中的程序自然有序性能夠總結爲一句話:若是在本線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句是指「指令重排序」現象和「工做內存中主內存同步延遲」現象。

06

思考題

有時爲了儘快釋放資源,避免無心義的耗費,會令部分功能提早結束,例如許多搶名額問題,這裏出一個思考題供你們參考實現:
題:8人百米賽跑,要求前三名跑到終點後中止運行,設計該問題的實現。

參考資料:
1.億級流量Java高併發與網絡編程實戰
2.LMAX文章(http://ifeve.com/lmax/)

下章預告:Volatile和Syncronize關鍵字

  • Volatile關鍵字

  • Synchronized關鍵字Volatile關鍵字
    Synchronized關鍵字

關於好將來技術更多內容請:微信掃碼關注「好將來技術」微信公衆號

4月23日世界讀書日「好將來技術」微信公衆號免費送書啦~

掃碼關注「好將來技術」微信公衆號回覆「送書」便可參與本次活動

專屬寵粉福利,數量有限先到先得,還在等什麼快來關注吧!!

 

相關文章
相關標籤/搜索