您好,登錄后才能下訂單哦!
本篇內容主要講解“為什么要學習Java并發”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“為什么要學習Java并發”吧!
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上真正運行的是線程),線程相當于一個進程中不同的執行路徑。
單線程:單線程就是一個叫做“進程”的房子里面,只住了你一個人,你可以在這個房子里面任何時間去做任何的事情。你是看電視、還是玩電腦,全都有你自己說的算。想干什么干什么,想什么時間做什么就什么時間做什么。
多線程:但是如果你處在一個“多人”的房子里面,每個房子里面都有叫做“線程”的住戶:線程1、線程2、線程3、線程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內存模型中的程序天然有序性可以總結為一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現為串行語義”,后半句是指“指令重排序”現象和“工作內存中主內存同步延遲”現象。
到此,相信大家對“為什么要學習Java并發”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。