Java 8 與并發(fā)(二)

一、并行流與并行排序

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操作,得到最大值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,312評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,410評(píng)論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,778評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,955評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,521評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,266評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,468評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,696評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評(píng)論 1 294
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,193評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,431評(píng)論 2 378

推薦閱讀更多精彩內(nèi)容