9.實戰java高併發程序設計(全書總結)

1.瞭解同步(Synchronous)異步(Asynchronous)的區別
同步方法調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行爲。異步方法調用更像一個消息傳遞,一旦開始,方法調用就會立即返回,

2. 併發(Concurrency)和並行(Parallelism)的區別

並行的多個任務是真的同時執行,而對於併發來說,這個過程只是交替的,一會兒執行任務A,一會兒執行任務B,系統會不停地在兩者之間切換。但對於外部觀察者來說,即使多個任務之間是串行併發的,也會造成多任務間並行執行的錯覺。

3.臨界區

臨界區用來表示一種公共資源或者說共享數據,可以被多個線程使用。但是每一次,只能有一個線程使用它,一旦臨界區資源被佔用,其他線程要想使用這個資源就必須等待。

4 阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用來形容多線程間的相互影響

5.死鎖(Deadlock)、飢餓(Starvation)和活鎖(Livelock)

死鎖是一個很嚴重的並且應該避免和時時小心的問題,都在等待對方的鎖,不願釋放自己的鎖

飢餓是指某一個或者多個線程因爲種種原因無法獲得所需要的資源,導致一直無法執行。比如它的線程優先級可能太低,而高優先級的線程不斷搶佔它需要的資源,導致低優先級線程無法工作.飢餓還是有可能在未來一段時間內解決的(比如,高優先級的線程已經完成任務,不再瘋狂執行)。

如果線程的智力不夠,且都秉承着“謙讓”的原則,主動將資源釋放給他人使用,那麼就會導致資源不斷地在兩個線程間跳動,而沒有一個線程可以同時拿到所有資源正常執行。這種情況就是活鎖。

6.併發級別

阻塞、無飢餓、無障礙、無鎖、無等待

當我們使用synchronized關鍵字或者重入鎖時(我們將在第2、3章介紹這兩種技術),我們得到的就是阻塞的線程。

對於非公平鎖來說,系統允許高優先級的線程插隊。這樣有可能導致低優先級線程產生飢餓。但如果鎖是公平的,按照先來後到的規則,那麼飢餓就不會產生,不管新來的線程優先級多高,要想獲得資源,就必須乖乖排隊,這樣所有的線程都有機會執行。

無障礙是一種最弱的非阻塞調度。兩個線程如果無障礙地執行,那麼不會因爲臨界區的問題導致一方被掛起。換言之,大家都可以大搖大擺地進入臨界區了。那麼大家一起修改共享數據,把數據改壞了怎麼辦呢?對於無障礙的線程來說,一旦檢測到這種情況,它就會立即對自己所做的修改進行回滾,確保數據安全。但如果沒有數據競爭發生,那麼線程就可以順利完成自己的工作,走出臨界區。一種可行的無障礙實現可以依賴一個“一致性標記”來實現.線程在操作之前,先讀取並保存這個標記,在操作完成後,再次讀取,檢查這個標記是否被更改過,如果兩者是一致的,則說明資源訪問沒有衝突。如果不一致,則說明資源可能在操作過程中與其他寫線程衝突,需要重試操作。而任何對資源有修改操作的線程,在修改數據前,都需要更新這個一致性標記,表示數據不再安全。

無鎖的併發保證必然有一個線程能夠在有限步內完成操作離開臨界區。

要求所有的線程都必須在有限步內完成,這樣就不會引起飢餓問題。

7.JMM的關鍵技術點都是圍繞着多線程的原子性、可見性和有序性來建立的。

原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾,對於32位系統來說,long型數據的讀寫不是原子性的

可見性是指當一個線程修改了某一個共享變量的值時,其他線程是否能夠立即知道這個修改

指令重排對於提高CPU處理性能是十分必要的。雖然確實帶來了亂序的問題,但是這點犧牲是完全值得的。

8.進程與線程

進程是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。

線程就是輕量級進程,是程序執行的最小單位。使用多線程而不是用多進程去進行併發程序的設計,是因爲線程間的切換和調度的成本遠遠小於進程

