Java并发学习-JUC
JUC体系
1、什么是JUC
2、 线程和进程:
参考:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
进程:一个程序,比如qq.exe程序的集合;
一个进程往往可以包括多个进程,至少包含一个;
Java 默认有几个线程? 2个 main、GC
线程:
对Java :Thread Runable Callable
类似“**进程是资源分配的最小单位,线程是CPU调度的最小单位”**这样的回答感觉太抽象,都不太容易让人理解。
做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢与前一节产生断裂,将影响后面的所有车厢)
- 进程可以拓展到多机,进程最适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
**Java真的可以开启线程吗? **不可以
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//本地方法,底层是c++,java是在虚拟机运行的,无法操作硬件
private native void start0();
并发、并行
并发编程:并发、并行
并发(多个线程操作同一个资源)
- 一核CPU,模拟出来多条线程,快速交替
并行(多个人一起行走)
- 多核CPU,多个线程同时执行;可以使用线程池
Runtime.getRuntime().availableProcessors(),获取cpu核数,即可用的cpu数
并发编程的本质:充分利用CPU的资源
线程有几个状态 6个
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//计时等待
TIMED_WAITING,
//终止
TERMINATED;
}
线程wait 和sleep的区别
1、来自不同的类
wait==>Object
sleep==>Thread ,开发时,一般用TimeUnit工具类
2、关于锁的释放
wait会释放锁,sleep不会。
3、使用的范围不同
wait必须在同步代码块中
sleep可以在任何地方睡
4、是否需要捕获异常
wait不需要捕获异常
sleep必须要捕获异常
3、Lock锁
传统的 Synchronized
线程操作资源
作为资源类中只有相应的属性和方法,不实现线程相关的接口,真正的是在new Thread中的调用资源类中的方法进行操作资源的。
Lock锁
![image-20210906092800727](https://i-blog.csdnimg.cn/blog_migrate/bba14a81a3a850a0d9bf3ad60f757f5d.png)
这里先了解Lock接口下的实现类:
Synchronized 和 Lock的区别
1、Synchronized 是内置的Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取带锁的状态,Lock可以
3、Synchronized 会自动释放锁,Lock必须要手动释放锁,如果不释放,会死锁
4、Synchronized 线程1(获得锁,后阻塞)、线程2(一直等待);Lock不会一直等待;
5、Synchronized 可重入锁,不可中断的,非公平的;Lock,可重入锁,公平可设置,可以判断锁
6、Synchronized 适合锁少量的代码同步问题;Lock适合大量的同步代码!
锁是什么,如何判断锁的是谁
4、生产者和消费者问题
synchronized实现生产者消费者
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
static class Data{
private int num = 0;
public synchronized void increment() throws InterruptedException {
//(判断)等待
while (num>0){ //这里用if会存在虚假唤醒
this.wait();
}
//业务
num++;
System.out.println(Thread.currentThread().getName()+"=>"+(num));
//唤醒
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while(num==0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+(num));
this.notifyAll();
}
}
JUC版本生产者和消费者
通过Lock找到一个Condition类
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (num>0){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"=>"+(num));
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
Condition实现精确地通知唤醒
static class Data{
//A->B->C->A->... 实现精确唤醒
private int num = 1;
final Lock lock = new ReentrantLock();
final Condition condition1 = lock.newCondition();
final Condition condition2 = lock.newCondition();
final Condition condition3 = lock.newCondition();
public void printA() {
lock.lock();
try {
while (num!=1){
condition1.await();
}
num = 2;
System.out.println(Thread.currentThread().getName()+"=>"+"AAA");
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (num!=2){
condition2.await();
}
num = 3;
System.out.println(Thread.currentThread().getName()+"=>"+"BBB");
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (num!=3){
condition3.await();
}
num = 1;
System.out.println(Thread.currentThread().getName()+"=>"+"CCC");
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
5、锁对象
如何判断锁的是谁,什么是锁,锁到底锁的是谁?
深刻理解锁
synchronized锁方法
锁的是:对象(普通方法)和Class(静态方法)
6、集合类不安全
多线程不安全的ArrayList
解决方案:
1、使用Vector集合
2、使用Collections.synchronizedList(new ArrayList<>())
3、juc下的 CopyOnWriteArrayList<>()
CopyOnWrite 写入时复制 COW是计算机程序设计领域的一种优化策略;
多个线程调用时,读取list时,是固定的,但是在写入会覆盖,相当是,在写入时避免覆盖,造成数据 问题。
CopyOnWriteArrayList比Vector优势在哪? Vector底层是synchronized,CopyOnWriteArrayList底层是Lock锁。
public static void main(String[] args) {
//java.util.ConcurrentModificationException 并发修改异常
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
Set不安全
解决方案:
1、使用Collections.synchronizedSet(new HashSet<>())
2、juc下的 CopyOnWriteArraySet<>()
HashMap不安全
1、使用Collections.synchronizedMap()
2、juc下的ConcurrentHashMap
7、Callable
1、可以有返回值
2、可以抛出异常
3、方法不同run()/call()
实现Callable接口的线程可以通过 Runnable接口的一个实现类FutureTask(适配类),传递给Thread执行
具体关系参照:
![Callable](https://i-blog.csdnimg.cn/blog_migrate/55b3351b362df10f108efefc08f12409.png)
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask(myThread);
new Thread(futureTask).start();
Integer o = futureTask.get();
System.out.println(o);
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 0;
}
}
总结:
1、get会阻塞
2、有缓存
8、常用辅助类
8.1 CountDownLatch
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"执行完毕");
countDownLatch.countDown(); //数量-1
},String.valueOf(i)).start();
}
countDownLatch.await(); //数量减到0后唤醒,执行下面的操作
System.out.println("所有线程执行完毕");
}
/* 一种结果:
2执行完毕
3执行完毕
1执行完毕
5执行完毕
0执行完毕
4执行完毕
所有线程执行完毕
*/
countDownLatch.countDown() 计数量-1
countDownLatch.await() 等待计数器归0后在向下执行
8.2 CyclicBarrier
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("成功");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
try {
System.out.println(temp);
cyclicBarrier.await(); //直到7条线程都在此屏障上调用了await
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
8.3 Semaphore
9、读写锁
ReadWriteLock 接口 ReentrantReadWriteLock是它的唯一的实现类。
相比于ReentrantLock更加细粒度加锁
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
//自定义缓存
static class MyCache{
private volatile Map<String,String> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,String val){
readWriteLock.writeLock().lock(); //写锁
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,val);
System.out.println(Thread.currentThread().getName()+"写入完毕");
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock(); //读锁
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取完毕");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
10、阻塞队列
![image-20210909153342076](https://i-blog.csdnimg.cn/blog_migrate/58e7101275369ddfc4589ef1f9b2c962.png)
四组Api:
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer |
移除 | remove | poll | take | poll |
检测队首元素 | element | peek | - | - |
/*
* 抛出异常
*/
public static void test1(){
//给定初始队列的大小
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("1"));
System.out.println(queue.add("2"));
System.out.println(queue.add("3"));
// java.lang.IllegalStateException: Queue full 抛出异常
//System.out.println(queue.add("4"));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
//java.util.NoSuchElementException 抛出异常
System.out.println(queue.remove());
}
SynchronousQueue 同步队列 BlockingQueue的实现类
没有容量,SynchronousQueue 放入(put)一个元素,只能等到取出(take)后才能再放入。
public static void main(String[] args) {
BlockingQueue<String> synchronousQueue = new SynchronousQueue();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + " put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName() + " put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take " + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T2").start();
}
11、线程池
线程池:三大方法,7大参数,4种拒绝策略。
池化技术
程序的运行本质:占用系统的资源,! 优化资源的使用=>池化技术,例如,线程池、连接池等的
池化技术:事先准备好一些资源,有人要用就过来拿,用完后换回来。
线程池的好处:
1,降低资源的消耗
2,提高效应的速度
3,方便管理
线程复用,可以控制最大并发数,管理线程
线程池:三大方法
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 100; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭线程池
threadPool.shutdown();
}
}
7大参数
//源码分析
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // max 是 21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, // max传进来的值
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, //max = 1
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//本质上都是,new ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize,//1、核心线程数
int maximumPoolSize,//2、最大数量
long keepAliveTime,//3、没有使用的,超时释放掉
TimeUnit unit,//4、超时参数
BlockingQueue<Runnable> workQueue,//5、阻塞队列
ThreadFactory threadFactory,//6、线程工厂,采用默认
RejectedExecutionHandler handler) {//7、拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bi2Vq9EF-1638714989003)(https://i.loli.net/2021/09/12/QlA2sx3VzrELOBW.png)]
手动创建线程池
//自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()//满了,再进来的线程会被拒接,并抛出RejectedExecutionException
);
四大拒绝策略
new ThreadPoolExecutor.AbortPolicy() 满了,再进来的线程会被拒接,并抛出RejectedExecutionException
new ThreadPoolExecutor.CallerRunsPolicy() 被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务,除非执行程序已关闭,在这种情况下任务将被丢弃。
new ThreadPoolExecutor.DiscardPolicy() 被拒绝任务的处理程序,它默默地丢弃被拒绝的任务。
new ThreadPoolExecutor.DiscardOldestPolicy() 被拒绝任务的处理程序,丢弃最早的未处理请求,然后重试execute ,除非执行程序关闭,在这种情况下任务将被丢弃。
小结与拓展
池的最大该设置为多少?(调优)
了解:
CPU密集型:直接和CPU的核数相关,几核就是几,就是达到服务器的最大并发数量
IO密集型:> 程序中非常消耗io的线程数量
//获取cpu的核数
Runtime.getRuntime().availableProcessors();
12、四大函数式接口
新特性:
- lambda表达式
- 链式编程
- 函数式接口
- Stream流式计算
函数式接口:只有一个方法的接口
//例如:Runnable,foreach(消费型函数式接口)
//简化编程模型,在新版本底层大量应用
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
原生的四大函数式接口:Function、Predicate、Consumer、Supplier。
1、Function
Function 函数型接口,有一个T类型形参t,返回R类型
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
//测试代码
public class Demo01 {
public static void main(String[] args) {
Function function = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer a) {
return a + 1;
}
};
Function<Integer, Integer> function1 = (a) -> {return a + 1;};
System.out.println(function.apply(1));
}
}
2、Predicate
Function 断定型接口,接受一个参数,返回布尔值
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
}
//测试
public class Demo02 {
public static void main(String[] args) {
Predicate<Integer> predicate = new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
if (integer <= 0) {
return false;
}
return true;
}
};
Predicate<Integer> predicate1 = (integer) -> {
if (integer >= 0) {
return false;
}
return true;
};
System.out.println(predicate1.test(-1));
}
}
3、Consumer
Consumer 消费型接口 ,只有输入参数,没有返回值
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
public class Demo03 {
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
//将接收到的值打印
System.out.println(s);
}
};
consumer.accept("123");
}
}
4、Supplier
Supplier 供给型接口 ,只有返回值,没有输入参数
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
public class Demo04 {
public static void main(String[] args) {
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "I love study.";
}
};
System.out.println(supplier.get());
}
}
13、Stream 流式计算
什么是Stream流式计算
Stream流式计算设计思想:用来做计算
demo:
public class Demo01 {
public static void main(String[] args) {
/*
* 1.pk是偶数
* 2.年龄>23
* 3.用户名转成大写
* 4.用户名倒序排序
* 5.只输出一个用户
*/
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(5, "e", 25);
//集合用来存储数据
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
//流用来计算
list.stream()
.filter(user -> user.getPk() % 2 == 0)
.filter(user -> {
return user.getAge() > 23;
})
.map(u -> {
u.setName(u.getName().toUpperCase());
return u;
})
.sorted((u11, u21) -> u21.getName().compareTo(u11.getName()))
.limit(1)
.forEach(System.out::println);
}
}
14、ForkJoin
sine 1.7
把大任务拆分为小任务
ForkJoin的特点:工作窃取
是因为,ForkJoin任务中维护的都是双端队列
使用方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xoNNDpEJ-1638714989007)(C:/Users/45869/AppData/Roaming/Typora/typora-user-images/image-20211113174624169.png)]
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() {
Long sum = 0l;
if (end - start < temp) {
for (Long i = start; i <= end; i++) {
sum+=i;
}
}else{
//走拆分
//计算中间值
Long middle =( end +start)/2;
// start~middle 一个任务
ForkJoinDemo task1 = new ForkJoinDemo(start,middle);
task1.fork();
// middle+1~end 一个任务
ForkJoinDemo task2 = new ForkJoinDemo(middle+1,end);
task2.fork();
sum = task1.join()+task2.join();
}
return sum;
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
test0();
//forkJoin
test1();
//流式计算
test2();
long end = System.currentTimeMillis();
System.out.println((end - start));
}
private static void test0() {
Long sum = new Long(0);
for (Long i = 0L; i <= 1000000000L; i++) {
sum += i;
}
System.out.println(sum);
}
private static void test1() throws InterruptedException, ExecutionException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 1000000000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
System.out.println("sum=" + sum);
}
private static void test2() {
//stream并行流计算比较快
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
System.out.println(sum);
}
}
15、异步回调
文章参考:https://zhuanlan.zhihu.com/p/344431341
前言
JAVA支持的多线程开启方式
根据Oracle官方出具的Java文档说明,创建线程的方式只有两种:继承Thread或者实现Runnable接口。 但是这两种方法都存在一个缺陷,没有返回值,也就是说我们无法得知线程执行结果。虽然简单场景下已经满足,但是当我们需要返回值的时候怎么办呢? Java 1.5 以后的Callable和Future接口就解决了这个问题,我们可以通过向线程池提交一个Callable来获取一个包含返回值的Future对象,从此,我们的程序逻辑就不再是同步顺序。
下面是Java8实战书籍的原文:
Future接口在Java5中被引入,设计初衷是对将来某个时刻会产生的结果进行建模。它建模了一种异步运算,返回一个执行结果的引用,当运算结束后,这个引用被返回给调用方。在Future中触发那些潜在耗时的操作完成。
如下图: 我们从最初的串行操作变成了并行,在异步的同时,我们还可以做其他事情来节约程序运行时间。
图片来源:https://zhuanlan.zhihu.com/p/54459770
机智的小伙伴肯定会问了,什么,这篇不是讲JAVA8新特性,CompletableFuture的吗?怎么说起Future了? 不急,看下面
Future接口的局限性
当我们得到包含结果的Future时,我们可以使用get方法等待线程完成并获取返回值,注意我加粗的地方,Future的get() 方法会阻塞主线程。 Future文档原文如下
A {@code Future} represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation.
谷歌翻译:
{@code Future}代表异步*计算的结果。提供了一些方法来检查计算是否完成,等待其完成并检索计算结果。
Future执行耗时任务
由此我们得知,Future获取得线程执行结果前,我们的主线程get()得到结果需要一直阻塞等待,即使我们使用isDone()方法轮询去查看线程执行状态,但是这样也非常浪费cpu资源。
图片来源:Java8实战
当Future的线程进行了一个非常耗时的操作,那我们的主线程也就阻塞了。 当我们在简单业务上,可以使用Future的另一个重载方法get(long,TimeUnit)来设置超时时间,避免我们的主线程被无穷尽地阻塞。 不过,有没有更好的解决方案呢?
我们需要更强大异步能力
不仅如此,当我们在碰到一下业务场景的时候,单纯使用Future接口或者FutureTask类并不能很好地完成以下我们所需的业务
- 将两个异步计算合并为一个,这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果
- 等待Future集合种的所有任务都完成。
- 仅等待Future集合种最快结束的任务完成(有可能因为他们试图通过不同的方式计算同一个值),并返回它的结果。
- 通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)。
- 应对Future的完成时间(即当Future的完成时间完成时会收到通知,并能使用Future的计算结果进行下一步的的操作,不只是简单地阻塞等待操作的结果)
正文
神奇的CompletableFuture
什么是CompletableFuture
在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,结合了Future的优点,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。
CompletableFuture被设计在Java中进行异步编程。异步编程意味着在主线程之外创建一个独立的线程,与主线程分隔开,并在上面运行一个非阻塞的任务,然后通知主线程进展,成功或者失败。
通过这种方式,你的主线程不用为了任务的完成而阻塞/等待,你可以用主线程去并行执行其他的任务。 使用这种并行方式,极大地提升了程序的表现。
Java8源码doc注释:
译文:
当一个Future可能需要显示地完成时,使用CompletionStage接口去支持完成时触发的函数和操作。
当2个以上线程同时尝试完成、异常完成、取消一个CompletableFuture时,只有一个能成功。
CompletableFuture实现了CompletionStage接口的如下策略:
1.为了完成当前的CompletableFuture接口或者其他完成方法的回调函数的线程,提供了非异步的完成操作。
2.没有显式入参Executor的所有async方法都使用ForkJoinPool.commonPool()为了简化监视、调试和跟踪,
所有生成的异步任务都是标记接口AsynchronousCompletionTask的实例。
3.所有的CompletionStage方法都是独立于其他共有方法实现的,因此一个方法的行为不会受到子类中其他
方法的覆盖。
CompletableFuture实现了Futurre接口的如下策略:
1.CompletableFuture无法直接控制完成,所以cancel操作被视为是另一种异常完成形式。
方法isCompletedExceptionally可以用来确定一个CompletableFuture是否以任何异常的方式完成。
2.以一个CompletionException为例,方法get()和get(long,TimeUnit)抛出一个ExecutionException,
对应CompletionException。为了在大多数上下文中简化用法,这个类还定义了方法join()和getNow,
而不是直接在这些情况中直接抛出CompletionException。
CompletableFuture API
想直接找例子上手的小伙伴可以跳过去后面
实例化CompletableFuture
实例化方式
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
有两种格式,一种是supply开头的方法,一种是run开头的方法
- supply开头:这种方法,可以返回异步线程执行之后的结果
- run开头:这种不会返回结果,就只是执行线程任务
或者可以通过一个简单的无参构造器
CompletableFuture<String> completableFuture = new CompletableFuture<String>();
小贴士:我们注意到,在实例化方法中,我们是可以指定Executor参数的,当我们不指定的试话,我们所开的并行线程使用的是默认系统及公共线程池ForkJoinPool,而且这些线程都是守护线程。我们在编程的时候需要谨慎使用守护线程,如果将我们普通的用户线程设置成守护线程,当我们的程序主线程结束,JVM中不存在其余用户线程,那么CompletableFuture的守护线程会直接退出,造成任务无法完成的问题,其余的包括守护线程阻塞问题我就不在本篇赘述。
Java8实战:
其中supplyAsync用于有返回值的任务,runAsync则用于没有返回值的任务。Executor参数可以手动指定线程池,否则默认ForkJoinPool.commonPool()系统级公共线程池,注意:这些线程都是Daemon线程,主线程结束Daemon线程不结束,只有JVM关闭时,生命周期终止。
获取结果
同步获取结果
public T get()
public T get(long timeout, TimeUnit unit)
public T getNow(T valueIfAbsent)
public T join()
简单的例子
CompletableFuture<Integer> future = new CompletableFuture<>();
Integer integer = future.get();
get() 方法同样会阻塞直到任务完成,上面的代码,主线程会一直阻塞,因为这种方式创建的future从未完成。有兴趣的小伙伴可以打个断点看看,状态会一直是not completed
前两个方法比较通俗易懂,认真看完上面Future部分的小伙伴肯定知道什么意思。 getNow() 则有所区别,参数valueIfAbsent的意思是当计算结果不存在或者Now时刻没有完成任务,给定一个确定的值。
join() 与get() 区别在于join() 返回计算的结果或者抛出一个unchecked异常(CompletionException),而get() 返回一个具体的异常.
计算完成后续操作1——complete
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
方法1和2的区别在于是否使用异步处理,2和3的区别在于是否使用自定义的线程池,前三个方法都会提供一个返回结果和可抛出异常,我们可以使用lambda表达式的来接收这两个参数,然后自己处理。 方法4,接收一个可抛出的异常,且必须return一个返回值,类型与钻石表达式种的类型一样,详见下文的exceptionally() 部分,更详细
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 10086;
});
future.whenComplete((result, error) -> {
System.out.println("拨打"+result);
error.printStackTrace();
});
计算完成后续操作2——handle
public <U> CompletableFuture<U> handle(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)
眼尖的小伙伴可能已经发现了,handle方法集和上面的complete方法集没有区别,同样有两个参数一个返回结果和可抛出异常,区别就在于返回值,没错,虽然同样返回CompletableFuture类型,但是里面的参数类型,handle方法是可以自定义的。
// 开启一个异步方法
CompletableFuture<List> future = CompletableFuture.supplyAsync(() -> {
List<String> list = new ArrayList<>();
list.add("语文");
list.add("数学");
// 获取得到今天的所有课程
return list;
});
// 使用handle()方法接收list数据和error异常
CompletableFuture<Integer> future2 = future.handle((list,error)-> {
// 如果报错,就打印出异常
error.printStackTrace();
// 如果不报错,返回一个包含Integer的全新的CompletableFuture
return list.size();
// 注意这里的两个CompletableFuture包含的返回类型不同
});
计算完成的后续操作3——apply
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
为什么这三个方法被称作,计算完成的后续操作2呢,因为apply方法和handle方法一样,都是结束计算之后的后续操作,唯一的不同是,handle方法会给出异常,可以让用户自己在内部处理,而apply方法只有一个返回结果,如果异常了,会被直接抛出,交给上一层处理。 如果不想每个链式调用都处理异常,那么就使用apply吧。
例子:请看下面的exceptionally() 示例
计算完成的后续操作4——accept
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
accept()三个方法只做最终结果的消费,注意此时返回的CompletableFuture是空返回。只消费,无返回,有点像流式编程的终端操作。
例子:请看下面的exceptionally() 示例
捕获中间产生的异常——exceptionally
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
exceptionally() 可以帮我们捕捉到所有中间过程的异常,方法会给我们一个异常作为参数,我们可以处理这个异常,同时返回一个默认值,跟服务降级 有点像,默认值的类型和上一个操作的返回值相同。 小贴士 :向线程池提交任务的时候发生的异常属于外部异常,是无法捕捉到的,毕竟还没有开始执行任务。作者也是在触发线程池拒绝策略的时候发现的。exceptionally() 无法捕捉RejectedExecutionException()
// 实例化一个CompletableFuture,返回值是Integer
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 返回null
return null;
});
CompletableFuture<String> exceptionally = future.thenApply(result -> {
// 制造一个空指针异常NPE
int i = result;
return i;
}).thenApply(result -> {
// 这里不会执行,因为上面出现了异常
String words = "现在是" + result + "点钟";
return words;
}).exceptionally(error -> {
// 我们选择在这里打印出异常
error.printStackTrace();
// 并且当异常发生的时候,我们返回一个默认的文字
return "出错啊~";
});
exceptionally.thenAccept(System.out::println);
}
最后输出结果
组合式异步编程
组合两个completableFuture
还记得我们上面说的Future做不到的事吗
- 将两个异步计算合并为一个,这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
thenApply()
假设一个场景,我是一个小学生,我想知道今天我需要上几门课程 此时我需要两个步骤,1.根据我的名字获取我的学生信息 2.根据我的学生信息查询课程 我们可以用下面这种方式来链式调用api,使用上一步的结果进行下一步操作
CompletableFuture<List<Lesson>> future = CompletableFuture.supplyAsync(() -> {
// 根据学生姓名获取学生信息
return StudentService.getStudent(name);
}).thenApply(student -> {
// 再根据学生信息获取今天的课程
return LessonsService.getLessons(student);
});
我们根据学生姓名获取学生信息,然后使用把得到的学生信息student传递到apply() 方法再获取得到学生今天的课程列表。
- 将两个异步计算合并为一个,这两个异步计算之间相互独立,互不依赖
thenCompose()
假设一个场景,我是一个小学生,今天有劳技课和美术课,我需要查询到今天需要带什么东西到学校
CompletableFuture<List<String>> total = CompletableFuture.supplyAsync(() -> {
// 第一个任务获取美术课需要带的东西,返回一个list
List<String> stuff = new ArrayList<>();
stuff.add("画笔");
stuff.add("颜料");
return stuff;
}).thenCompose(list -> {
// 向第二个任务传递参数list(上一个任务美术课所需的东西list)
CompletableFuture<List<String>> insideFuture = CompletableFuture.supplyAsync(() -> {
List<String> stuff = new ArrayList<>();
// 第二个任务获取劳技课所需的工具
stuff.add("剪刀");
stuff.add("折纸");
// 合并两个list,获取课程所需所有工具
List<String> allStuff = Stream.of(list, stuff).flatMap(Collection::stream).collect(Collectors.toList());
return allStuff;
});
return insideFuture;
});
System.out.println(total.join().size());
我们通过CompletableFuture.supplyAsync() 方法创建第一个任务,获得美术课所需的物品list,然后使用thenCompose() 接口传递list到第二个任务,然后第二个任务获取劳技课所需的物品,整合之后再返回。至此我们完成两个任务的合并。 (说实话,用compose去实现这个业务场景看起来有点别扭,我们看下一个例子)
- 将两个异步计算合并为一个,这两个异步计算之间相互独立,互不依赖
thenCombine()
还是上面那个场景,我是一个小学生,今天有劳技课和美术课,我需要查询到今天需要带什么东西到学校
CompletableFuture<List<String>> painting = CompletableFuture.supplyAsync(() -> {
// 第一个任务获取美术课需要带的东西,返回一个list
List<String> stuff = new ArrayList<>();
stuff.add("画笔");
stuff.add("颜料");
return stuff;
});
CompletableFuture<List<String>> handWork = CompletableFuture.supplyAsync(() -> {
// 第二个任务获取劳技课需要带的东西,返回一个list
List<String> stuff = new ArrayList<>();
stuff.add("剪刀");
stuff.add("折纸");
return stuff;
});
CompletableFuture<List<String>> total = painting
// 传入handWork列表,然后得到两个CompletableFuture的参数Stuff1和2
.thenCombine(handWork, (stuff1, stuff2) -> {
// 合并成新的list
List<String> totalStuff = Stream.of(stuff1, stuff1)
.flatMap(Collection::stream)
.collect(Collectors.toList());
return totalStuff;
});
System.out.println(JSONObject.toJSONString(total.join()));
- 等待Future集合中的所有任务都完成。
获取所有完成结果——allOf
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
allOf方法,当所有给定的任务完成后,返回一个全新的已完成CompletableFuture
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
try {
//使用sleep()模拟耗时操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
return 2;
});
CompletableFuture.allOf(future1, future1);
// 输出3
System.out.println(future1.join()+future2.join());
获取率先完成的任务结果——anyOf
- 仅等待Future集合种最快结束的任务完成(有可能因为他们试图通过不同的方式计算同一个值),并返回它的结果。 小贴士 :如果最快完成的任务出现了异常,也会先返回异常,如果害怕出错可以加个exceptionally() 去处理一下可能发生的异常并设定默认返回值
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
throw new NullPointerException();
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
try {
// 睡眠3s模拟延时
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
CompletableFuture<Object> anyOf = CompletableFuture
.anyOf(future, future2)
.exceptionally(error -> {
error.printStackTrace();
return 2;
});
System.out.println(anyOf.join());
几个小例子
多个方法组合使用
- 通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)。
- 应对Future的完成时间(即当Future的完成时间完成时会收到通知,并能使用Future的计算结果进行下一步的的操作,不只是简单地阻塞等待操作的结果)
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> 1)
.whenComplete((result, error) -> {
System.out.println(result);
error.printStackTrace();
})
.handle((result, error) -> {
error.printStackTrace();
return error;
})
.thenApply(Object::toString)
.thenApply(Integer::valueOf)
.thenAccept((param) -> System.out.println("done"));
}
循环创建并发任务
public static void main(String[] args) {
long begin = System.currentTimeMillis();
// 自定义一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 循环创建10个CompletableFuture
List<CompletableFuture<Integer>> collect = IntStream.range(1, 10).mapToObj(i -> {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 在i=5的时候抛出一个NPE
if (i == 5) {
throw new NullPointerException();
}
try {
// 每个依次睡眠1-9s,模拟线程耗时
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
return i;
}, executorService)
// 这里处理一下i=5时出现的NPE
// 如果这里不处理异常,那么异常会在所有任务完成后抛出,小伙伴可自行测试
.exceptionally(Error -> {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 100;
});
return future;
}).collect(Collectors.toList());
// List列表转成CompletableFuture的Array数组,使其可以作为allOf()的参数
// 使用join()方法使得主线程阻塞,并等待所有并行线程完成
CompletableFuture.allOf(collect.toArray(new CompletableFuture[]{})).join();
System.out.println("最终耗时" + (System.currentTimeMillis() - begin) + "毫秒");
executorService.shutdown();
}
使用CompletableFuture场景
- 执行比较耗时的操作时,尤其是那些依赖一个或多个远程服务的操作,使用异步任务可以改善程序的性能,加快程序的响应速度
- 使用CompletableFuture类,它提供了异常管理的机制,让你有机会抛出、管理异步任务执行种发生的异常
- 如果这些异步任务之间相互独立,或者他们之间的的某一些的结果是另一些的输入,你可以讲这些异步任务构造或合并成一个
小贴士 :测试多线程的小伙伴请勿使用JUit单元测试,因为JUnit在主线程完成之后就会关闭JVM,有兴趣的小伙伴请自行百度
结语
用就vans了
这篇博客不是特别的完整,很多CompletableFuture的成员方法没包含进去,不过日常开发已经足够 如果文章中有错误的地方,望不吝赐教,感激不尽!
16、ThreadLocal
ThreadLocal解析:https://mp.weixin.qq.com/s/g9XcPjVQMsD8i2b8r-zW0Q