目录
一、ForkJoin分支合并
1、概述
Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行;
ava.util.concurrent.ForkJoinPool由Java大师Doug Lea主持编写,它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果并返回;
Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务;
2、ForkJoin工作特点
工作窃取:自己执行完之后,会执行另外的部分;
3、基本思路
设置临界值——拿到总计算数量——如果总计算数量小于等于临界值就直接计算——如果总计算数量大于临界值就分成两个子任务进行——再对子任务计算数量与临界值进行判断——依此循环(递归),直到最小单位的任务计算数量小于等于临界值进行直接计算——将所有计算结果逐级(递归特性)合并——最后一次合并得到计算最终结果——返回此结果;
4、基本使用
代码:
package com.zibo.forkjoin;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class Main {
public static void main(String[] args) {
// 创建2000个随机数组成的数组:
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join:
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
static Random random = new Random(0);
static long random() {
return random.nextInt(10000);
}
}
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;//临界值,大于临界值就算大,小于临界值就算小
long[] array;
int start;//头索引
int end;//长度(尾索引+1)
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {//所运算值的数量小于等于临界值就直接计算
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
}
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
//左边递归计算(从此可以看出,不包括尾索引,所以前面要用长度来包括要计算的所有元素)
SumTask subtask1 = new SumTask(this.array, start, middle);
//右边递归计算
SumTask subtask2 = new SumTask(this.array, middle, end);
//执行
invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}
结果:
Expected sum: 9788366
split 0~2000 ==> 0~1000, 1000~2000
split 0~1000 ==> 0~500, 500~1000
split 1000~2000 ==> 1000~1500, 1500~2000
result = 2391591 + 2419573 ==> 4811164
result = 2485485 + 2491717 ==> 4977202
result = 4811164 + 4977202 ==> 9788366
Fork/join sum: 9788366 in 24 ms.
5、参考文章
https://blog.csdn.net/tyrroo/article/details/81390202
https://www.jianshu.com/p/ef2eb256840d
https://www.cnblogs.com/senlinyang/p/7885964.html
二、异步回调
1、简单使用
(用法见注释)
代码:
package com.zibo.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* 异步调用:
* 异步执行、成功回调、失败回调
*/
public class Main {
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("哈哈哈哈!");
});
System.out.println("啦啦啦啦!");
//异步
completableFuture.get();
//发起一个异步请求(有返回值)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1024);
future.whenComplete((integer, throwable) -> {//成功回调
System.out.println("成功啦:");
System.out.println("通过成功回调方法拿到的返回值:" + integer);
}).exceptionally(throwable -> {//失败回调
System.out.println("失败咯:");
throwable.printStackTrace();
return 404;//这是出错后的返回信息
});
System.out.println("通过future.get()拿到的返回值:" + future.get());
}
}
结果:
啦啦啦啦!
哈哈哈哈!
成功啦:
通过成功回调方法拿到的返回值:1024
通过future.get()拿到的返回值:1024
三、JMM
1、先了解Volatile
是Java虚拟机提供的轻量级的同步机制;
①保证可见性;
②不保证原子性;
③禁止指令重排;
2、什么是JMM
JMM:Java内存模型,不存在的东西!概念!约定!
关于JMM一些同步约定:
线程解锁前:必须把共享变量立刻从线程工作内存同步到主内存;
线程枷锁前:必须把共享变量从主内存读取最新值到工作内存;
锁:加锁和解锁必须是同一把锁;
主内存与工作内存之间的8种操作:
(注意:下图write和store位置写反了)
内存交互操作:
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外);
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write;
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存;
- 不允许一个线程将没有assign的数据从工作内存同步回主内存;
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作;
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁;
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值;
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量;
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存;
代码测试:
package com.zibo.volatile001;
//测试JMM
public class Main {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {//主线程
new Thread(()->{//子线程
while (number==0){
}
}).start();
Thread.sleep(1000);
number = 1;
System.out.println(number);
}
}
结果:
(主内存中的number已经变成了1,但是子线程拿到的number一直是0,我们想要的是主内存中的值发生了变化,子线程要知道,我们就会用到volatile关键字)
参考文章:
https://www.cnblogs.com/null-qige/p/9481900.html
四、Volatile关键字
1、保证可见性
代码:
(给number加上volatile关键字)
package com.zibo.volatile001;
//测试JMM
public class Main {
private static volatile int number = 0;
public static void main(String[] args) throws InterruptedException {//主线程
new Thread(()->{//子线程
while (number==0){
}
}).start();
Thread.sleep(1000);
number = 1;
System.out.println(number);
}
}
结果:
(程序停了下来)
2、不保证原子性
代码:
package com.zibo.volatile001;
//不保证原子性
public class Test {
private static volatile int num = 0;
private static void add(){
num++;
}
public static void main(String[] args) {
//理论结果是2万
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(Thread.currentThread().getName() + " " + num);
}
}
结果:
3、不加lock和synchronized怎么保证原子性
num++底层操作:
(看似一步,底层好几步,所以会出现并发问题)
那咋办?
使用原子类!这和操作系统底层挂钩!
代码:
package com.zibo.volatile001;
import java.util.concurrent.atomic.AtomicInteger;
//不保证原子性
public class Test {
private static volatile AtomicInteger num = new AtomicInteger();//原子类
private static void add(){
// num++;
num.getAndIncrement();//自增1
}
public static void main(String[] args) {
//理论结果是2万
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(Thread.currentThread().getName() + " " + num);
}
}
结果:
五、指令重排
1、概述
指令重排是指在程序执行过程中,为了性能考虑,编译器和CPU可能会对指令重新排序;
源代码——编译器优化重排——指令并行重排——内存系统重排——执行;
指令重排并不是胡乱重排,会考虑数据之间的依赖性;
2、指令重排可能造成的影响
3、Volatile可以避免指令重排
内存屏障:
内存屏障(Memory Barrier)是一种CPU指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行;
作用:
①保证特定操作的执行顺序;
②可以保证某些变量的内存可见性(Volatile就是利用的这些特性实现的内存可见性);
图示:
(Volatile写)
使用场景:
在单例模式中使用得最多;