9.線程狀態

新建線程new
開啓線程start

線程中斷(interrupt線程中斷並不會使線程立即退出,而是給線程發送一個通知,告知目標線程,有人希望你退出啦)

暴力終止線程stop(原因是stop()方法過於暴力,強行把執行到一半的線程終止,可能會引起一些數據不一致的問題.)

等待(wait)和通知(notify:這兩個方法並不是在Thread類中的,而是輸出Object類。

注意:Object.wait()方法和Thread.sleep()方法都可以讓線程等待若干時間。除wait()方法可以被喚醒外,另外一個主要區別就是wait()方法會釋放目標對象的鎖,而Thread.sleep()方法不會釋放任何資源。

掛起(suspend)和繼續執行(resume)線程:廢棄方法,並不推薦使用。

 等待線程結束(join,也可以加入等待時間)和謙讓(yeild):

join():)一個線程的輸入可能非常依賴於另外一個或者多個線程的輸出,此時,這個線程就需要等待依賴線程執行完畢,才能繼續執行。JDK提供了join()操作來實現這個功能。

Thread.yeild()靜態方法,一旦執行,它會使當前線程讓出CPU。但要注意,讓出CPU並不表示當前線程不執行了。當前線程在讓出CPU後,還會進行CPU資源的爭奪,但是是否能夠再次被分配到就不一定了。

10.volatile與Java內存模型(JMM):爲了在適當的場合,確保線程間的有序性、可見性和原子性。Java使用了一些特殊的操作或者關鍵字來聲明、告訴虛擬機,在這個地方,要尤其注意,不能隨意變動優化目標指令。關鍵字volatile就是其中之一。當你用關鍵字volatile聲明一個變量時,就等於告訴了虛擬機,這個變量極有可能會被某些程序或者線程修改。爲了確保這個變量被修改後,應用程序範圍內的所有線程都能夠“看到”這個改動,虛擬機就必須採用一些特殊的手段,保證這個變量的可見性等特點。

11.分門別類的管理:線程組(ThreadGroup,注意:是線程組不是線程池,可以創建線程加入線程組中)

11.駐守後臺:守護線程(Daemon)(守護線程守護的是用戶線程,xxThread.setDaemom(true)):

守護線程是一種特殊的線程,就和它的名字一樣,它是系統的守護者,在後臺默默地完成一些系統性的服務,比如垃圾回收線程、JIT線程就可以理解爲守護線程。與之相對應的是用戶線程,用戶線程可以認爲是系統的工作線程,它會完成這個程序應該要完成的業務操作。如果用戶線程全部結束,則意味着這個程序實際上無事可做了。守護線程要守護的對象已經不存在了,那麼整個應用程序就應該結束。因此,當一個Java應用內只有守護線程時,Java虛擬機就會自然退出。
12.先做重要的事:線程優先級(xxxThread.setPriority()):Java中的線程可以有自己的優先級。優先級高的線程在競爭資源時會更有優勢,更可能搶佔資源,當然,這只是一個概率問題

13.線程安全的概念與關鍵字synchronized

關鍵字synchronized可以有多種用法,這裏做一個簡單的整理。● 指定加鎖對象:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖。● 直接作用於實例方法:相當於對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。● 直接作用於靜態方法:相當於對當前類加鎖,進入同步代碼前要獲得當前類的鎖。

14.程序中的幽靈:隱蔽的錯誤

  • 無提示的錯誤案例
  • 併發下的ArrayList,:改進的方法很簡單,使用線程安全的Vector代替ArrayList即可。
  • 併發下詭異的HashMap,最簡單的解決方案就是使用ConcurrentHashMap代替HashMap。
  • 初學者常見的問題:錯誤的加鎖

15.多線程的團隊協作:同步控制,重入鎖ReentrantLock

關鍵字synchronized的功能擴展:重入鎖,從JDK 6.0開始,JDK在關鍵字synchronized上做了大量的優化,使得兩者的性能差距並不大。

