Java并发之JUC上

JUC

1、卖票问题

1.1synchronized(自动挡)

多线程保证:

1、资源类就是纯的资源类,不去实现Rrunnable接口

2、方法内部的细节必须写在方法内部(高内聚) 创建的多线程只调用资源类的方法

3、创建一个资源类对象,多个线程去操作一个资源类

public class 卖票复习 {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                ticket.saleTicket();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                ticket.saleTicket();
            }
        }, "B").start();
    }
}

//
class Ticket {

    private int number = 1000;

    public synchronized void saleTicket() {
        if (number > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "买到了第" + number-- + "张票还剩下" + number + "张票");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1.2Lock(手动挡)

  • Lock是一个接口
  • 定义了最常用的加锁lock()解锁unlock()
  • 一般使用:
lock();//上来先锁住,下面是业务逻辑
	try{
        xxx业务逻辑
    }finally{
        unlock(); //解锁操作必须放在finally块里 保证不会死锁
    }
  • 其典型的实现 ReentrantLock 可重入锁 默认是非公平锁

使用Lock锁实现同步

public class 卖票Lock {

    public static void main(String[] args) {

        TicketL ticketL = new TicketL();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {

                ticketL.saleTic();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ticketL.saleTic();
            }
        }, "B").start();
    }
}

class TicketL {
    //创建一个可重入锁
    Lock reenlock = new ReentrantLock();

    private int num = 100;

    public void saleTic() {
        reenlock.lock(); //加锁
        if (num > 0) {
            try {
                num--;
                System.out.println("剩下"+num+"张票");
            } finally {
                reenlock.unlock(); //解锁
            }
        }
    }

}

2、生产者消费者问题

1.1wait notify notifyAll(防止虚假唤醒)

  • 设计到的三个方法:必须只能写在同步代码块中或者同步方法中 同步代码块的监视器必须和调用wait方法的对象是一个 否则报异常
  • wait() 一旦执行此方法,当前线程就进入阻塞状态,并且释放已获得的锁意味着其他的线程可以进入同步代码块
  • notify() 执行此方法,会==随机==唤醒一个wait()的线程,如果有多个线程被wait,就唤醒优先级高的
  • notifyAll() 唤醒==所有==正在wait()的进行
  • 这三个方法都是定义在java.lang.Object中的 且不能使用在lock锁的代码里面
  • synchronized wait notify notifyAll 这四个必须在一起使用

wait与sleep的区别

  • 1、sleep和wait定义的位置不同 sleep()在Thread类中,wait()是Object类中的方法
  • 2、sleep()是静态方法,wait()方法是非静态方法
  • 3、调用范围不同,sleep是在任何需要的场景下调用,wait()只能在同步方法或者同步代码块中调用
  • 4、sleep()调用不会释放同步监视器(锁)wait()会释放同步监视器

生产者消费者问题演示实现多个线程交替打印1和0

虚假唤醒的问题:

  • 必须使用while进行判断条件,不能使用if判断
  • 因为我们在判断条件的时候,假设现在A(加1)线程来了,判断这时的data是1,就wait()释放锁并且阻塞,,然后又有一个B(加一)线程来了,经过判断也wait()阻塞,,注意这时这两个线程都已经进入了方法判断代码块内,然后一个(减一)线程将data的值减了一,后唤醒了所有wait()的线程,注意如果这时我们使用的是if的话,这两个线程就不会再次判断data的值了,直接进行后续的加1,所以导致多加(或类似的多减的操作),这跟if的判断有关,因为if只会判断一次,而while会进行多次循环判断,此时两个加一线程都被唤醒了,因为第一次不满足条件,那么就还会去判断data的值,最后只有一个线程才能完成加1,另一个则继续wait()
public class 生产者消费者复习 {

    public static void main(String[] args) {

        Num num = new Num();

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                num.decrement();
            }
        },"d1").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                num.decrement();
            }
        },"d2").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                num.increment();
            }
        },"i1").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                num.increment();
            }
        },"i2").start();
    }
}

