Java 高并发十: JDK8对并发的新支持详解
作者:Hosee 发布时间:2022-12-02 02:43:09
1. LongAdder
和AtomicLong类似的使用方式,但是性能比AtomicLong更好。
LongAdder与AtomicLong都是使用了原子操作来提高性能。但是LongAdder在AtomicLong的基础上进行了热点分离,热点分离类似于有锁操作中的减小锁粒度,将一个锁分离成若干个锁来提高性能。在无锁中,也可以用类似的方式来增加CAS的成功率,从而提高性能。
LongAdder原理图:
AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过CAS 指令从机器指令级别操作保证并发的原子性。唯一会制约AtomicLong高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong效率降低。
而LongAdder将把一个value拆分成若干cell,把所有cell加起来,就是value。所以对LongAdder进行加减操作,只需要对不同的cell来操作,不同的线程对不同的cell进行CAS操作,CAS的成功率当然高了(试想一下3+2+1=6,一个线程3+1,另一个线程2+1,最后是8,LongAdder没有乘法除法的API)。
可是在并发数不是很高的情况,拆分成若干的cell,还需要维护cell和求和,效率不如AtomicLong的实现。LongAdder用了巧妙的办法来解决了这个问题。
初始情况,LongAdder与AtomicLong是相同的,只有在CAS失败时,才会将value拆分成cell,每失败一次,都会增加cell的数量,这样在低并发时,同样高效,在高并发时,这种“自适应”的处理方式,达到一定cell数量后,CAS将不会失败,效率大大提高。
LongAdder是一种以空间换时间的策略。
2. CompletableFuture
实现CompletionStage接口(40余个方法),大多数方法多数应用在函数式编程中。并且支持流式调用
CompletableFuture是Java 8中对Future的增强版
简单实现:
import java.util.concurrent.CompletableFuture;
public class AskThread implements Runnable {
CompletableFuture<Integer> re = null;
public AskThread(CompletableFuture<Integer> re) {
this.re = re;
}
@Override
public void run() {
int myRe = 0;
try {
myRe = re.get() * re.get();
} catch (Exception e) {
}
System.out.println(myRe);
}
public static void main(String[] args) throws InterruptedException {
final CompletableFuture<Integer> future = new CompletableFuture<Integer>();
new Thread(new AskThread(future)).start();
// 模拟长时间的计算过程
Thread.sleep(1000);
// 告知完成结果
future.complete(60);
}
}
Future最令人诟病的就是要等待,要自己去检查任务是否完成了,在Future中,任务完成的时间是不可控的。而 CompletableFuture的最大改进在于,任务完成的时间也开放了出来。
future.complete(60);
用来设置完成时间。
CompletableFuture的异步执行:
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 = CompletableFuture
.supplyAsync(() -> calc(50));
System.out.println(future.get());
}
CompletableFuture的流式调用:
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 {
CompletableFuture<Void> fu = CompletableFuture
.supplyAsync(() -> calc(50))
.thenApply((i) -> Integer.toString(i))
.thenApply((str) -> "\"" + str + "\"")
.thenAccept(System.out::println);
fu.get();
}
组合多个CompletableFuture:
public static Integer calc(Integer para) {
return para / 2;
}
public static void main(String[] args) throws InterruptedException,
ExecutionException {
CompletableFuture<Void> fu = CompletableFuture
.supplyAsync(() -> calc(50))
.thenCompose(
(i) -> CompletableFuture.supplyAsync(() -> calc(i)))
.thenApply((str) -> "\"" + str + "\"")
.thenAccept(System.out::println);
fu.get();
}
这几个例子更多是侧重Java8的一些新特性,这里就简单举下例子来说明特性,就不深究了。
CompletableFuture跟性能上关系不大,更多的是为了支持函数式编程,在功能上的增强。当然开放了完成时间的设置是一大亮点。
3. StampedLock
在上一篇中刚刚提到了锁分离,而锁分离的重要的实现就是ReadWriteLock。而StampedLock则是ReadWriteLock的一个改进。StampedLock与ReadWriteLock的区别在于,StampedLock认为读不应阻塞写,StampedLock认为当读写互斥的时候,读应该是重读,而不是不让写线程写。这样的设计解决了读多写少时,使用ReadWriteLock会产生写线程饥饿现象。
所以StampedLock是一种偏向于写线程的改进。
StampedLock示例:
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
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;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
上述代码模拟了写线程和读线程, StampedLock根据stamp来查看是否互斥,写一次stamp变增加某个值
tryOptimisticRead()
就是刚刚所说的读写不互斥的情况。
每次读线程要读时,会先判断
if (!sl.validate(stamp))
validate中会先查看是否有写线程在写,然后再判断输入的值和当前的 stamp是否相同,即判断是否读线程将读到最新的数据。
如果有写线程在写,或者 stamp数值不同,则返回失败。
如果判断失败,当然可以重复的尝试去读,在示例代码中,并没有让其重复尝试读,而采用的是将乐观锁退化成普通的读锁去读,这种情况就是一种悲观的读法。
stamp = sl.readLock();
StampedLock的实现思想:
CLH自旋锁:当锁申请失败时,不会立即将读线程挂起,在锁当中会维护一个等待线程队列,所有申请锁,但是没有成功的线程都记录在这个队列中。每一个节点(一个节点代表一个线程),保存一个标记位(locked),用于判断当前线程是否已经释放锁。当一个线程试图获得锁时,取得当前等待队列的尾部节点作为其前序节点。并使用类似如下代码判断前序节点是否已经成功释放锁
while (pred.locked) {
}
这个循环就是不断等前面那个结点释放锁,这样的自旋使得当前线程不会 * 作系统挂起,从而提高了性能。
当然不会进行无休止的自旋,会在若干次自旋后挂起线程。