與關鍵字synchronized相比,重入鎖有着顯示的操作過程。開發人員必須手動指定何時加鎖,何時釋放鎖。也正因爲這樣,重入鎖對邏輯控制的靈活性要遠遠優於關鍵字synchronized。但值得注意的是,在退出臨界區時,必須記得釋放鎖,否則,其他線程就沒有機會再訪問臨界區了。

  • 中斷響應lockInterruptibly():這是一個可以對中斷進行響應的鎖申請動作,即在等待鎖的過程中,可以響應中斷
  • 鎖申請等待限時tryLock()
  • 公平鎖 new ReentrantLock(true):它不會產生飢餓現象,公平鎖的實現成本比較高,性能卻非常低下
  • unlock():釋放鎖。
  • lock():獲得鎖,如果鎖已經被佔用,則等待。

16. 重入鎖的好搭檔:Condition : lock.newCondition():利用Condition對象,我們就可以讓線程在合適的時間等待,或者在某一個特定的時刻得到通知,繼續執行。類似wait(),notify()

● await()方法會使當前線程等待,同時釋放當前鎖,當其他線程中使用signal()方法或者signalAll()方法時,線程會重新獲得鎖並繼續執行。或者當線程被中斷時,也能跳出等待。這和Object.wait()方法相似。

● awaitUninterruptibly()方法與await()方法基本相同,但是它並不會在等待過程中響應中斷。

● singal()方法用於喚醒一個在等待中的線程,singalAll()方法會喚醒所有在等待中的線程。這和Obejct.notify()方法很類似。


17. 允許多個線程同時訪問:信號量(Semaphore):信號量卻可以指定多個線程,同時訪問某一個資源

18.ReadWriteLock讀寫鎖 lock.writeLock(),lock.readLock()

讀寫分離鎖可以有效地幫助減少鎖競爭,提升系統性能。

● 讀-讀不互斥:讀讀之間不阻塞。

● 讀-寫互斥:讀阻塞寫,寫也會阻塞讀。

● 寫-寫互斥:寫寫阻塞。

19.倒計數器:CountDownLatch:CountDownLatch是一個非常實用的多線程控制工具類。這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計數結束,再開始執行。

20.循環柵欄:CyclicBarrier

CyclicBarrier是另外一種多線程併發控制工具。和CountDownLatch非常類似,它也可以實現線程間的計數等待,但它的功能比CountDownLatch更加複雜且強大。

21.線程阻塞工具類:LockSupport

LockSupport是一個非常方便實用的線程阻塞工具,它可以在線程內任意位置讓線程阻塞。與Thread.suspend()方法相比,它彌補了由於resume()方法發生導致線程無法繼續執行的情況。和Object.wait()方法相比,它不需要先獲得某個對象的鎖,也不會拋出InterruptedException異常。
LockSupport的靜態方法park()可以阻塞當前線程,類似的還有parkNanos()、parkUntil()等方法。它們實現了一個限時的等待。

22.線程複用:線程池

爲了避免系統頻繁地創建和銷燬線程,我們可以讓創建的線程複用。

JDK對線程池的支持Executor,java.util.concurrent包中,是JDK併發包的核心類。

● newFixedThreadPool()方法:該方法返回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務提交時,線程池中若有空閒線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閒時,便處理任務隊列中的任務。固定數量線程池

● newSingleThreadExecutor()方法:該方法返回一個只有一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閒,按先入先出的順序執行隊列中的任務。單個線程線程池

● newCachedThreadPool()方法:該方法返回一個可根據實際情況調整線程數量的線程池。線程池的線程數量不確定,但若有空閒線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新的任務提交,則會創建新的線程處理任務。所有線程在當前任務執行完畢後,將返回線程池進行復用。可自動擴充數量線程池

● newSingleThreadScheduledExecutor()方法:該方法返回一個ScheduledExecutorService對象,線程池大小爲1。ScheduledExecutorService接口在ExecutorService接口之上擴展了在給定時間執行某任務的功能,如在某個固定的延時之後執行,或者週期性執行某個任務。可指定時間的單個線程線程池

