Java 8 与并发(二)

一、并行流与并行排序

Java 8中可以在接口不变的情况下,将流改为并行流,方便在多线程中进行集合中的数据处理。

1.1 使用并行流过滤数据

下面示例统计1~1000000内所有质数的数量。下面是一个判断质数的函数:

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;
    }
}

接着,使用函数式编程统计给定范围内所有的质数:

IntStream.range(1,100000).filter(PrimeUtil::isPrime).count();

上述代码是串行的,将它改造成并行计算非常简单,只需要将流并行化即可:

IntStream.range(1,100000).parallel.filter(PrimeUtil::isPrime).count();

上述代码中,首先parallel()方法得到一个并行流,接着,在并行流上进行过滤,此时,PrimeUtil.isPrime()函数会被多线程并发调用,应用于流中的所有元素。

1.2 从集合得到并行流

在函数式编程中,可以从集合得到一个流或者并行流。下面这段代码试图统计集合内所有学生的平均分:

List<Student> ss = new ArrayList<Student>();
double ave = ss.stream().mapToInt(s->s.score).average().getAsDouble();

从集合对象List中,我们使用stream()方法可以得到一个流。如果希望将这段代码并行化,则可以使用parallelStream()函数。

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用于数组中的数据的赋值,比如:

public static void setAll(int[] arr, IntUnaryOperator generator)

这是一个函数式味道很浓的接口,它的第2个参数是一个函数式接口。如果想给数组中每一个元素都附上一个随机值,可以这么做:

Random r = new Random();
Arrays.setAll(arr, r.nextInt());

以上过程是串行的。只要使用setAll()对应的并行版本,就可以将它执行在多个CPU上:

Random r = new Random();
Arrays.parallelSetAll(arr, r.nextInt());

二、增强的Future:CompletableFuture

CompletableFuture是Java 8新增的一个超大型工具类。它实现了Future接口,也实现了CompletionStage接口。CompletionStage接口拥有多达40种方法,是为了函数式编程中的流式调用准备的。通过CompletionStage提供的接口,可以在一个执行结果上进行多次流式调用,以此可以得到最终结果。比如,可以在一个CompletionStage上进行如下调用:

stage.thenApply(x -> square(x)).thenAccept(x -> System.out.println(x)).thenRun(() -> System.out.println())

这一连串的调用就会挨个执行。

2.1 完成了就通知我

CompletableFuture和Future一样,可以作为函数调用的契约。如果向CompletableFuture请求一个数据,如果数据还没有准备好,请求线程就会等待。通过CompletableFuture,可以手动设置CompletableFuture的完成状态。

//义了一个AskThread线程。它接收一个CompletableFuture作为其构造函数,
//它的任务是计算CompletableFuture表示的数字的平方,并将其打印。
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 {
            //此时阻塞,因为CompletableFuture中根本没有它所需要的数据,整个CompletableFuture处于未完成状态
            myRe = re.get() * re.get();
        } catch(Exception e) {
        }
        System.out.println(myRe);
    }
}

    public static void main(String[] args) throws InterruptedException {
        //创建一个CompletableFuture对象实例,将这个对象实例传递给AskThread线程,并启动这个线程
        final CompletableFuture<Integer> future = new CompletableFuture<>();
        new Thread(new AskThread(future)).start();
        //模拟长时间的计算过程
        Thread.sleep(1000);
        //将最终数据载入CompletableFuture,并标记为完成状态
        //告知完成结果
        future.complete(60);
}

2.2 异步执行任务

通过CompletableFuture提供的进一步封装,很容易实现Future模式那样的异步调用。比如:

public static Integer calc(Integer para) {
    try {
        //模拟一个长时间执行
        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()方法构造一个CompletableFuture实例,在supplyAsync函数中,它会在一个新的线程中,执行传入的参数。在这里,它会执行calc()方法。而calc()方法的执行可能是比较慢的,但是这不影响CompletableFuture实例的构造速度,因此supplyAsync()会立即返回,它返回CompletableFuture对象实例就可以作为这次调用的契约,在将来任何场合,用于获得最终的计算结果。最后一行代码试图获得calc()的计算结果,如果当前计算没有完成,则调用get()方法的线程就会等待。
在CompletableFuture中,类似的工厂方法有以下几个:

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()方法用于那些需要有返回值的场景,比如计算某个数据等。而runAsync()方法用于没有返回值的场景,比如,仅仅是简单地执行某一个异步动作。

在这两对方法中,都有一个方法可以接收一个Executor参数。这就使我们让Supplier<U>或者Runnable在指定的线程池中工作。如果不指定,则在默认的系统公共的ForkJoinPool.common线程池中执行(在Java 8中,新增了ForkJoinPool.commonPool()方法。它可以获得一个公共ForkJoin线程池。这个公共的线程池中的所有线程都是Daemon线程。这意味着如果主线程退出,这些线程无论是否执行完毕,都会退出系统)。

2.3 流式调用

CompletionStage的约40个接口是为函数式编程做准备的。在这里,看一下如何使用这些接口进行函数式的流式API调用:

public static Integer calc(Integer para) {
    try {
    //模拟一个长时间执行
    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()函数执行一个异步任务。接着连续使用流式调用对任务的处理结果进行再加工,直到最后的结果输出。

这里,执行CompletableFuture.get()方法,目的是等待calc()函数执行完成。不过不进行这个等待调用,由于CompletableFuture异步执行的缘故,主函数不等calc()方法执行完毕就会退出,随着主线程的结束,所有的Daemon线程都会立即退出,从而导致calc()方法无法正常完成。

2.4 CompletableFuture中的异常处理

如果CompletableFuture在执行过程中遇到异常,我们可以用函数式编程的风格处理这些异常。CompletableFuture提供了一个异常处理方法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))
        //对当前的CompletableFuture进行异常处理
        .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 组合多个CompletableFuture

CompletableFuture还允许将多个CompletableFuture进行组合。一种方法是使用thenCompose(),它的签名如下:

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)

一个CompletableFuture可以在执行完成后,将执行结果通过Function传递给下一个CompletionStage进行处理(Function接口返回新的CompletionStage实例):

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();
}

上述代码中,将处理后的结果传递给thenCompose(),并进一步传递给后续新生成的CompletableFuture实例,以上代码的输出如下:

"12"

另外一种组合多个CompletableFuture的方法是thenCombine(),它的签名如下:

public <U,V> CompletableFuture<U> thenCombime
  (CompletionStage<? extends U> other,
  BiFunction<? super T, ? super U,? extends V> fn)

方法thenCombime()首先完成当前CompletableFuture和other的执行。接着,将这两者的执行结果传递给BiFunction(该接口接收两个参数,并有一个返回值),并返回代表BiFunction实例的CompletableFuture对象:

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();
}

上述代码中,首先生成两个CompletableFuture实例,接着使用thenCombine()组合这两个CompletableFuture,将两者的执行结果进行累加,并将其累加结果转为字符串,并输出,上述代码的输出是:

"37"

三、读写锁的改进:StampedLock