class Num {

    int data = 0;

    public synchronized void increment() {
        while (data != 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        data++;
        System.out.println(Thread.currentThread().getName() + "我已加一完毕" + data);
        //通知 其他线程加一完毕
        notifyAll();
    }

    public synchronized void decrement() {
        //必须使用while判断,循环判断
        // if只会判断一次 阻塞后不会判断,直接就去加或减,就出现了多加多减的问题
        while (data == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        data--;
        System.out.println(Thread.currentThread().getName() + "我已减一完毕" + data);
        notifyAll();
    }
    public void test() throws InterruptedException {
        synchronized (Class.class) {
            System.out.println("xx");
            wait();
        }
    }
}

1.2新版(Lock)精确唤醒await() signal()

  • Lock锁要实现生产者消费者只能使用Condition接口中的 await() signal() 方法
  • 可以实现精准的唤醒 notify和notifyAll都不可以,因为notify是随机唤醒一个等待的线程,notifyAll是唤醒所有等待的线程,无法实现精准的通知唤醒
  • 新版的写法步骤 判断->业务->修改标志位/唤醒

新版代码实现

三个线程ABC交替打印ABC字符串,要求线程执行顺序是A,B,C 这时就需要精准的唤醒A打印完通知B,B打印完通知C,C打印完通知A,都需要修改标志位

思路:定义一个标志位,当flag是"A"时,A打印,打印完修改flag为"B",并唤醒B,B打印完修改flag为"C",C打印完修改flag为"A",并唤醒A …

public class Resource {

    private Lock lock = new ReentrantLock();
    //标志位
    private String flag = "A";
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    public void printA() {
        //加锁
        lock.lock();
        //也有tryLock()方法,返回Boolean值,尝试加锁,要么加锁成功,要么直接失败,不等待
        try {
            //判断
            while (!"A".equals(flag)) {
                conditionA.await();
            }
            //业务
            System.out.println("A");
            //修改标志位,精准唤醒
            flag = "B";
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (!"B".equals(flag)) {
                conditionB.await();
            }
            System.out.println("B");
            flag = "C";
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            while (!"C".equals(flag)) {
                conditionC.await();
            }
            System.out.println("C");
            flag = "A";
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

class Test {
    public static void main(String[] args) {
        Resource resource = new Resource();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.printC();
            }
        }, "ThreadName").start();


        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.printB();
            }
        }, "ThreadName").start();


        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource.printA();
            }
        }, "ThreadName").start();
    }
}

3、8锁问题

// (假设所有的方法的执行时间(不睡眠)都是相同的)
class Resource(){
    
    //同步方法m1
    public synchronized void syncm1(){     
        xxxx...
    }
    
    //同步方法m2
    public synchronized void syncm2(){
        xxxx...
    }
    
    //普通方法m3
    public viod normm3(){
        xxx...
    }
    
    //静态同步方法m4
    public static synchronized void stsyncm4(){
        xxx...
    }
    
      //静态同步方法m5
    public static synchronized void stsyncm5(){
        xxx...
    }
}

1.一个对象,两个线程,一个线程t1调用syncm1, 另一个线程t2调用syncm2,主线程睡眠1秒,执行情况? (假设所有的方法的执行时间(不睡眠)都是相同的)

t1->syncm1
主线程sleep 1秒
t2->syncm2

解释

syncm1先执行,并且syncm1先执行完毕 如果主线程不休眠1秒的话,syncm1和syncm2都有可以先执行,因为CPU调度我们是无法确定的,但是我们这里的主线程睡眠了1秒,保证了t1线程会先被CPU调度,就保证了syncm1会先被执行,且syncm1和syncm2都是非静态同步方法,所以说syncm1执行完毕才会执行syncm2。

2.一个对象 两个线程 一个线程t1调用syncm1(syncm1方法睡眠4秒) 另一个线程t2调用syncm2,主线程睡眠1秒,执行情况如何? (假设所有的方法的执行时间(不睡眠)都是相同的)