● newScheduledThreadPool()方法:該方法也返回一個ScheduledExecutorService對象,但該線程池可以指定線程數量。可指定數量線程池,可以根據時間需要對線程進行調度。

23.核心線程池的內部實現,其內部實現均使用了ThreadPoolExecutor類\

24.拒絕策略

ThreadPoolExecutor類的最後一個參數指定了拒絕策略。也就是當任務數量超過系統實際承載能力時,就要用到拒絕策略了。拒絕策略可以說是系統超負荷運行時的補救措施,通常由於壓力太大而引起的,也就是線程池中的線程已經用完了,無法繼續爲新任務服務,同時,等待隊列中也已經排滿了,再也放不下新任務了。這時,我們就需要有一套機制合理地處理這個問題。

● AbortPolicy策略:該策略會直接拋出異常,阻止系統正常工作。

● CallerRunsPolicy策略:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降。

● DiscardOldestPolicy策略:該策略將丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務。

● DiscardPolicy策略:該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,我覺得這可能是最好的一種方案了吧!
25. 自定義線程創建:ThreadFactory

自定義線程池可以幫助我們做不少事。比如,我們可以跟蹤線程池究竟在何時創建了多少線程,也可以自定義線程的名稱、組以及優先級等信息,甚至可以任性地將所有的線程設置爲守護線程。總之,使用自定義線程池可以讓我們更加自由地設置線程池中所有線程的狀態。

26.擴展線程池

ThreadPoolExecutor是一個可以擴展的線程池。它提供了beforeExecute()、afterExecute()和terminated()三個接口用來對線程池進行控制

27.堆棧去哪裏了:在線程池中尋找堆棧

一種最簡單的方法就是放棄submit()方法,改用execute()方法\

28.併發集合簡介

JDK提供的這些容器大部分在java.util.concurrent包中。我先提綱挈領地介紹一下它們,初次露臉,大家只需要知道它們的作用即可。有關具體的實現和注意事項,在後面我會一一道來。

● ConcurrentHashMap:這是一個高效的併發HashMap。你可以把它理解爲一個線程安全的HashMap。

● CopyOnWriteArrayList:這是一個List,從名字看就知道它和ArrayList是一族的。在讀多寫少的場合,這個List的性能非常好,遠遠優於Vector(從這個類的名字我們可以看到,所謂CopyOnWrite就是在寫入操作時,進行一次自我複製。換句話說,當這個List需要修改時,我並不修改原有的內容(這對於保證當前在讀線程的數據一致性非常重要),而是對原有的數據進行一次複製,將修改的內容寫入副本中。寫完之後,再用修改完的副本替換原來的數據,這樣就可以保證寫操作不會影響讀了)
 

● ConcurrentLinkedQueue:高效的併發隊列,使用鏈表實現。可以看作一個線程安全的LinkedList。

● BlockingQueue:這是一個接口,JDK內部通過鏈表、數組等方式實現了這個接口。表示阻塞隊列,非常適合作爲數據共享的通道。

● ConcurrentSkipListMap:跳錶的實現。這是一個Map,使用跳錶的數據結構進行快速查找。除以上併發包中的專有數據結構以外,java.util下的Vector是線程安全的(雖然性能和上述專用工具沒得比),另外Collections工具類可以幫助我們將任意集合包裝成線程安全的集合(Collections.synchronizedMap())。

跳錶是一種可以用來快速查找的數據結構,有點類似於平衡樹。它們都可以對元素進行快速查找。但一個重要的區別是:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整,而對跳錶的插入和刪除只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高併發的情況下,你會需要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,你只需要部分鎖即可。這樣,在高併發環境下,你就可以擁有更好的性能。

使用跳錶實現Map和使用哈希算法實現Map的另外一個不同之處是:哈希並不會保存元素的順序,而跳錶內所有的元素都是有序的。

29.使用JMH進行性能測試

Maven進行導入