StamppedLock是Java 8中引入的一种新的锁机制。读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发。但是,读和写之间依然是冲突的。读锁会完全阻塞写锁,它使用的依然是悲观锁的策略,如果有大量的读线程,它也有可能引起写线程的“饥饿”。而StampedLock提供了一种乐观的读策略。这种乐观策略的锁非常类似无锁的操作,使得乐观锁完全不会阻塞写线程。

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()函数可以申请写锁
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }

   double distanceFromOrigin() { // A read-only method
     //试图尝试一次乐观读
     long stamp = sl.tryOptimisticRead();
     double currentX = x, currentY = y;
     //判断这个stamp是否在读过程发生期间被修改过
     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的官方文档。它定义了一个Point类,内部有两个元素x和y,表示点的坐标。第3行定义了StampedLock锁。第15行定义的distanceFromOrigin()方法是一个只读方法,它只会读取Point的x和y坐标。在读取时,首先使用了StampedLock.tryOptimisticRead()方法。这个方法表示试图尝试一次乐观读。它会返回一个类似于时间的邮戳整数stamp。这个stamp就可以作为这一次锁获取的凭证。

接着,在第17行,读取x和y的值。当然,这时并不确定这个x和y是否是一致的(在读取x的时候,可能其他线程改写了y的值,使得currentX和currentY处于不一致的状态)。因此,我们必须在18行,使用validate()方法,判断这个stamp是否在读过程发生期间被修改过。如果stamp没有被修改过,则认为这次读取的过程中,可能被其他线程改写了数据,因此,有可能出现了脏读。如果出现这种情况,我们可以像处理CAS操作那样在一个死循环中一直使用乐观读,直到成功为止。
也可以升级锁的级别。在本例中,我们升级乐观锁的级别,将乐观锁变为悲观锁。在第19行,当判断乐观读失败后,使用readLock()获得悲观的读锁,并进一步读取数据。如果当前对象正在被修改,则读锁的申请可能导致线程挂起。

写入的情况可以参考第5行定义的move()函数。使用writeLock()函数可以申请写锁。这里的含义和读写锁是类似的。

在退出临界区时,不要忘记释放写锁(第11行)或者读锁(第24行)。

3.2 StampedLock的小陷阱

StampedLock内部实现时,使用类似于CAS操作的死循环反复尝试的策略。在它挂起线程时,使用的是Unsafe.park()函数,而park()函数在遇到线程中断时,会直接返回(不同于Thread.sleep(),它不会抛出异常)。而在StampedLock的死循环逻辑中,没有处理有关中断的逻辑。因此,这就会导致阻塞在park()上的线程被中断后,会再次进入循环。而当退出条件得不到满足时,就会发生疯狂占用CPU的情况。下面演示了这个问题:

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个读线程,让它们请求读锁。此时,由于写锁的存在,所有读线程都会被最终挂起。读线程因为 park() 的操作进入了等待状态,这种情况是正常的。

而在10秒钟以后(代码在17行执行了10秒等待),系统中断了这3个读线程,之后,就会发现,CPU占用率极有可能会飙升。这是因为中断导致 park() 函数返回,使线程再次进入运行状态。
此时,这个线程的状态是RUNNABLE,这是我们不愿意看到的,它会一直存在并耗尽CPU资源,直到自己抢占到了锁。

四、原子类的增强

无锁的原子类操作使用系统的CAS指令,有着远远超越锁的性能。在Java 8中引入了LongAddr类,这个类也在java.util.concurrent.atomic包下,因此,它也是使用了CAS指令。

4.1 更快的原子类:LongAddr

AtomicInteger的基本实现机制,它们都是在一个死循环内,不断尝试修改目标值,知道修改成功。如果竞争不激烈,那么修改成功的概率就很高,否则,修改失败的概率就很高。在大量修改失败时,这些原子操作就会进行多次循环尝试,因此性能会受到影响。

当竞争激烈的时候,为了进一步提高系统的性能,一种基本方案就是可以使用热点分离,将竞争的数据进行分解,基于这个思路,可以想到一种对传统AtomicInteger等原子类的改进方法。虽然在CAS操作中没有锁,但是像减小锁粒度这种分离热点的思想依然可以使用。一种可行的方案就是仿造ConcurrentHashMap,将热点数据分离。比如,可以将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计算,而最终的计算结果,则为这个数组的求和累加。热点value被分离成多个单元cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成,这样,热点就进行了有效的分离,提高了并行度。LongAddr正是使用了这种思想。

在实际的操作中,LongAddr并不会一开始就动用数组进行处理,而是将所有数据都先记录在一个称为base的变量中。如果在多线程条件下,大家修改base都没有冲突,那么也没有必要扩展为cell数组。但是,一旦base修改发生冲突,就会初始化cell数组,使用新的策略。如果使用cell数组更新后,发现在某一个cell上的更新依然发生冲突,那么系统就会尝试创建新的cell,或者将cell的数量加倍,以减少冲突的可能。

下面简单分析一下 increment() 方法(该方法会将LongAddr自增1)的内部实现:

public void increment() {
    add(1L);
}
public void add(long x) {  
    Cell[] as; long b, v; int m; Cell a;  	
    //如果cell表为null,会尝试将x累加到base上。  
    if ((as = cells) != null || !casBase(b = base, b + x)) {  
        /*
         * 如果cell表不为null或者尝试将x累加到base上失败,执行以下操作。
         * 如果cell表不为null且通过当前线程的probe值定位到的cell表中的Cell不为null。
         * 那么尝试累加x到对应的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,或者尝试失败,都会调用下面的Striped64中定义的longAccumulate方法。  
    longAccumulate(x, null, uncontended);  
 	}  
}

它的核心是 addd() 方法。最开始cells为null,因此数据会向base增加。但是如果对base的操作冲突,则会设置冲突标记uncontended 为true。接着,如果判断cells数组不可用,或者当前线程对应的cell为null,则直接进入 longAccumulate() 方法。否则会尝试使用CAS方法更新对应的cell数据,如果成功,则退出,失败则进入 longAccumulate() 方法。

由于 longAccumulate() 方法的大致内容是,根据需要创建新的cell或者对cell数组进行扩容,以减少冲突。

下面,简单地对LongAddr、原子类以及同步锁进行性能测试。测试方法使用多个线程对同一个整数进行累加,观察3中不同方法时所消耗的时间。首先,定义一些辅助变量:

private static final int MAX_THREADS = 3;        //线程数
private static final int TASK_COUNT = 3;         //任务书
private static final int TARGET_COUNT = 3;       //线程数

private AtomicLong acount = new AtomicLong(0L);  //无锁的原子操作
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);

上述代码中,指定了测试线程数量、目标总数以及3个初始化值为0的整型变量acount、lacount、count。它们分别表示使用AtomicLong、LongAddr和锁进行同步时的操作对象。下面是使用同步锁时的测试代码:

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()方法中,使用线程池控制多线程进行累加操作。使用类似的方法实现原子类累加计时统计:

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实现类似功能:

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中,将单个数值分解为多个不同的段。因此,在进行累加后,上述代码中increment()函数并不能返回当前的数值。要取得当前的实际值,需要使用 sum()函数重新计算。这个计算是需要有额外的成本的,但即使加上这个额外成本,LongAddr的表现还是比AtomicLong要好。

就计数性能而言,LongAddr已经超越了普通的原子操作。LongAddr的另外一个优化手段是避免了伪共存。LongAddr中并不是直接使用padding这种看起来比较碍眼的做法,而是引入了一种新的注释@sun.misc.Contended

对于LongAddr中的每一个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。这将会使得Java虚拟机自动为Cell解决伪共享问题。
当然,在我们自己的代码中也可以使用sun.misc.Contended来解决伪共享问题,但是需要额外使用虚拟机参数-XX:-RestrictContended,否则,这个注释将被忽略。

4.2 LongAddr的功能增强版:LongAccumulator

LongAccumulator是LongAddr的亲兄弟,它们有公共的父类Striped64。因此,LongAccumulator内部的优化方式和LongAddr是一样的。它们都将一个long型整数进行分割,存储在不同的变量中,以防止多线程竞争。两者的主要逻辑类似,但是LongAccumulator是LongAddr的功能扩展,对于LongAddr来说,它只是每次对给定的整数执行一次加法,而LongAccumulator则可以实现任意函数惭怍。

可以使用下面的构造函数创建一个LongAccumulator实例:

public LongAccumulator(LongBinaryOperator accumulatorFunction, long identify)

第一个参数accumulatorFunction就是需要执行的二元函数(接收两个long形参数并返回long),第2个参数是初始值。下面这个例子展示了LongAccurator的使用,它将通过多线程访问若干个整数,并返回遇到的最大的那个数字。

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);
}

上述代码中,构造了LongAccumulator实例。因为要过滤最大值,因此传入Long::max函数句柄。当有数据通过accumulate()方法传入LongAccumulator后,LongAccumulator会通过Long::max识别最大值并且保存在内部(很可能是cell数组,也可能是base)。通过longValue()函数对所有的cell进行Long::max操作,得到最大值。

猜你喜欢

转载自blog.csdn.net/AaronSimon/article/details/82874924