t1->syncm1 (m1方法sleep4秒)
主线程sleep 1秒
t2->syncm2    

解释

syncm1先执行,并且syncm1先执行完毕 因为主线程sleep1毫秒,保证了t1线程被cpu优先调度,又因为m1和m2都是同步方法,所以t2线程即使被cpu调度,但是必须等待t1线程释放锁后才能执行m2方法,即非静态同步方法的synchronized锁住的是当前对象,即当前对象的所有(非静态)同步方法

3.一个对象,两个线程,一个线程t1调用syncm1(syncm1方法睡眠4秒),另一个线程t2调用normm3,执行情况如何? (假设所有的方法的执行时间(不睡眠)都是相同的)

t1->syncm1 (m1方法sleep4秒)
主线程sleep 1秒
t2->normm3

解释

syncm1先执行,但是先执行完毕的是normm3 跟前两种情况一致,由于主线程睡眠了1秒,所以线程t1先被CPU调度执行,过了一秒后线程t2被线程调度,但是因为normm3方法不是同步方法,在调用时不需要判断当前线程是否获得了对象锁,也就不需要等待t1线程结束释放锁就可以直接执行normm3方法

4、两个对象,两个线程,一个线程t1调用syncm1,另一个线程t2调用syncm2,主线程睡眠1秒,执行情况如何? (假设所有的方法的执行时间(不睡眠)都是相同的)

re1.t1->syncm1
主线程睡眠1秒
re2.t2->syncm2

解释

syncm1先执行,且syncm1先执行完毕 因为两个对象,锁就不一样了,这时就是两把锁,因为我们让主线程睡眠了1秒,所以t1线程先被CPU调度,即syncm1先被执行,等到主线程睡眠完毕,不等t1结束就直接可以执行t2线程,因为是两把锁,因为我们假设了方法的执行时间都相同,所以syncm1会先执行完毕

前四种的总结

  • 一个对象里有多个同步方法,在某一时刻,多个线程A B同时调用同步方法时,锁的不是当前的方法,而是整个对象 即只有一个线程可以拿到对象锁去执行同步方法,其他的线程必须等到锁的释放才能去争抢锁,即使执行的不是同一个同步方法
  • 即非静态的同步方法的锁是当前的对象(this)
  • 普通的方法静态同步方法不受对象锁的控制,执行普通方法的线程不需要去争抢锁就可以执行方法,而静态的同步方法的锁是当前的类对象(Class对象)
  • 所以说不同的对象就是不同的锁

5.一个对象,两个线程,线程t1执行staticsyncm4,线程t2执行staticsyncm5,执行情况如何?(假设所有的方法的执行时间(不睡眠)都是相同的)

t1->staticsyncm4
主线程睡眠4秒
t2->staticsyncm5 

6.两个对象,两个线程,线程t1执行staticsyncm4,线程t2执行staticsyncm5,执行情况如何?(假设所有的方法的执行时间(不睡眠)都是相同的)

r1.t1->staticsyncm4
主线程睡眠4秒
r2.t2->staticsyncm5 

解释

情况5 和 6 都是t1线程先执行,且t1线程先执行完毕,t2线程会等待t1的结束才会执行 因为是静态同步方法,此时的锁是类对象(即类锁),所以说,不管你有几个对象,只要你调用的是静态的同步方法,就必须等待锁的释放,所以5 6这两种情况都是 t2线程会等待t1线程执行结束时才会执行staticsyncm5

7.一个对象,两个线程,线程t1调用staticsyncm4,线程t2调用syncm1,主线程睡眠1秒,执行情况如何?(假设所有的方法的执行时间(不睡眠)都是相同的)

t1->staticsyncm4
主线程睡眠4秒
t2->syncm1

解释

t1线程会先执行,且t1线程先执行完毕,t2线程不会等待t1的结束才执行 因为一个是静态同步方法,一个是非静态同步方法,即锁不一样,一个是对象锁,一个是类锁。