1.模式(Mode)Mode表示JMH的測量方式和角度,共有4種。

● Throughput:整體吞吐量,表示1秒內可以執行多少次調用。

● AverageTime:調用的平均時間,指每一次調用所需要的時間。

● SampleTime:隨機取樣,最後輸出取樣結果的分佈,例如“99%的調用在xxx毫秒以內,99.99%的調用在xxx毫秒以內”。

● SingleShotTime:以上模式都是默認一次Iteration是1秒,唯有SingleShotTime只運行一次。往往同時把 warmup 次數設爲0,用於測試冷啓動時的性能。

30.有關性能的一些思考

HashMap的get()方法比ConcurrentHashMap的快,也不能說明它的put()方法或者size()方法同樣也會更快。因此,快慢的比較不能離開具體的使用場景。在單線程下,ConcurrentHashMap的get()方法比HashMap的略快,但是size()方法卻比HashMap的慢很多。當HashMap進行同步後,由於同步鎖的開銷,size()方法的性能急劇下降,與ConcurrentHashMap的size()方法在一個數量級上,但依然比ConcurrentHashMap快。
 

31.有助於提高鎖性能的幾點建議

  •  減少鎖持有時間
  • 減小鎖粒度
  • 用讀寫分離鎖來替換獨佔鎖
  •  鎖粗化(虛擬機在遇到一連串連續地對同一個鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步的次數,這個操作叫作鎖的粗化)

32.Java虛擬機對鎖優化所做的努力

鎖偏向:它的核心思想是:如果一個線程獲得了鎖,那麼鎖就進入偏向模式。當這個線程再次請求鎖時,無須再做任何同步操作。這樣就節省了大量有關鎖申請的操作,從而提高了程序性能

自旋鎖:鎖膨脹後,爲了避免線程真實地在操作系統層面掛起,虛擬機還會做最後的努力—自旋鎖。當前線程暫時無法獲得鎖,而且什麼時候可以獲得鎖是一個未知數,也許在幾個CPU時鐘週期後就可以得到鎖。如果這樣,簡單粗暴地掛起線程可能是一種得不償失的操作。系統會假設在不久的將來,線程可以得到這把鎖。因此,虛擬機會讓當前線程做幾個空循環(這也是自旋的含義),在經過若干次循環後,如果可以得到鎖,那麼就順利進入臨界區。如果還不能獲得鎖,纔會真的將線程在操作系統層面掛起。
 鎖消除:鎖消除是一種更徹底的鎖優化。Java虛擬機在JIT編譯時,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖。通過鎖消除,可以節省毫無意義的請求鎖時間

31.所謂逃逸分析就是觀察某一個變量是否會逃出某一個作用域。變量v逃逸出了當前函數,也就是說變量v有可能被其他線程訪問

32.人手一支筆:ThreadLocal

這是一個線程的局部變量。也就是說,只有當前線程可以訪問。既然是隻有當前線程可以訪問的數據,自然是線程安全的。

ThreadLocal的實現原理:我們需要關注的自然是ThreadLocal的set()方法和get()方法。先從set()方法說起:

其中,key爲ThreadLocal當前對象,value就是我們需要的值。而threadLocals本身就保存了當前自己所在線程的所有“局部變量”,也就是一個ThreadLocal變量的集合.  在ThreadLocal類中有一個ThreadLocalMap, 用於存放每一個線程的變量副本,Map中元素的key爲線程對象,value爲對應線程的變量副本。

  另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,並不是通過ThreadLocal.set()來實現的,而是通過每個線程中的new 對象 的操作來創建的對象,每個線程創建一個,不是什麼對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作爲map的key來使用的。 
在瞭解了ThreadLocal的內部實現後,我們自然會引出一個問題:那就是這些變量是維護在Thread類內部的(ThreadLocalMap定義所在類),這也意味着只要線程不退出,對象的引用將一直存在。當線程退出時,Thread類會進行一些清理工作,其中就包括清理ThreadLocalMap.

