概念:(immutable)對象建立後,狀態不可更改。不可變對象在併發程序中尤爲有用,因狀態不可變,不會被線程干擾,也不會出現不一致狀態。html
書中經過實例是可變的類,並今後類衍生出一個不可變實例。java
SynchronizedRGB類是表示顏色的類,每個對象表明一種顏色,使用三個整形數表示顏色的三基色,字符串表示顏色名稱。git
public class SynchronizedRGB { // Values must be between 0 and 255. private int red; private int green; private int blue; private String name; private void check(int red, int green, int blue) { if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) { throw new IllegalArgumentException(); } } public SynchronizedRGB(int red, int green, int blue, String name) { check(red, green, blue); this.red = red; this.green = green; this.blue = blue; this.name = name; } public void set(int red, int green, int blue, String name) { check(red, green, blue); synchronized (this) { this.red = red; this.green = green; this.blue = blue; this.name = name; } } public synchronized int getRGB() { return ((red << 16) | (green << 8) | blue); } public synchronized String getName() { return name; } public synchronized void invert() { red = 255 - red; green = 255 - green; blue = 255 - blue; name = "Inverse of " + name; } }
爲避免不一致狀態,須當心使用SynchronizedRGB,如某線程執行以下代碼:程序員
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black"); ... int myColorInt = color.getRGB(); //Statement 1 String myColorName = color.getName(); //Statement 2
若是在statement1和statement2間,另外一個線程執行了color.set方法,則myColorInt的值與myColorName值不匹配。可經過以下方法避免:github
synchronized (color) { int myColorInt = color.getRGB(); String myColorName = color.getName(); }
目前爲止,以前的教程都是重點講述了最初做爲 Java 平臺一部分的低級別 API。這些API 對於很是基本的任務來講已經足夠,可是對於更高級的任務就須要更高級的 API。特別是針對充分利用了當今多處理器和多核系統的大規模併發應用程序。 本章,咱們將着眼於 Java 5.0 新增的一些高級併發特徵。大多數功能已經在新的java.util.concurrent 包中實現。Java 集合框架中也定義了新的併發數據結構。算法
1)鎖對象,支持可簡化許多併發應用程序的鎖用法編程
2)執行器,定義運行和管理線程的高級api。適用於大型應用程序的線程池管理。api
3)併發集合,簡化大型數據集的管理,減小同步需求。數組
4)原子變量,擁有最小化同步特性,避免內存一致性問題。安全
5)ThreadLockRandom,提供多線程中的僞隨機數生成。
同步代碼依賴於簡單的可重入鎖(synchronized),但有不少限制。java.util.concurrent.locks包中的最基本接口:Lock接口。Lock對象和同步代碼的外部鎖相似,每次只能一個線程擁有一個Lock對象,一樣支持
wait和notify機制。Lock對象對比內部鎖最大優點是可從獲取鎖的嘗試中退出(當鎖當前不可用或超時,tryLock方法退出;當另外一個線程的這個鎖得到前發送中斷,則lockInterruptibly方法會退出。
(進一步瞭解?)
讓咱們使用 Lock 對象來解決咱們在活躍度中見到的死鎖問題。Alphonse 和 Gaston 已經把本身訓練成能注意到朋友什麼時候要鞠躬。咱們經過要求 Friend 對象在雙方鞠躬前必須先得到鎖來模擬此次改善。下面是改善後模型的源代碼 Safelock :
public class Safelock { static class Friend { private final String name; private final Lock lock = new ReentrantLock(); public Friend(String name) { this.name = name; } public String getName() { return this.name; } public boolean impendingBow(Friend bower) { Boolean myLock = false; Boolean yourLock = false; try { myLock = lock.tryLock(); yourLock = bower.lock.tryLock(); } finally { if (!(myLock && yourLock)) { if (myLock) { lock.unlock(); } if (yourLock) { bower.lock.unlock(); } } } return myLock && yourLock; } public void bow(Friend bower) { if (impendingBow(bower)) { try { System.out.format("%s: %s has" + " bowed to me!%n", this.name, bower.getName()); bower.bowBack(this); } finally { lock.unlock(); bower.lock.unlock(); } } else { System.out.format( "%s: %s started" + " to bow to me, but saw that" + " I was already bowing to" + " him.%n", this.name, bower.getName()); } } public void bowBack(Friend bower) { System.out.format("%s: %s has" + " bowed back to me!%n", this.name, bower.getName()); } } static class BowLoop implements Runnable { private Friend bower; private Friend bowee; public BowLoop(Friend bower, Friend bowee) { this.bower = bower; this.bowee = bowee; } public void run() { Random random = new Random(); for (;;) { try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { } bowee.bow(bower); } } } public static void main(String[] args) { final Friend alphonse = new Friend("Alphonse"); final Friend gaston = new Friend("Gaston"); new Thread(new BowLoop(alphonse, gaston)).start(); new Thread(new BowLoop(gaston, alphonse)).start(); } }
前面例子中,新線程完成的任務(如runnable對象定義的)和這個線程自己(如一個線程對象所定義的)間有很強聯繫。這種方式不適合大型應用。大型應用中,把線程管理和應用的其餘部分的建立分開,封裝
這些功能的對象即執行器(executor)
1)執行器接口
java.util.concurrent包中定義了三種執行器接口:
1.1)Executor,支持運行新任務的簡單接口
1.2)ExecutorService,Executor的子接口,爲幫助管理單個任務和執行器本身的生命週期添加了某些功能
1.3)ScheduleExecutorService,ExecutorService的子接口,支持任務的定時執行等。
引用執行器對象的變量會以這三種接口中的一種形式聲明,沒有執行器類這種類型。
2)Executor接口
接口只有一個 execute 方法,用來替代一般建立(啓動)線程的方法。例如:r 是一個 Runnable 對象,e 是一個 Executor 對象。可使用
e.execute(r);
代替
(new Thread(r)).start();
但 execute 方法沒有定義具體的實現方式。對於不一樣的 Executor 實現,execute 方法多是建立一個新線程並當即啓動,但更有多是使用已有的工做線程運行r,或者將 r放入到隊列中等待可用的工做線程。(咱們將在線程池一節中描述工做線程。)
3)ExecutorService 接口
在execute方法以外添加了一個submit方法。
3.1)submit 方法除了和 execute 方法同樣能夠接受 Runnable 對象做爲參數,還能夠接受 Callable 對象做爲參數。使用 Callable對象能夠能使任務返還執行的結果。
3.2)經過 submit 方法返回的 Future 對象能夠讀取 Callable 任務的執行結果,或是管理 Callable 任務和 Runnable 任務的狀態。 ExecutorService 也提供了批量運行 Callable 任務的方法。
3.3)最後,ExecutorService 還提供了一些關閉執行器的方法。若是須要支持即時關閉,執行器所執行的任務須要正確處理中斷。
4)ScheduleExecutorService 接口
ScheduledExecutorService 擴展 ExecutorService接口並添加了 schedule 方法。
4.1)調用 schedule 方法能夠在指定的延時後執行一個Runnable 或者 Callable 任務。
4.2)ScheduledExecutorService 接口還定義了按照指定時間間隔按期執行任務的 scheduleAtFixedRate 方法和 scheduleWithFixedDelay 方法。
5)線程池
大部分執行器實現都使用由工做者線程(worker threads)組成的線程池(thread pools),這種類型的線程獨立於它執行的Runnable和callable任務存在,而且經常使用來執行多個任務。
使用工做線程可使建立線程的開銷最小化。在大規模併發應用中,建立大量的 Thread 對象會佔用佔用大量系統內存,分配和回收這些對象會產生很大的開銷。
5.1)一種最多見的線程池是固定大小的線程池(fixed thread pool)。這種線程池始終有必定數量的線程在運行,若是一個線程因爲某種緣由終止運行了,線程池會自動建立一個新的線程來代替它。須要執行的任務經過一個內部隊列提交給線程,當沒有更多的工做線程能夠用來執行任務時,隊列保存額外的任務。
使用固定大小的線程池一個很重要的好處是能夠實現優雅退化(degrade gracefully)。例如一個 Web 服務器,每個 HTTP 請求都是由一個單獨的線程來處理的,若是爲每個 HTTP 都建立一個新線程,那麼當系統的開銷超出其能力時,會忽然地對全部請求都中止響應。若是限制 Web 服務器能夠建立的線程數量,那麼它就沒必要當即處理全部收到的請求,而是在有能力處理請求時才處理。
5.2) 建立一個使用線程池的執行器最簡單的方法是調用 java.util.concurrent.Executors 的 newFixedThreadPool 方法。Executors 類還提供了下列一下方法:
若是上面的方法都不知足須要,能夠嘗試
java.util.concurrent.ThreadPoolExecutor 或者java.util.concurrent.ScheduledThreadPoolExecutor。
(關於四種方法區別待整理?)
6)fork/join
6.1) 該框架是 JDK 7 中引入的併發框架。fork/join 框架是 ExecutorService 接口的一種具體實現,目的是爲了幫助你更好地利用多處理器帶來的好處。它是爲那些可以被遞歸地拆解成子任務的工做類型量身設計的。其目的在於可以使用全部可用的運算能力來提高你的應用的性能。
6.2) fork/join 框架會將任務分發給線程池中的工做線程。fork/join 框架的獨特之處在與它使用工做竊取(work-stealing)算法。完成本身的工做而處於空閒的工做線程可以從其餘仍然處於忙碌(busy)狀態的工做線程處竊取等待執行的任務。
6.3) fork/join 框架的核心是 ForkJoinPool 類,它是對 AbstractExecutorService 類的擴展。ForkJoinPool 實現了工做竊取算法,並能夠執行 ForkJoinTask 任務。
6.3.1)基本用法:
第一步編寫執行部分工做的代碼,相似於:
if (當前這個任務工做量足夠小)
直接完成這個任務
else
將這個任務或這部分工做分解成兩個部分
分別觸發(invoke)這兩個子任務的執行,並等待結果
將該代碼封裝稱一個ForkJoinTask子類,一般狀況下會使用一種更爲具體的的類型,或者是 RecursiveTask(會返回一個結果),或者是 RecursiveAction。 當你的 ForkJoinTask 子類準備好了,建立一個表明全部須要完成工做的對象,而後將其做爲參數傳遞給一個ForkJoinPool 實例的 invoke() 方法便可。
6.3.2) 模糊操做
想要了解 fork/join 框架的基本工做原理,接下來的這個例子會有所幫助。假設你想要模糊一張圖片。原始的 source 圖片由一個整數的數組表示,每一個整數表示一個像素點的顏色數值。與 source 圖片相同,模糊以後的 destination 圖片也由一個整數數組表示。 對圖片的模糊操做是經過對 source 數組中的每個像素點進行處理完成的。處理的過程是這樣的:將每一個像素點的色值取出,與周圍像素的色值(紅、黃、藍三個組成部分)放在一塊兒取平均值,獲得的結果被放入 destination 數組。由於一張圖片會由一個很大的數組來表示,這個流程會花費一段較長的時間。若是使用 fork/join 框架來實現這個模糊算法,你就可以藉助多處理器系統的並行處理能力。下面是上述算法結合 fork/join 框架的一種簡單實現:
public class ForkBlur extends RecursiveAction { private int[] mSource; private int mStart; private int mLength; private int[] mDestination; // Processing window size; should be odd. private int mBlurWidth = 15; public ForkBlur(int[] src, int start, int length, int[] dst) { mSource = src; mStart = start; mLength = length; mDestination = dst; } protected void computeDirectly() { int sidePixels = (mBlurWidth - 1) / 2; for (int index = mStart; index < mStart + mLength; index++) { // Calculate average. float rt = 0, gt = 0, bt = 0; for (int mi = -sidePixels; mi <= sidePixels; mi++) { int mindex = Math.min(Math.max(mi + index, 0), mSource.length - 1); int pixel = mSource[mindex]; rt += (float)((pixel & 0x00ff0000) >> 16) / mBlurWidth; gt += (float)((pixel & 0x0000ff00) >> 8) / mBlurWidth; bt += (float)((pixel & 0x000000ff) >> 0) / mBlurWidth; } // Reassemble destination pixel. int dpixel = (0xff000000 ) | (((int)rt) << 16) | (((int)gt) << 8) | (((int)bt) << 0); mDestination[index] = dpixel; } } ...
接下來你須要實現父類中的 compute() 方法,它會直接執行模糊處理,或者將當前的工做拆分紅兩個更小的任務。數組的長度能夠做爲一個簡單的閥值來判斷任務是應該直接完成仍是應該被拆分。
protected static int sThreshold = 100000; protected void compute() { if (mLength < sThreshold) { computeDirectly(); return; } int split = mLength / 2; invokeAll(new ForkBlur(mSource, mStart, split, mDestination), new ForkBlur(mSource, mStart + split, mLength - split, mDestination)); }
若是前面這個方法是在一個 RecursiveAction 的子類中,那麼設置任務在ForkJoinPool 中執行就再直觀不過了。一般會包含如下一些步驟:
建立一個表示全部須要完成工做的任務。
// source image pixels are in src
// destination image pixels are in dst
ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
建立將要用來執行任務的 ForkJoinPool。
ForkJoinPool pool = new ForkJoinPool();
執行任務。
pool.invoke(fb);
6.3.3)標準實現
除了可以使用 fork/join 框架來實現可以在多處理系統中被並行執行的定製化算法(如前文中的 ForkBlur.java 例子),在 Java SE 中一些比較經常使用的功能點也已經使用 fork/join 框架來實現了。在 Java SE 8 中,java.util.Arrays 類的一系列parallelSort() 方法就使用了 fork/join 來實現。這些方法與 sort() 方法很相似,可是經過使用 fork/join框 架,藉助了併發來完成相關工做。在多處理器系統中,對大數組的並行排序會比串行排序更快。這些方法到底是如何運用 fork/join 框架並不在本教程的討論範圍內。想要了解更多的信息,請參見 Java API 文檔。 其餘採用了 fork/join 框架的方法還包括java.util.streams包中的一些方法,此包是做爲 Java SE 8 發行版中 Project Lambda 的一部分。想要了解更多信息,請參見 Lambda 表達式一節。
併發集合簡化了大型數據集合管理,且極大的減小了同步的需求。
java.util.concurrent 包囊括了 Java 集合框架的一些附加類。它們也最容易按照集合類所提供的接口來進行分類:
全部這些集合,經過在集合裏新增對象和訪問或移除對象的操做之間,定義一個happens-before 的關係,來幫助程序員避免內存一致性錯誤。
java.util.concurrent.atomic 包定義了對單一變量進行原子操做的類。全部的類都提供了 get 和 set 方法,可使用它們像讀寫 volatile 變量同樣讀寫原子類。就是說,同一變量上的一個 set 操做對於任意後續的 get 操做存在 happens-before 關係。原子的 compareAndSet 方法也有內存一致性特色,就像應用到整型原子變量中的簡單原子算法。
爲了看看這個包如何使用,讓咱們返回到最初用於演示線程干擾的 Counter 類:
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
使用同步是一種使 Counter 類變得線程安全的方法,如 SynchronizedCounter:
class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
對於這個簡單的類,同步是一種可接受的解決方案。可是對於更復雜的類,咱們可能想要避免沒必要要同步所帶來的活躍度影響。將 int 替換爲 AtomicInteger 容許咱們在不進行同步的狀況下阻止線程干擾,如 AtomicCounter:
import java.util.concurrent.atomic.AtomicInteger; class AtomicCounter { private AtomicInteger c = new AtomicInteger(0); public void increment() { c.incrementAndGet(); } public void decrement() { c.decrementAndGet(); } public int value() { return c.get(); } }
併發隨機數(JDK7)提供了高效的多線程生成僞隨機數的方法。
在 JDK7 中,java.util.concurrent 包含了一個至關便利的類 ThreadLocalRandom,能夠在當應用程序指望在多個線程或 ForkJoinTasks 中使用隨機數時使用。
對於併發訪問,使用 TheadLocalRandom 代替 Math.random() 能夠減小競爭,從而得到更好的性能。
你只需調用 ThreadLocalRandom.current(), 而後調用它的其中一個方法去獲取一個隨機數便可。下面是一個例子:
int r = ThreadLocalRandom.current() .nextInt(4, 77);
參考:
編程要點:https://github.com/waylau/essential-java
文中內容參考:https://blog.csdn.net/kkkloveyou/article/details/50561269