一、并行流與并行排序
Java 8中可以在接口不變的情況下,將流改為并行流,方便在多線程中進(jìn)行集合中的數(shù)據(jù)處理。
1.1 使用并行流過(guò)濾數(shù)據(jù)
下面示例統(tǒng)計(jì)1~1000000內(nèi)所有質(zhì)數(shù)的數(shù)量。下面是一個(gè)判斷質(zhì)數(shù)的函數(shù):
public class PrimeUtil {
public static boolean isPrime(int number) {
int tmp = number;
if(tmp<2) {
return false;
}
for(int i=2; Math.sqrt(tmp)>=i; i++) {
if(tmp%i==0) {
return false;
}
}
return true;
}
}
接著,使用函數(shù)式編程統(tǒng)計(jì)給定范圍內(nèi)所有的質(zhì)數(shù):
IntStream.range(1,100000).filter(PrimeUtil::isPrime).count();
上述代碼是串行的,將它改造成并行計(jì)算非常簡(jiǎn)單,只需要將流并行化即可:
IntStream.range(1,100000).parallel.filter(PrimeUtil::isPrime).count();
上述代碼中,首先parallel()方法得到一個(gè)并行流,接著,在并行流上進(jìn)行過(guò)濾,此時(shí),PrimeUtil.isPrime()函數(shù)會(huì)被多線程并發(fā)調(diào)用,應(yīng)用于流中的所有元素。
1.2 從集合得到并行流
在函數(shù)式編程中,可以從集合得到一個(gè)流或者并行流。下面這段代碼試圖統(tǒng)計(jì)集合內(nèi)所有學(xué)生的平均分:
List<Student> ss = new ArrayList<Student>();
double ave = ss.stream().mapToInt(s->s.score).average().getAsDouble();
從集合對(duì)象List中,我們使用stream()方法可以得到一個(gè)流。如果希望將這段代碼并行化,則可以使用parallelStream()函數(shù)。
double ave = ss.parallelStream().mapToInt(s->s.score).average().getAsDouble();
1.3 并行排序
在Java 8中,可以使用新增的Arrays.parallelSort()方法直接使用并行排序。
比如,可以這樣使用:
int[] arr = new int[1000000];
Arrays.parallelSort(arr);
除了并行排序外,Arrays中還增加了一些API用于數(shù)組中的數(shù)據(jù)的賦值,比如:
public static void setAll(int[] arr, IntUnaryOperator generator)
這是一個(gè)函數(shù)式味道很濃的接口,它的第2個(gè)參數(shù)是一個(gè)函數(shù)式接口。如果想給數(shù)組中每一個(gè)元素都附上一個(gè)隨機(jī)值,可以這么做:
Random r = new Random();
Arrays.setAll(arr, r.nextInt());
以上過(guò)程是串行的。只要使用setAll()對(duì)應(yīng)的并行版本,就可以將它執(zhí)行在多個(gè)CPU上:
Random r = new Random();
Arrays.parallelSetAll(arr, r.nextInt());
二、增強(qiáng)的Future:CompletableFuture
CompletableFuture是Java 8新增的一個(gè)超大型工具類。它實(shí)現(xiàn)了Future接口,也實(shí)現(xiàn)了CompletionStage接口。CompletionStage接口擁有多達(dá)40種方法,是為了函數(shù)式編程中的流式調(diào)用準(zhǔn)備的。通過(guò)CompletionStage提供的接口,可以在一個(gè)執(zhí)行結(jié)果上進(jìn)行多次流式調(diào)用,以此可以得到最終結(jié)果。比如,可以在一個(gè)CompletionStage上進(jìn)行如下調(diào)用:
stage.thenApply(x -> square(x)).thenAccept(x -> System.out.println(x)).thenRun(() -> System.out.println())
這一連串的調(diào)用就會(huì)挨個(gè)執(zhí)行。
2.1 完成了就通知我
CompletableFuture和Future一樣,可以作為函數(shù)調(diào)用的契約。如果向CompletableFuture請(qǐng)求一個(gè)數(shù)據(jù),如果數(shù)據(jù)還沒(méi)有準(zhǔn)備好,請(qǐng)求線程就會(huì)等待。通過(guò)CompletableFuture,可以手動(dòng)設(shè)置CompletableFuture的完成狀態(tài)。
//義了一個(gè)AskThread線程。它接收一個(gè)CompletableFuture作為其構(gòu)造函數(shù),
//它的任務(wù)是計(jì)算CompletableFuture表示的數(shù)字的平方,并將其打印。
public static class AskThread implements Runnable {
CompletableFuture<Integer> re = null;
public AskThread(CompletableFuture<Integer> re) {
this.re = re;
}
@Override
public void run() {
int myRe = 0;
try {
//此時(shí)阻塞,因?yàn)镃ompletableFuture中根本沒(méi)有它所需要的數(shù)據(jù),整個(gè)CompletableFuture處于未完成狀態(tài)
myRe = re.get() * re.get();
} catch(Exception e) {
}
System.out.println(myRe);
}
}
public static void main(String[] args) throws InterruptedException {
//創(chuàng)建一個(gè)CompletableFuture對(duì)象實(shí)例,將這個(gè)對(duì)象實(shí)例傳遞給AskThread線程,并啟動(dòng)這個(gè)線程
final CompletableFuture<Integer> future = new CompletableFuture<>();
new Thread(new AskThread(future)).start();
//模擬長(zhǎng)時(shí)間的計(jì)算過(guò)程
Thread.sleep(1000);
//將最終數(shù)據(jù)載入CompletableFuture,并標(biāo)記為完成狀態(tài)
//告知完成結(jié)果
future.complete(60);
}
2.2 異步執(zhí)行任務(wù)
通過(guò)CompletableFuture提供的進(jìn)一步封裝,很容易實(shí)現(xiàn)Future模式那樣的異步調(diào)用。比如:
public static Integer calc(Integer para) {
try {
//模擬一個(gè)長(zhǎng)時(shí)間執(zhí)行
Thread.sleep(1000);
} catch(InterruptedException e) {
}
return para*para;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
final CompletableFuture<Integer> future = new CompletableFuture.supplyAsync(() -> calc(50));
System.out.println(future.get());
}
上述代碼中,使用CompletableFuture.supplyAsync()方法構(gòu)造一個(gè)CompletableFuture實(shí)例,在supplyAsync函數(shù)中,它會(huì)在一個(gè)新的線程中,執(zhí)行傳入的參數(shù)。在這里,它會(huì)執(zhí)行calc()方法。而calc()方法的執(zhí)行可能是比較慢的,但是這不影響CompletableFuture實(shí)例的構(gòu)造速度,因此supplyAsync()會(huì)立即返回,它返回CompletableFuture對(duì)象實(shí)例就可以作為這次調(diào)用的契約,在將來(lái)任何場(chǎng)合,用于獲得最終的計(jì)算結(jié)果。最后一行代碼試圖獲得calc()的計(jì)算結(jié)果,如果當(dāng)前計(jì)算沒(méi)有完成,則調(diào)用get()方法的線程就會(huì)等待。
在CompletableFuture中,類似的工廠方法有以下幾個(gè):
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
static CompletableFuture<Void> runAsync(Runnable runnable);
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
其中supplyAsync()方法用于那些需要有返回值的場(chǎng)景,比如計(jì)算某個(gè)數(shù)據(jù)等。而runAsync()方法用于沒(méi)有返回值的場(chǎng)景,比如,僅僅是簡(jiǎn)單地執(zhí)行某一個(gè)異步動(dòng)作。
在這兩對(duì)方法中,都有一個(gè)方法可以接收一個(gè)Executor參數(shù)。這就使我們讓Supplier<U>
或者Runnable在指定的線程池中工作。如果不指定,則在默認(rèn)的系統(tǒng)公共的ForkJoinPool.common線程池中執(zhí)行(在Java 8中,新增了ForkJoinPool.commonPool()方法。它可以獲得一個(gè)公共ForkJoin線程池。這個(gè)公共的線程池中的所有線程都是Daemon線程。這意味著如果主線程退出,這些線程無(wú)論是否執(zhí)行完畢,都會(huì)退出系統(tǒng))。
2.3 流式調(diào)用
CompletionStage的約40個(gè)接口是為函數(shù)式編程做準(zhǔn)備的。在這里,看一下如何使用這些接口進(jìn)行函數(shù)式的流式API調(diào)用:
public static Integer calc(Integer para) {
try {
//模擬一個(gè)長(zhǎng)時(shí)間執(zhí)行
Thread.sleep(1000);
} catch(InterruptedException e) {
}
return para*para;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> calc(50))
.thenApply((i)->Integer.toString(i))
.thenApply((str)->"\"" +str + "\"")
.thenAccept(System.out::println);
future.get();
}
上述代碼中,使用supplyAsync()函數(shù)執(zhí)行一個(gè)異步任務(wù)。接著連續(xù)使用流式調(diào)用對(duì)任務(wù)的處理結(jié)果進(jìn)行再加工,直到最后的結(jié)果輸出。
這里,執(zhí)行CompletableFuture.get()方法,目的是等待calc()函數(shù)執(zhí)行完成。不過(guò)不進(jìn)行這個(gè)等待調(diào)用,由于CompletableFuture異步執(zhí)行的緣故,主函數(shù)不等calc()方法執(zhí)行完畢就會(huì)退出,隨著主線程的結(jié)束,所有的Daemon線程都會(huì)立即退出,從而導(dǎo)致calc()方法無(wú)法正常完成。
2.4 CompletableFuture中的異常處理
如果CompletableFuture在執(zhí)行過(guò)程中遇到異常,我們可以用函數(shù)式編程的風(fēng)格處理這些異常。CompletableFuture提供了一個(gè)異常處理方法exceptionally():
public static Integer calc(Integer para) {
return para/0;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
final CompletableFuture<Void> future = CompletableFuture
.supplyAsync(() -> calc(50))
//對(duì)當(dāng)前的CompletableFuture進(jìn)行異常處理
.exceptionally(ex-> {
System.out.println(ex.toString());
return 0;
})
.thenApply((i)->Integer.toString(i))
.thenApply((str)->"\"" +str + "\"")
.thenAccept(System.out::println);
future.get();
}
2.5 組合多個(gè)CompletableFuture
CompletableFuture還允許將多個(gè)CompletableFuture進(jìn)行組合。一種方法是使用thenCompose(),它的簽名如下:
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)
一個(gè)CompletableFuture可以在執(zhí)行完成后,將執(zhí)行結(jié)果通過(guò)Function傳遞給下一個(gè)CompletionStage進(jìn)行處理(Function接口返回新的CompletionStage實(shí)例):
public static Integer calc(Integer para) {
return para/2;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> calc(50))
.thenCompose((i)->CompletableFuture.supplyAsync(()->calc(i)))
.thenApply((str)->"\"" +str + "\"")
.thenAccept(System.out::println);
future.get();
}
上述代碼中,將處理后的結(jié)果傳遞給thenCompose(),并進(jìn)一步傳遞給后續(xù)新生成的CompletableFuture實(shí)例,以上代碼的輸出如下:
"12"
另外一種組合多個(gè)CompletableFuture的方法是thenCombine(),它的簽名如下:
public <U,V> CompletableFuture<U> thenCombime
(CompletionStage<? extends U> other,
BiFunction<? super T, ? super U,? extends V> fn)
方法thenCombime()首先完成當(dāng)前CompletableFuture和other的執(zhí)行。接著,將這兩者的執(zhí)行結(jié)果傳遞給BiFunction(該接口接收兩個(gè)參數(shù),并有一個(gè)返回值),并返回代表BiFunction實(shí)例的CompletableFuture對(duì)象:
public static Integer calc(Integer para) {
return para/2;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Integer> intFuture = CompletableFuture.supplyAsync(() -> calc(50));
CompletableFuture<Integer> intFuture2 = CompletableFuture.supplyAsync(() -> calc25));
CompletableFuture<Void> future = intFuture.thenCombine(intFuture2, (i,j) -> (i+j))
.thenApply((str)->"\"" +str + "\"")
.thenAccept(System.out::println);
future.get();
}
上述代碼中,首先生成兩個(gè)CompletableFuture實(shí)例,接著使用thenCombine()組合這兩個(gè)CompletableFuture,將兩者的執(zhí)行結(jié)果進(jìn)行累加,并將其累加結(jié)果轉(zhuǎn)為字符串,并輸出,上述代碼的輸出是:
"37"
三、讀寫鎖的改進(jìn):StampedLock
StamppedLock是Java 8中引入的一種新的鎖機(jī)制。讀寫鎖雖然分離了讀和寫的功能,使得讀與讀之間可以完全并發(fā)。但是,讀和寫之間依然是沖突的。讀鎖會(huì)完全阻塞寫鎖,它使用的依然是悲觀鎖的策略,如果有大量的讀線程,它也有可能引起寫線程的“饑餓”。而StampedLock提供了一種樂(lè)觀的讀策略。這種樂(lè)觀策略的鎖非常類似無(wú)鎖的操作,使得樂(lè)觀鎖完全不會(huì)阻塞寫線程。
3.1 StampedLock使用示例
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
//使用writeLock()函數(shù)可以申請(qǐng)寫鎖
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
//試圖嘗試一次樂(lè)觀讀
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
//判斷這個(gè)stamp是否在讀過(guò)程發(fā)生期間被修改過(guò)
if (!sl.validate(stamp)) {
//使用readLock()獲得悲觀的讀鎖
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
上述代碼出自JDK的官方文檔。它定義了一個(gè)Point類,內(nèi)部有兩個(gè)元素x和y,表示點(diǎn)的坐標(biāo)。第3行定義了StampedLock鎖。第15行定義的distanceFromOrigin()方法是一個(gè)只讀方法,它只會(huì)讀取Point的x和y坐標(biāo)。在讀取時(shí),首先使用了StampedLock.tryOptimisticRead()方法。這個(gè)方法表示試圖嘗試一次樂(lè)觀讀。它會(huì)返回一個(gè)類似于時(shí)間的郵戳整數(shù)stamp。這個(gè)stamp就可以作為這一次鎖獲取的憑證。
接著,在第17行,讀取x和y的值。當(dāng)然,這時(shí)并不確定這個(gè)x和y是否是一致的(在讀取x的時(shí)候,可能其他線程改寫了y的值,使得currentX和currentY處于不一致的狀態(tài))。因此,我們必須在18行,使用validate()方法,判斷這個(gè)stamp是否在讀過(guò)程發(fā)生期間被修改過(guò)。如果stamp沒(méi)有被修改過(guò),則認(rèn)為這次讀取的過(guò)程中,可能被其他線程改寫了數(shù)據(jù),因此,有可能出現(xiàn)了臟讀。如果出現(xiàn)這種情況,我們可以像處理CAS操作那樣在一個(gè)死循環(huán)中一直使用樂(lè)觀讀,直到成功為止。
也可以升級(jí)鎖的級(jí)別。在本例中,我們升級(jí)樂(lè)觀鎖的級(jí)別,將樂(lè)觀鎖變?yōu)楸^鎖。在第19行,當(dāng)判斷樂(lè)觀讀失敗后,使用readLock()獲得悲觀的讀鎖,并進(jìn)一步讀取數(shù)據(jù)。如果當(dāng)前對(duì)象正在被修改,則讀鎖的申請(qǐng)可能導(dǎo)致線程掛起。
寫入的情況可以參考第5行定義的move()函數(shù)。使用writeLock()函數(shù)可以申請(qǐng)寫鎖。這里的含義和讀寫鎖是類似的。
在退出臨界區(qū)時(shí),不要忘記釋放寫鎖(第11行)或者讀鎖(第24行)。
3.2 StampedLock的小陷阱
StampedLock內(nèi)部實(shí)現(xiàn)時(shí),使用類似于CAS操作的死循環(huán)反復(fù)嘗試的策略。在它掛起線程時(shí),使用的是Unsafe.park()函數(shù),而park()函數(shù)在遇到線程中斷時(shí),會(huì)直接返回(不同于Thread.sleep(),它不會(huì)拋出異常)。而在StampedLock的死循環(huán)邏輯中,沒(méi)有處理有關(guān)中斷的邏輯。因此,這就會(huì)導(dǎo)致阻塞在park()上的線程被中斷后,會(huì)再次進(jìn)入循環(huán)。而當(dāng)退出條件得不到滿足時(shí),就會(huì)發(fā)生瘋狂占用CPU的情況。下面演示了這個(gè)問(wèn)題:
public class StampedLockCUPDemo {
static Thread[] holdCpuThreads = new Thread[3];
static final StampedLock lock = new StampedLock();
public static void main(String[] args) throws InterruptedException {
new Thread() {
public void run(){
long readLong = lock.writeLock();
LockSupport.parkNanos(6100000000L);
lock.unlockWrite(readLong);
}
}.start();
Thread.sleep(100);
for( int i = 0; i < 3; ++i) {
holdCpuThreads [i] = new Thread(new HoldCPUReadThread());
holdCpuThreads [i].start();
}
Thread.sleep(10000);
for(int i=0; i<3; i++) {
holdCpuThreads [i].interrupt();
}
}
private static class HoldCPUReadThread implements Runnable {
public void run() {
long lockr = lock.readLock();
System.out.println(Thread.currentThread().getName() + " get read lock");
lock.unlockRead(lockr);
}
}
}
在上述代碼中,首先開啟線程占用寫鎖(第7行),為了演示效果,這里使用寫線程不釋放鎖而一直等待。接著,開啟3個(gè)讀線程,讓它們請(qǐng)求讀鎖。此時(shí),由于寫鎖的存在,所有讀線程都會(huì)被最終掛起。讀線程因?yàn)?park() 的操作進(jìn)入了等待狀態(tài),這種情況是正常的。
而在10秒鐘以后(代碼在17行執(zhí)行了10秒等待),系統(tǒng)中斷了這3個(gè)讀線程,之后,就會(huì)發(fā)現(xiàn),CPU占用率極有可能會(huì)飆升。這是因?yàn)橹袛鄬?dǎo)致 park() 函數(shù)返回,使線程再次進(jìn)入運(yùn)行狀態(tài)。
此時(shí),這個(gè)線程的狀態(tài)是RUNNABLE,這是我們不愿意看到的,它會(huì)一直存在并耗盡CPU資源,直到自己搶占到了鎖。
四、原子類的增強(qiáng)
無(wú)鎖的原子類操作使用系統(tǒng)的CAS指令,有著遠(yuǎn)遠(yuǎn)超越鎖的性能。在Java 8中引入了LongAddr類,這個(gè)類也在java.util.concurrent.atomic包下,因此,它也是使用了CAS指令。
4.1 更快的原子類:LongAddr
AtomicInteger的基本實(shí)現(xiàn)機(jī)制,它們都是在一個(gè)死循環(huán)內(nèi),不斷嘗試修改目標(biāo)值,知道修改成功。如果競(jìng)爭(zhēng)不激烈,那么修改成功的概率就很高,否則,修改失敗的概率就很高。在大量修改失敗時(shí),這些原子操作就會(huì)進(jìn)行多次循環(huán)嘗試,因此性能會(huì)受到影響。
當(dāng)競(jìng)爭(zhēng)激烈的時(shí)候,為了進(jìn)一步提高系統(tǒng)的性能,一種基本方案就是可以使用熱點(diǎn)分離,將競(jìng)爭(zhēng)的數(shù)據(jù)進(jìn)行分解,基于這個(gè)思路,可以想到一種對(duì)傳統(tǒng)AtomicInteger等原子類的改進(jìn)方法。雖然在CAS操作中沒(méi)有鎖,但是像減小鎖粒度這種分離熱點(diǎn)的思想依然可以使用。一種可行的方案就是仿造ConcurrentHashMap,將熱點(diǎn)數(shù)據(jù)分離。比如,可以將AtomicInteger的內(nèi)部核心數(shù)據(jù)value分離成一個(gè)數(shù)組,每個(gè)線程訪問(wèn)時(shí),通過(guò)哈希等算法映射到其中一個(gè)數(shù)字進(jìn)行計(jì)算,而最終的計(jì)算結(jié)果,則為這個(gè)數(shù)組的求和累加。熱點(diǎn)value被分離成多個(gè)單元cell,每個(gè)cell獨(dú)自維護(hù)內(nèi)部的值,當(dāng)前對(duì)象的實(shí)際值由所有的cell累計(jì)合成,這樣,熱點(diǎn)就進(jìn)行了有效的分離,提高了并行度。LongAddr正是使用了這種思想。
在實(shí)際的操作中,LongAddr并不會(huì)一開始就動(dòng)用數(shù)組進(jìn)行處理,而是將所有數(shù)據(jù)都先記錄在一個(gè)稱為base的變量中。如果在多線程條件下,大家修改base都沒(méi)有沖突,那么也沒(méi)有必要擴(kuò)展為cell數(shù)組。但是,一旦base修改發(fā)生沖突,就會(huì)初始化cell數(shù)組,使用新的策略。如果使用cell數(shù)組更新后,發(fā)現(xiàn)在某一個(gè)cell上的更新依然發(fā)生沖突,那么系統(tǒng)就會(huì)嘗試創(chuàng)建新的cell,或者將cell的數(shù)量加倍,以減少?zèng)_突的可能。
下面簡(jiǎn)單分析一下 increment() 方法(該方法會(huì)將LongAddr自增1)的內(nèi)部實(shí)現(xiàn):
public void increment() {
add(1L);
}
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
//如果cell表為null,會(huì)嘗試將x累加到base上。
if ((as = cells) != null || !casBase(b = base, b + x)) {
/*
* 如果cell表不為null或者嘗試將x累加到base上失敗,執(zhí)行以下操作。
* 如果cell表不為null且通過(guò)當(dāng)前線程的probe值定位到的cell表中的Cell不為null。
* 那么嘗試?yán)奂觴到對(duì)應(yīng)的Cell上。
*/
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
//或者cell表為null,或者定位到的cell為null,或者嘗試失敗,都會(huì)調(diào)用下面的Striped64中定義的longAccumulate方法。
longAccumulate(x, null, uncontended);
}
}
它的核心是 addd() 方法。最開始cells為null,因此數(shù)據(jù)會(huì)向base增加。但是如果對(duì)base的操作沖突,則會(huì)設(shè)置沖突標(biāo)記uncontended 為true。接著,如果判斷cells數(shù)組不可用,或者當(dāng)前線程對(duì)應(yīng)的cell為null,則直接進(jìn)入 longAccumulate() 方法。否則會(huì)嘗試使用CAS方法更新對(duì)應(yīng)的cell數(shù)據(jù),如果成功,則退出,失敗則進(jìn)入 longAccumulate() 方法。
由于 longAccumulate() 方法的大致內(nèi)容是,根據(jù)需要?jiǎng)?chuàng)建新的cell或者對(duì)cell數(shù)組進(jìn)行擴(kuò)容,以減少?zèng)_突。
下面,簡(jiǎn)單地對(duì)LongAddr、原子類以及同步鎖進(jìn)行性能測(cè)試。測(cè)試方法使用多個(gè)線程對(duì)同一個(gè)整數(shù)進(jìn)行累加,觀察3中不同方法時(shí)所消耗的時(shí)間。首先,定義一些輔助變量:
private static final int MAX_THREADS = 3; //線程數(shù)
private static final int TASK_COUNT = 3; //任務(wù)書
private static final int TARGET_COUNT = 3; //線程數(shù)
private AtomicLong acount = new AtomicLong(0L); //無(wú)鎖的原子操作
private LongAddr lacount = new LongAddr();
private long count = 0;
static CountDownLatch cdlsync = new CountDownLatch(TASK_COUNT);
static CountDownLatch cdlatomic = new CountDownLatch(TASK_COUNT);
static CountDownLatch cdladdr = new CountDownLatch(TASK_COUNT);
上述代碼中,指定了測(cè)試線程數(shù)量、目標(biāo)總數(shù)以及3個(gè)初始化值為0的整型變量acount、lacount、count。它們分別表示使用AtomicLong、LongAddr和鎖進(jìn)行同步時(shí)的操作對(duì)象。下面是使用同步鎖時(shí)的測(cè)試代碼:
protected synchronized long inc() {
return ++count;
}
protected synchronized long getCount() {
return count;
}
public class SyncThread implements Runnable {
protected String name;
protected long starttime;
LongAddrDemo out;
public SyncThread(LongAddrDemo o, long starttime) {
out = o;
this.starttime = starttime;
}
@Override
public void run() {
long v = out.getCount();
while(v<TARGET_COUNT) {
v = out.inc();
}
long endtime = System.currentTimeMills();
System.out.println("SyncThread spend:" + (endtime - starttime) + "ms" + " v=" + v);
cdlsync.countDown();
}
}
public void testSync() throws InterruptedException {
ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
long starttime = System.currentTimeMills();
SyncThread sync = new SyncThread(this, starttime);
for(int i=0; i<TASK_COUNT; i++) {
exe.submit(sync);
}
cdlsync.await();
exe.shutdown();
}
上述代碼,定義線程SyncThread,它使用加鎖方式增加count的值。在 testSync()方法中,使用線程池控制多線程進(jìn)行累加操作。使用類似的方法實(shí)現(xiàn)原子類累加計(jì)時(shí)統(tǒng)計(jì):
public class AtomicThread implements Runnable {
protected String name;
protected long starttime;
public AtomicThread(long starttime) {
this.starttime = starttime;
}
@Override
public void run() {
long v = acount.get();
while(v<TARGET_COUNT) {
v = acount.incrementAndGet();
}
long endtime = System.currentTimeMills();
System.out.println("AtomicThread spend:" + (endtime - starttime) + "ms" + " v=" + v);
cdlatomic.countDown();
}
}
public void testAtomic() throws InterruptedException {
ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
long starttime = System.currentTimeMills();
AtomicThread sync = new AtomicThread(starttime);
for(int i=0; i<TASK_COUNT; i++) {
exe.submit(atomic);
}
cdlatomic.await();
exe.shutdown();
}
同理,以下代碼使用LongAddr實(shí)現(xiàn)類似功能:
public class LongAddrThread implements Runnable {
protected String name;
protected long starttime;
public AtomicThread(long starttime) {
this.starttime = starttime;
}
@Override
public void run() {
long v = lacount.sum();
while(v<TARGET_COUNT) {
lacount.increment();
v = lacount.sum();
}
long endtime = System.currentTimeMills();
System.out.println(" LongAddrThread spend:" + (endtime - starttime) + "ms" + " v=" + v);
cdladdr.countDown();
}
}
public void testLongAddr() throws InterruptedException {
ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
long starttime = System.currentTimeMills();
LongAddrThread sync = new LongAddrThread(starttime);
for(int i=0; i<TASK_COUNT; i++) {
exe.submit(atomic);
}
cdladdr.await();
exe.shutdown();
}
注意,由于LongAddr中,將單個(gè)數(shù)值分解為多個(gè)不同的段。因此,在進(jìn)行累加后,上述代碼中increment()函數(shù)并不能返回當(dāng)前的數(shù)值。要取得當(dāng)前的實(shí)際值,需要使用 sum()函數(shù)重新計(jì)算。這個(gè)計(jì)算是需要有額外的成本的,但即使加上這個(gè)額外成本,LongAddr的表現(xiàn)還是比AtomicLong要好。
就計(jì)數(shù)性能而言,LongAddr已經(jīng)超越了普通的原子操作。LongAddr的另外一個(gè)優(yōu)化手段是避免了偽共存。LongAddr中并不是直接使用padding這種看起來(lái)比較礙眼的做法,而是引入了一種新的注釋@sun.misc.Contended
。
對(duì)于LongAddr中的每一個(gè)Cell,它的定義如下:
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value=x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
}
可以看到,在上述代碼第1行申明了Cell類為sun.misc.Contended。這將會(huì)使得Java虛擬機(jī)自動(dòng)為Cell解決偽共享問(wèn)題。
當(dāng)然,在我們自己的代碼中也可以使用sun.misc.Contended來(lái)解決偽共享問(wèn)題,但是需要額外使用虛擬機(jī)參數(shù)-XX:-RestrictContended,否則,這個(gè)注釋將被忽略。
4.2 LongAddr的功能增強(qiáng)版:LongAccumulator
LongAccumulator是LongAddr的親兄弟,它們有公共的父類Striped64。因此,LongAccumulator內(nèi)部的優(yōu)化方式和LongAddr是一樣的。它們都將一個(gè)long型整數(shù)進(jìn)行分割,存儲(chǔ)在不同的變量中,以防止多線程競(jìng)爭(zhēng)。兩者的主要邏輯類似,但是LongAccumulator是LongAddr的功能擴(kuò)展,對(duì)于LongAddr來(lái)說(shuō),它只是每次對(duì)給定的整數(shù)執(zhí)行一次加法,而LongAccumulator則可以實(shí)現(xiàn)任意函數(shù)慚怍。
可以使用下面的構(gòu)造函數(shù)創(chuàng)建一個(gè)LongAccumulator實(shí)例:
public LongAccumulator(LongBinaryOperator accumulatorFunction, long identify)
第一個(gè)參數(shù)accumulatorFunction就是需要執(zhí)行的二元函數(shù)(接收兩個(gè)long形參數(shù)并返回long),第2個(gè)參數(shù)是初始值。下面這個(gè)例子展示了LongAccurator的使用,它將通過(guò)多線程訪問(wèn)若干個(gè)整數(shù),并返回遇到的最大的那個(gè)數(shù)字。
public static void main(String[] args) throws Exception {
LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE);
Thread[] ts = new Thread[1000];
for(int i=0; i<1000; i++) {
ts[i] = new Thread(()->{
Random random = new Random();
long value = random.nextLong();
accumulator.accumulate(value);
});
ts[i].start();
}
for(int i=0; i<1000; i++) {
ts[1000].join();
}
System.out.println(accumulator .longValue);
}
上述代碼中,構(gòu)造了LongAccumulator實(shí)例。因?yàn)橐^(guò)濾最大值,因此傳入Long::max函數(shù)句柄。當(dāng)有數(shù)據(jù)通過(guò)accumulate()方法傳入LongAccumulator后,LongAccumulator會(huì)通過(guò)Long::max識(shí)別最大值并且保存在內(nèi)部(很可能是cell數(shù)組,也可能是base)。通過(guò)longValue()函數(shù)對(duì)所有的cell進(jìn)行Long::max操作,得到最大值。