8.两个对象,两个线程,t1执行staticsyncm4,t2执行syncm1,执行情况如何?(假设所有的方法的执行时间(不睡眠)都是相同的)

r1.t1->staticsyncm4
主线程睡眠4秒
r2.t2->syncm1

解释

t1线程先执行,且t1线程先执行完毕,t2线程不会等待t1的结束才执行 一个是静态同步方法–有锁(类锁),,一个是非静态同步方法(对象锁), 两个锁,互不影响

总结:

  • synchronized是实现同步的基础,Java中的每一个对象(包括类对象xx.class)都可以作为锁

    1.对于非静态同步方法,锁是当前对象(this)

    2.对于同步代码块,锁是括号里的对象

    3.对于静态的同步方法,锁是当前类对象 (xx.class)

  • ----(核心)当一个线程试图访问同步方法或者同步代码块时不管多个线程访问的是不是同一个同步方法首先必须得到锁,当一个线程正常执行完同步方法或代码块时或者是抛出异常时必须释放锁

  • 类锁和对象锁不是一把锁,不同的对象锁也不是同一把锁,对象锁有很多(一个对象一把对象锁),但是所有的静态同步方法只有一把锁(类锁) 即一个类只有一把类锁,有很多把对象锁

  • 锁的不是方法,是对象!!!

在这里插入图片描述

4、集合不安全

1.List不安全

  • ArrayList是线程不安全的,在多线程下的修改会报异java.util.ConcurrentModificationException

解决方案

  • 1、改用Vector
  • 2、使用Collections工具类中的synchronizedList()方法,将ArrayList转成一个线程安全的SynchronizedList类的对象,也是List接口的间接实现类
  • 3、使用CopyOnWriteArrayList类代替ArrayList,写时复制

一般在高并发下我们只用第三种,使用CopyOnWriteArrayList类 读数据时不加锁,在写数据时(删除 添加 修改)时先复制原数组并加Lock锁

底层也是一个Object类型的数组

private transient volatile Object[] array;

add()方法的源码:

大致就是先去复制一个原来的数组,长度加1,然后对新数组进行操作,操作之后使用set方法替换原数组 使用的是Lock锁(JDK1.8)

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //获取原数组
        Object[] elements = getArray();
        //获取原长度
        int len = elements.length;
        //基于原数组复制一个新数组,长度+1 
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将添加的元素放到新数组最后的位置
        newElements[len] = e;
        //新数组替换原数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

2.Set不安全

  • 使用Collectios工具类的synchronizedSet方法将HashSet转为线程安全的
  • 使用CopyOnWriteArraySet类代替HashSet

3.Map不安全

复习:

HashMap底层是Node类型的数组,默认长度是16,默认负载因子是0.75 默认最大长度是2的30次方,扩容是2倍,

使用线程安全的HashMap的方法

  • 使用Collectios工具类的synchronizedMap方法将HashMap转为线程安全的
  • 使用HashTable,线程安全,古老的集合(一般不用)
  • 使用ConcurrentHashMap替换HashMap

​ 1、ConcurrentHashMap不能存储null的key和value

​ 2、使用了分段锁技术 效率高

5、Callable接口实现多线程

  • JDK5的新特性,Callable接口(异步任务)+FutureTask实现多线程
  • Callable接口源码: 使用Callable实现多线程异步任务可以获取返回结果并捕获异常
//是一个函数式接口,带有一个泛型V,是方法的返回值类型,只有一个方法call(),抛出了异常,也即我们可以在外部获取异步任务的异常 
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

//与Runnable接口对比,run方法没有返回值,不能在外部捕获异常
@FunctionalInterface
public interface Runnable {
   public abstract void run();
}

​ FutureTask与Callable接口、Runnable接口的关系及常用方法

FutureTask与Runnable接口的关系,可以发现,FutureTask就是Runnable接口的实现类

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable

源码:FutureTask与Callable接口的关系,Callable作为FutureTask的一个属性,通过构造器传入Callable的实现类对象,从而可以在run方法里调用我们Callable的方法

