【java】多线程详解2
狂神老师的视频地址:https://space.bilibili.com/95256449
1、ForkKJoin和Stream并发
1.1 简介
Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。类似于Java 8中的parallel Stream。
在大数据量下的利用ForkKJoin并发提高效率,数据少就没必要了。
原理:工作窃取
如下图:
当A和B同时执行任务队列,B先执行完毕之后,可以帮A从队尾开始执行任务,节省时间
1.2、实现
计算类
/*
* 求和计算的任务!
* 如何使用ForkJoin
* 1、通过forkjoinpool来执行
* 2、计算任务forkjoinPool.execute(ForkJoinTask task)
* 3、计算类要继承RecursiveTask
* */
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long temp = 10000l;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if ((end - start) > temp) {
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long middle = (end - start) / 2;
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork();
return task1.join() + task2.join();
}
}
}
对比普通方法和ForkJoin计算时间和Stream
public class Test {
public static void main(String[] args) {
test1();
test2();
test3();
}
//普通方式
public static void test1() {
Long sum = 0L;
long start = System.currentTimeMillis();
for (Long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
//ForkJoin方式
public static void test2() {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(1L, 10_0000_0000L);
//forkJoinPool.execute(task);//执行任务无返回结果
ForkJoinTask<Long> submit = forkJoinPool.submit(task);//异步提交
Long sum = null;
try {
sum = submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
//Stream
public static void test3() {
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(1L,10_0000_0000L).parallel().reduce(0,Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
}
Console:
sum=500000000500000000 时间:5841
sum=500000000500000000 时间:5020
sum=500000000500000000 时间:160
可以看到ForkJoin效率提高了一部分(可以调节临界值来进一步提高),而Stream效率明显要高很多!
2、异步回调
2.1、概念
Ajax中已经讲过了异步请求是什么,即不需要等待前面的请求执行完毕,随时可以发送请求。
异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。
如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。
CompletableFuture
/**
* 异步调用:例如Ajax
*/
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的异步回调
/*CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "runAsyc=>Void");
});
System.out.println("111");
completableFuture.get();*/
//有返回值的异步回调
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");
//int i = 10 / 0; //制造错误
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t==>" + t); //正常的返回结果
System.out.println("u==>" + u); //错误信息
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 233; //错误时候的返回结果
}).get());
//使用ExecutorService运行异步
ExecutorService executorService = Executors.newFixedThreadPool(10);
completableFuture = CompletableFuture.runAsync(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "runAsyc=>Void");
},executorService);
}
}
没有返回结果:
111
ForkJoinPool.commonPool-worker-9runAsyc=>Void
有返回结果无错误时:
t==>1024
u==>null
1024
有返回结果有错误时(int i = 10 / 0;注释取消):
ForkJoinPool.commonPool-worker-9supplyAsync=>Integer
t==>null
u==>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
233
2.2、常用方法
# 对异步线程的下一步处理
thenApply(): 将调用它的异步线程的返回值作为输入值,自身带有返回值
thenApplyAsync(): 与前者不同的是会自己再起一个线程
thenAccept(): 将调用博客后台 - 博客园的异步线程的返回值作为输入值,自身不带有返回值
thenAcceptAsync(): 与前者不同的是会自己再起一个线程
thenRun(): 接着调用它的异步线程后面执行,没有输入,也没有输出
thenRunAsync(): 与前者不同的是会自己再起一个线程
whenComplete(): 将调用它的异步线程的返回值和异常情况作为输入值,自己的返回值和输入的值一样
whenCompleteAsync():前者不同的是会自己再起一个线程
handle()和handleAsync(): 与whenComplete不同的是有自己的返回值
# 对异步线程的下一步组合处理
thenCombine():将两个线程的参数作为输入值,有自己的返回值
thenAcceptBoth(): 将两个线程的参数作为输入值,没有自己的返回值
runAfterBoth(): 没有输入值,也没有返回值
- 异步运行
- 获取线程状态
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
CompletableFuture completableFuture = CompletableFuture.runAsync(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//isDone() 判断是否完成,不管是异常还是正常
System.out.println(completableFuture.isDone());
//isCancelled() 判断正常完成之前是否取消了
System.out.println(completableFuture.isCancelled());
//isCompletedExceptionally() 判断是否异常结束,包括取消cancel、显示调用completeExceptionally、中断。
System.out.println(completableFuture.isCompletedExceptionally());
//取消线程
completableFuture.cancel(true);
//告知CompletableFuture任务完成。
completableFuture.complete(true);
//做超时处理,如果超时报错,注意超时和被取消不一样
completableFuture.get(5, TimeUnit.SECONDS);
}
}
3、Volatile
Volatile是Java虚拟机提供的轻量级的同步机智
- 保证可见性
- 不保证原子性
- 禁止指令重排
而要了解指令重拍就得了解JMM
3.1、JMM
JMM即java内存模型,是一种概念
JMM不保证可见性的原因:B线程修改了flag值,但是线程A不能及时可见
3.2、保证可见性
public class JMMDemo {
//不加上volatile,无法保证可见性
private static int num = 0;
public static void main(String[] args) {
new Thread(() -> {
while (num == 0) {
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
会发现,num变成了1,但是我们new的线程一直没有结束,说明它一直没有获取到num的值
如果给num加上volatile,那就可以了
3.3、不保证原子性
public class VDemo2 {
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(num);
}
}
console:
19884 //每次不一样
会发现,即使添加了volatile,结果也和逾期对不上。
因为num++不是一个原子性操作,其实底层是获取值、+1、写入值三步骤。
所以这一类情况,使用原子类就可以保证原子性:
private static AtomicInteger num = new AtomicInteger(0);
public static void add() {
num.getAndIncrement(); // CAS
}
那么为什么不用synchronized呢?因为Atomic类底层用了CAS。
3.4、指令重排
代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,当然这是在不影响数据逻辑依赖的前提下的。
但是在多线程情况下,线程A中的没有依赖关系的指令,和线程B中没有依赖关系的指令,之间是可能有逻辑关系的,重排之后就会出问题。
而volatile可以通过插入内存屏障来禁止指令重排
4、深入理解CAS
4.1、什么是CAS(compare and swap)锁
即在修改数据之前验证值是否会修改,如果被修改读取最新的值,如果没被修改保存数据。
4.2、AtomicInteger中的使用
上文原子性讲的AtomicInteger类中的getAndIncrement方法是使用了CAS的:
4.3、CAS锁的弊端:
- 要是结果一直就一直循环了(自旋锁),CUP开销是个问题
- 只能保证一个共享变量原子操作的问题
- 还有ABA问题
4.4、解决ABA问题
给变量添加一个版本号就好了嘛
AtomicStampedReference类就是干这个的:
5、常见的死锁和定位
最常见的死锁就是A调用B,B调用A,循环等待,例如下面这个代码
public class Demo1 {
public static void main(String[] args) {
String a = "a";
String b = "b";
new Thread(new Mythread(a,b)).start();
new Thread(new Mythread(b,a)).start();
}
}
class Mythread implements Runnable {
private String lockA;
private String lockB;
public Mythread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "lock:" + lockA + "=>get" + lockB); //当第一个线程拿到A锁的,等待的时候,第二个线程拿到B,开始循环等待
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "lock:" + lockB + "=>get" + lockA);
}
}
}
}
console:
Thread-0lock:a=>getb
Thread-1lock:b=>geta
//一直等待出现死锁
那么怎么定位死锁呢?除了常见的查看代码逻辑,查看日志之外,还可以查看堆栈信息
使用 jps -l
定位进程
使用 jstack 进程id
查看堆栈信息
C:\Users\Administrator>jstack 10548
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001d5f3628 (object 0x000000076bf99088, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001d5f0d98 (object 0x000000076bf990b8, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.company.lock.Mythread.run(Demo1.java:33)
- waiting to lock <0x000000076bf99088> (a java.lang.String)
- locked <0x000000076bf990b8> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.company.lock.Mythread.run(Demo1.java:33)
- waiting to lock <0x000000076bf990b8> (a java.lang.String)
- locked <0x000000076bf99088> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.