因此,使用線程池就意味着當前線程未必會退出(比如固定大小的線程池,線程總是存在)。如果這樣,將一些大的對象設置到ThreadLocal中(它實際保存在線程持有的threadLocals Map內),可能會使系統出現內存泄漏的可能(這裏我的意思是:你設置了對象到ThreadLocal中,但是不清理它,在你使用幾次後,這個對象也不再有用了,但是它卻無法被回收)。

此時,如果你希望及時回收對象,最好使用ThreadLocal.remove()方法將這個變量移除。
 

33.無鎖

無鎖的策略使用一種叫作比較交換(CAS,Compare And Swap)的技術來鑑別線程衝突,一旦檢測到衝突產生,就重試當前操作直到沒有衝突爲止

與鎖相比,使用比較交換會使程序看起來更加複雜一些,但由於其非阻塞性,它對死鎖問題天生免疫,並且線程間的相互影響也遠遠比基於鎖的方式要小。更爲重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,因此,它要比基於鎖的方式擁有更優越的性能。

CAS算法的過程是:它包含三個參數CAS(V,E,N),其中V表示要更新的變量,E表示預期值,N表示新值。僅當V值等於E值時,纔會將V的值設爲N,如果V值和E值不同,說明已經有其他線程做了更新,則當前線程什麼都不做。最後,CAS返回當前V的真實值。CAS操作是抱着樂觀的態度進行的,它總是認爲自己可以成功完成操作。當多個線程同時使用CAS操作一個變量時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的線程不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的線程放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他線程對當前線程的干擾,並進行恰當的處理。
 

34.無鎖的線程安全整數:AtomicInteger

JDK併發包中有一個atomic包,裏面實現了一些直接使用CAS操作的線程安全的類型。

與Integer不同,它是可變的,並且是線程安全的。使用AtomicInteger會比使用鎖具有更好的性能

無鎖的對象引用:AtomicReference

帶有時間戳的對象引用:AtomicStampedReference(當AtomicStampedReference對應的數值被修改時,除了更新數據本身外,還必須要更新時間戳。當AtomicStampedReference設置對象值時,對象值及時間戳都必須滿足期望值,寫入纔會成功。)

數組也能無鎖:AtomicIntegerArray

 

35.有關死鎖的問題

通常的表現就是相關的進程不再工作,並且CPU佔用率爲0(因爲死鎖的線程不佔用CPU)

我們可以使用jps命令得到Java進程的進程ID,接着使用jstack命令得到線程的線程堆棧

 

37.探討單例模式

(1)對於頻繁使用的對象,可以省略new操作花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷。

(2)由於new操作的次數減少,因而對系統內存的使用頻率也會降低,這將減輕GC壓力,縮短GC停頓時間。

一種不錯的單例創建方式

上述代碼實現了一個單例,並且同時擁有前兩種方式的優點。首先getInstance()方法中沒有鎖,這使得在高併發環境下性能優越。其次,只有在getInstance()方法第一次被調用時,StaticSingleton的實例纔會被創建。因爲這種方法巧妙地使用了內部類和類的初始化方式。內部類SingletonHolder被聲明爲private,這使得我們不可能在外部訪問並初始化它。而我們只可能在getInstance()方法內部對SingletonHolder類進行初始化,利用虛擬機的類初始化機制創建單例。
 

38.不變模式

爲了儘可能地去除這些同步操作,提高並行程序性能可以使用一種不可改變的對象,依靠對象的不變性,可以確保其在沒有同步操作的多線程環境中依然保持內部狀態的一致性和正確性。這就是不變模式。

不變模式天生就是多線程友好的,它的核心思想是,一個對象一旦被創建,它的內部狀態將永遠不會發生改變。沒有一個線程可以修改其內部狀態和數據,同時其內部狀態也絕不會自行發生改變。基於這些特性,對不變對象的多線程操作不需要進行同步控制。

不變模式的主要使用場景需要滿足以下

● 去除setter方法及所有修改自身屬性的方法。

● 將所有屬性設置爲私有,並用final標記,確保其不可修改。