//FutureTask的一个属性
private Callable<V> callable;
	
//FutureTask的构造器
 public FutureTask(Callable<V> callable) {
      this.callable = callable;
}
//在FutureTask重写的run方法里调用了call方法
 public void run() {
  result = c.call();
 }

重要方法get()

我们可以通过FutureTask对象的get()方法获取异步任务的返回值捕获异步任务出现的异常,注意:此方法是阻塞方法,会阻塞当前线程一直等到异步任务结束拿到返回值

代码实现

  • 分析

我们知道,开启一个线程一般都需要入我们Runnable接口的实现类,然后使用Thread类的start()方法,先开启一个异步线程,然后自动调用run()方法执行异步方法 . 所以我们现在有Runnable的实现类(FutureTask),我们将异步任务Callable接口的实现类通过构造器传到FutureTask里后,直接将FutureTask类的对象通过Thread类的构造器传入,然后直接调用start()方法即可开启多线程

public static void main(String[] args) {
    	//函数式接口 使用Lambda表达式 指明返回值类型是Integer
        Callable<Integer> calllable = ()-> 10 / 1;
    	//将异步任务传入FutureTask中
        FutureTask<Integer> task = new FutureTask<>(calllable);
    	//直接将task传到Thread类中,调用start开启任务
        new Thread(task).start();
        try {
            //调用get方法获取异步任务返回值,并可以捕获异步任务的异常
            //阻塞方法,会一直阻塞等待异步任务的返回值结果线程才会继续往下执行
            Integer result = task.get();
            xxx....
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

6、线程池与CompletableFuture

6.1引言

在真实的业务中,前三种开启多线程的方式都不会使用,本质上就是一种。

new Thread(Runnable r).start();

我们在真实业务中,使用的基本都是线程池,并且保证一个系统基本上只有一到两个线程池,系统将所有的异步任务都交给线程池去处理.可以控制系统的稳定性(稳定性永远是第一位,高并发写的在牛逼,运行几秒系统崩了也没卵用)。因为前三种方式频繁的创建销毁造成系统的资源不必要开销,并且一直无休止的开启线程可能会造成资源耗尽,直接导致系统崩溃。

开发中为什么使用线程池

1、降低资源的消耗

2、提高响应速度

3、提高线程的可管理性

6.2线程池的创建及七大参数详解

线程池本质上就是ThreadPoolExecutor这个类,看一下它的继承及实现关系

public class ThreadPoolExecutor extends AbstractExecutorService
public abstract class AbstractExecutorService implements ExecutorService
public interface ExecutorService extends Executor
 //最顶级的接口 里面只定义了 void execute(Runnable command) 方法
public interface Executor

6.2.1七大参数及原生创建

  • int corePoolSize

    线程池中的核心线程数量,会一直保留在池中的(即使它们处于空闲状态),除非设置了allowCoreThreadTimeOut这个参数。
    
  • int maximumPoolSize

    线程池中最大可以开的线程数量
    
  • long keepAliveTime

    非核心|空闲线程的超时时间,超过这个时间空闲线程将被回收

  • TimeUnit unit

    时间单位 上面超时时间的时间单位

    //传入TimeUnit枚举类中的对象
    public enum TimeUnit {
      
        NANOSECONDS {
        },
     
        MICROSECONDS {
        },
    
        MILLISECONDS {
        },
        
        SECONDS {
        },
        ....
    
  • BlockingQueue<Runnable> workQueue

    阻塞队列,将多余的任务放入阻塞队列中,如果线程空闲了,就去队列中取出任务执行
    ==注意:==默认创建的队列的长度是Integer的最大值,我们必须显示的指定队列的长度

  • ThreadFactory threadFactory

    线程池中线程创建的工厂,有各个线程创建的细节,比如指定线程的名字等
    
  • RejectedExecutionHandler handler

    拒绝策略,当队列中的任务满了在来的任务就会走拒绝策略

    //四种拒绝策略
    DiscardOldestPolicy   --->丢弃队列中最老(最先入队)的任务
    AbortPolicy           --->直接丢弃新来的任务 抛出异常 (默认的)
    CallerRunsPolicy      --->直接调用run方法,相当于同步方法
    DiscardPolicy         --->直接丢弃新来的任务 不抛出异常
    

举例:创建原生线程池

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
        100,
        200,
        2,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue(1000), //一定要指定队列长度,否则默认是Integer最大值
        Executors.defaultThreadFactory(), 
        new ThreadPoolExecutor.AbortPolicy());

6.3使用Excutors工具类创建线程池

在这里插入图片描述

6.4线程池的工作流程

1、线程池创建,准备好core数量的核心线程,准备接收任务

2、当核心线程全部都在执行任务,将在来的任务直接放入阻塞队列中(注意,可不是直接在开线程),当某一个核心线程执行完任务了就去队列中拿任务去执行。

3、当队列中的任务也满了,这时才开启新的线程执行任务,最大只能开到max最大数量的线程数

4、当所有的线程全在执行任务,队列中的任务也满了,这时在来的任务就要走拒绝策略

5、当所有的任务全部执行完毕,经过了keepAliveTime时间没有新的任务,释放max-core这些线程。 (当设置了allowCoreThreadTimeOut(true)时,核心的线程空闲时间达到keepAliveTime也会被回收)
在这里插入图片描述

练习:一个线程池,core:7 max:20 queue:50 100个任务怎样分配

7个任务立即执行,50个进入队列,然后在开20-7=13个线程执行(这时队列中的任务已满及所有的线程全部都在执行任务),即7 + 50 +13= 70个线程已经安排上了,剩下的30个直接走丢弃策略

6.5使用线程池执行任务

------------------------执行Callable任务 submit()方法-------------------
Callable<String> callabe = (()->{
    TimeUnit.SECONDS.sleep(2);
    return "Hello World";
});
//调用submit()方法传入一个Callable任务,相当于start()
//返回一个Future接口的实现类对象 
//Futuretask类是Future接口的一个实现类 
Future<String> task = poolExecutor.submit(callabe);
//通过此对象调用get()方法可以获取任务的返回值并且感知异常
String s = task.get();

------------------------------执行Runnable任务 execute()方法------------
    Runnable runnable = () -> {
    System.out.println("xxx");
   };

//执行任务,相当于start()方法 没有返回值
poolExecutor.execute(runnable);

----------关闭线程池(一般不使用)----------
   poolExecutor.shutdown(); 

6.6CompletableFuture(1.8)异步编排

异步编排的场景:有很多异步任务,比如 A B C,且三个任务之间有某种关系,比如B任务要获取A的结果后才能执行等,这时我们就必须编排任务

//源码声明:
public class CompletableFuture<T> implements Future<T> 
   //Future接口可以获取异步结果 FutureTask类也实现了Future接口

6.6.1创建异步任务对象

调用方法都返回CompletableFuture对象,通过这个对象可以调用get()方法阻塞拿到异步任务的返回值(如果有)但是不管有没有返回值都会阻塞等待异步任务的执行结束,还可以编排下一步要执行的任务 进行异步编排 底层还是使用的线程池去执行的任务

--------有返回值的------上面的都是使用默认的线程池执行,下面的是使用自定义线程池执行-----
//泛型U就是返回值的类型
1public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
    
 //传入一个供给型接口,(无参有返回值) 类似于Callable接口(空参有返回值) 使用自定义线程池 (第二个方法)
//将任务提交给自定义线程池去执行
2public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

--------------------没有返回值的 相当于run方法的异步------------------------------
public static CompletableFuture<Void> runAsync(Runnable runnable)

    //使用自定义线程池,将任务提交给线程池去执行
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
                                               
supplyAsync测试
//创建了一个异步任务去执行,使用自定义的线程池, 返回一个CompletableFuture对象
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2, poolExecutor);
//阻塞当前线程等待异步任务的结果
Integer result = future.get();
System.out.println(result);
runAsync测试
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("xxx"), poolExecutor);
//阻塞等待异步任务执行完毕 
future.get();