猜你喜欢
- 本文实例讲述了Java线程同步方法。分享给大家供大家参考,具体如下:1. Semaphore1.1 二进制SemaphoreSemaphor
- 1.图集导航1.1 为什么对包名的命名要有所规范呢!使用规范的命名有益于程序的开发和后期阅读通俗的说:就是自己写的代码别人也能看的懂,代码结
- 前段时间写了一篇C#解析Lrc歌词文件,对lrc文件进行解析,支持多个时间段合并。本文借下载歌词文件来探讨一下同步和异步方法。 L
- 使用@Tolerate实现冲突兼容使用Lombok能够减少程序员的重复工作提高工作效率,而Lombok的注解基本是基于标准的(如,标准的Bu
- startJVM是加载jvm用的方法。在JPype,apache mod等等很多地方都用到。但凡要用其他语言来加载jvm进程,就要用到这个。
- 利用Java,在控制台操作下,编写的五子棋,作为复习二维数组,面向对象等基础知识。w表示白棋,b表示黑棋import java.util.S
- 研究了下这个,记录下代码。主页面代码:activity_main.xml<?xml version="1.0" e
- 问题描述在开发批量删除功能时,往往都是多条数据,所以前台需要传一个数组给后台,但是怎么在URL中绑定一个数组,同时在后台用@PathVari
- 绑定(Binding)元素介绍首先,盗用张图。这图形象的说明了Binding的机理。此处主要介绍的绑定类是System.Windows.Da
- 今天对接一个海康监控的sdk,其中sdk 是以aar的形式提供的,并且我需要用到此aar的模块是个library。所以按照正常的在appli
- 前言说实话当第一次看到这个需求的时候,第一反应就是Canvas只有drawLine方法,并没有drawDashLine方法啊!这咋整啊,难道
- 前言相信对于RxJava,大家应该都很熟悉,他最核心的两个字就是异步,诚然,它对异步的处理非常的出色,但是异步绝对不等于并发,更不等于线程安
- Spring Boot产生环形注入***************************APPLICATION FAILED TO STAR
- C# 的类型转换有显式转型 和 隐式转型 两种方式。显式转型:有可能引发异常、精确度丢失及其他问题的转换方式。需要使用手段进行转换操作。隐式
- 实现“摇一摇”功能,其实很简单,就是检测手机的重力感应,具体实现代码如下:1、在 AndroidManifest.xml 中添加操作权限2、
- 上一节我们了解了Lock接口的一些简单的说明,知道Lock锁的常用形式,那么这节我们正式开始进入JUC锁(java.util.concurr
- 前言相信大家在使用spring的项目中,前台传递参数到后台是经常遇到的事, 我们必须熟练掌握一些常用的参数传递方式和注解的使用,本文将给大家
- 导语:有些时候我们所需要查询的数据量比较大,但是jvm内存又是有限制的,数据量过大会导致内存溢出。这个时候就可以使用流式查询,数据一条条的返
- 本文实例为大家分享了java数据库唯一id生成工具类的具体代码,供大家参考,具体内容如下import java.io.File;import
- 这里主要是总结一下如何监听有未接来电的问题 1.1 使用广播 * BrocastReceiver实现思路 : 静态注册监听and