● 確保沒有子類可以重載修改它的行爲。

● 有一個可以創建完整對象的構造函數。以下代碼實現了一個不變的產品對象,它擁有序列號、名稱和價格三個屬性

39.生產者-消費者模式

消費者線程並不直接與生產者線程通信,而是在共享內存緩衝區中獲取任務,並進行處理

注意:生產者-消費者模式中的內存緩衝區的主要功能是數據在多線程間的共享,此外,通過該緩衝區,可以緩解生產者和消費者間的性能差。

生產者-消費者模式的核心組件是共享內存緩衝區,它作爲生產者和消費者間的通信橋樑,避免了生產者和消費者直接通信,從而將生產者和消費者進行解耦。生產者不需要知道消費者的存在,消費者也不需要知道生產者的存在。

40.Java 8/9/10與併發

函數式編程:一個函數可以作爲另外一個函數的返回值

特點:

  • 無副作用(顯式函數指函數與外界交換數據的唯一渠道就是參數和返回值,顯式函數不會去讀取或者修改函數的外部狀態。與之相對的是隱式函數,隱式函數除參數和返回值外,還會讀取外部信息,或者修改外部信息。)
  • 聲明式的(Declarative)對於聲明式的編程範式,你不再需要提供明確的指令操作,所有的細節指令將會更好地被程序庫封裝,你要做的只是提出你的要求,聲明你的用意即可。
  • 不變的對象:在函數式編程中,幾乎所有傳遞的對象都不會被輕易修改。
  • 易於並行:由於對象都處於不變的狀態,因此函數式編程更加易於並行
  • 更少的代碼

41.FunctionalInterface註釋

所謂函數式接口,簡單地說,就是隻定義了單一抽象方法的接口,,函數式接口只能有一個抽象方法,而不是隻能有一個方法

42.接口默認方法

接口裏面可以定義的方法裏面可以有方法體,實現類可以不重寫接口裏的默認方法 ,實現類可以直接拿來用,不需要重寫方法,有繼承的味道

43. lambda表達式

lambda表達式可以說是函數式編程的核心。lambda表達式即匿名函數,它是一段沒有函數名的函數體,可以作爲參數直接傳遞給相關的調用者,lambda表達式極大地增強了Java語言的表達能力。

44. 方法引用

法引用在Java 8中的使用非常靈活。總的來說,可以分爲以下幾種。

● 靜態方法引用:ClassName::methodName。

● 實例上的實例方法引用:instanceReference::methodName。

● 超類上的實例方法引用:super::methodName。

● 類型上的實例方法引用:ClassName::methodName。

● 構造方法引用:Class::new。

● 數組構造方法引用:TypeName[]::new。

首先,方法引用使用“::”定義,“::”的前半部分表示類名或者實例名,後半部分表示方法名稱。如果是構造函數,則使用new表示
 

lambda表達式。表達式由“->”分割,左半部分表示參數,右半部分表示實現體。因此,我們也可以簡單地理解lambda表達式只是匿名對象實現的一種新的方式。

45.並行流與並行排序

parallel()方法得到一個並行流

Arrays.parallelSort()

46.ConcurrentHashMap的增強

foreach操作

reduce操作

條件插入computeIfAbsent()

search操作

我們需要理解ArrayList的工作方式。在ArrayList初始化時,默認會分配10個數組空間。當數組空間消耗完畢後,ArrayList就會進行自動擴容。在每次add()函數時,系統總要事先檢查一下內部空間是否滿足所需的大小,如果不滿足,就會擴容,否則就正常添加元素。多線程共同訪問ArrayList的問題在於:在ArrayList容量快用完時(只有1個可用空間),如果兩個線程同時進入add()函數,並同時判斷認爲系統滿足繼續添加元素而不需要擴容,那麼兩者都不會進行擴容操作。之後,兩個線程先後向系統寫入自己的數據,那麼必然有一個線程會將數據寫到邊界外,從而產生了ArrayIndexOutOfBoundsException。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章