6.6.2计算完成时的回调

1、whenComplete系列

在方法执行结束(正确执行)时只能获取结果和异常信息,不能对结果进行操作

whenComplete和whenCompleteAsync的区别:

whenComplete:使用当前任务的线程继续执行whenComplete的任务

whenCompleteAsync:将whenCompleteAsync的任务继续提交给线程池来执行

//两个参数 第一个是任务的返回值,第二个是任务出现的异常信息  在计算完成时跟任务使用(线程池中的)同一个线程执行
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)

举例应用

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 10 / 0, poolExecutor).
whenComplete((result,exception)->{
    //只能得到任务的结果和异常信息,出异常后不能修改结果  
    //得到返回值后做一些操作
System.out.println("返回值结果是"+result+"异常信息"+exception);
    //一些操作 ......
}).exceptionally(exception->{
    //感知到异常后可以指定一个默认值 做一些操作
    return 23;
    // 一些操作 ......
});
2、handle方法

方法完成后的处理,可以对结果进行操作

//跟whenComplete一样,传入一个结果 一个异常信息 但是多了一个返回值,就是操作结果的
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 10 / 0, poolExecutor).handle((res,exr)->{
    //判断任务是否有结果返回  
    if (res!=null){
        return res;
    }
    //判断是否出现了异常 然后写出现异常后的逻辑 然后可以指定一个结果
    if (exr!=null){
        .....业务
        return 0;
    }
    return 0;
});

6.6.3线程串行化(编排)单任务

规定了上一个异步任务(A)执行结束后新任务(B)如何执行,对任务进行编排

方法后面带了Async就是新任务使用新的线程执行,不带Async就是与原任务使用同一个线程执行一般都使用第三个方法,指定使用我们自己的线程池

**最后返回CompletableFuture的对象的泛型就是本次异步任务的返回值类型,就是最后一个任务的返回值类型,在中间执行时,可以改变返回值的类型**

总共分为三种情况:

1.新任务B不接收上个任务A的返回值,并且新任务B也没有返回值

传入一个Runnable任务,即不接收上一步任务的返回值,新任务也没有返回值 (run()方法的特性) (void run())

public CompletableFuture<Void> thenRun(Runnable action)

public CompletableFuture<Void> thenRunAsync(Runnable action)

public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executo)

2.新任务B接收上个任务A的返回值,但是新任务B没有返回值

传入一个Consumer接口的实现类,接收上一任务的返回值,但是新任务没有返回值,消费型接口的特性 (void accept(T t))

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) {

3.新任务B接收上个任务A的返回值,并且新任务B有返回值

传一个函数型接口的实现类,接受上一任务的返回值,并且新任务有返回,函数型接口的特性

(R apply(T t))

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)

代码演示:

CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 10 / 3, executor);
CompletableFuture<Void> task2 = task1.thenRunAsync(() -> System.out.println("Runnable任务已启动"), executor);

CompletableFuture<Void> task3 = task1.thenAcceptAsync(res->System.out.println("我已获取任务的返回值"+res+"本任务已启动"),executor);

CompletableFuture<Integer> task4 = task1.thenApplyAsync(res -> {
    System.out.println("已接收返回值" + res);
    return res * 2; //有返回结果
}, executor);

6.6.4两任务组合–全部完成

就是编排一个任务C,要求必须等待A任务和B任务全部执行结束才能执行C

解释:

  • 方法的调用者相当于任务A
  • 方法的第一个参数CompletionStage就是CompletableFuture实现的一个接口,就是一个异步任务任务对象(相当是任务B),
  • 第二个参数是两任务执行结束后要执行的任务(任务C),

分为三种

1、任务C不接收A和B的返回值,只要等到A和B结束完成之后就可以执行C,并且C没有返回值

public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,
                                                     Runnable action,
                                                     Executor executor)

2、任务C接收A和B的返回值,等到A和B结束完成之后拿到A和B的返回值就可以执行C,但是C没有返回值

public <U> CompletableFuture<Void> thenAcceptBoth(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action)
    
public <U> CompletableFuture<Void> thenAcceptBothAsync(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action)
    
public <U> CompletableFuture<Void> thenAcceptBothAsync(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action,
    Executor executor)

3、任务C接收A和B的返回值,等到A和B结束完成之后拿到A和B的返回值就可以执行C,并且C有返回值

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

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

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

代码示例

CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 10 / 3, executor);
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "任务二已经启动", executor);

task1.runAfterBothAsync(task2, () -> System.out.println("任务三已经启动"), executor);

task1.thenAcceptBothAsync(task2, (res1,res2)->System.out.println("任务四已经启动"),executor);

//接收返回值,并且自己也有返回值
task1.thenCombineAsync(task2,(res1,res2)->3,executor);

6.6.5两任务组合–一个完成

就是新任务C只要等到A和B任意一个任务结束就可执行 保证A和B任务返回值的类型是相同的

三种情况

1、只要等到A和B任意一个任务结束后就可执行C,且C不接收A或B的返回值,且C也没有返回值

public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,
                                              Runnable action)

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
                                                   Runnable action) 
    
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
                                                   Runnable action,
                                                   Executor executor)

2、只要等到A和B任意一个任务结束后就可执行C,C接收A或B的返回值,但是C没有返回值

public CompletableFuture<Void> acceptEither(
    CompletionStage<? extends T> other, Consumer<? super T> action)

public CompletableFuture<Void> acceptEitherAsync(
    CompletionStage<? extends T> other, Consumer<? super T> action)
    
public CompletableFuture<Void> acceptEitherAsync(
    CompletionStage<? extends T> other,
    Consumer<? super T> action,
    Executor executor)

3、只要等到A和B任意一个任务结束后就可执行C,C接收A或B的返回值,且C有返回值

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

public <U> CompletableFuture<U> applyToEitherAsync(
    CompletionStage<? extends T> other,
    Function<? super T, U> fn)
    
public <U> CompletableFuture<U> applyToEitherAsync(
    CompletionStage<? extends T> other,
    Function<? super T, U> fn,
    Executor executor)

代码示例

//要求两个任务的返回值类型必须相同,如果两个任务的返回值不同,系统无法判断
CompletableFuture<Object> task1 = CompletableFuture.supplyAsync(() -> 10 / 3, executor);
CompletableFuture<Object> task2 = CompletableFuture.supplyAsync(() -> "任务二已经启动", executor);

//第一种
task1.runAfterEitherAsync(task2, () -> System.out.println("任务三已经启动"), executor);

//第二种
task1.acceptEitherAsync(task2,res->System.out.println("任务四已经启动"),executor);

//第三种
task1.applyToEitherAsync(task2,res->3,executor);

6.6.6多任务组合(获取所有任务的结果)

使用场景 同时等待所有任务的结果 保证所有的任务都执行完毕 以保证下面代码的正确性

//get()方法是一个阻塞方法,下面如果我们想获取所有异步任务的结果,如果都调get()的话,B的返回值必须等到A的get方法结束才会调,是一个同步的过程,浪费了时间,我们想要让获取多个任务的返回值还是一个异步的过程就需要还是用下面的方法
A.get(); //阻塞,B.get()方法无法执行 浪费了时间 
B.get();
....

//异步获取所有的结果,使用返回的CompletableFuture对象调用get方法进行阻塞等待所有的结果完成,等待所有结果完成后才结束阻塞
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) 

 //只要有一个任务完成,就结束阻塞
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

代码举例

//task1.get();
//task2.get();
//task3.get();
//task4.get();
---------异步等待所有的任务全部执行结束 下面方可使用任务的结果--------
CompletableFuture<Void> mainTask = CompletableFuture.allOf(task1, task2, task3, task4);
//主要还是调get()方法,阻塞获取所有的结果 
mainTask.get();
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shstart7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值