JUC初级

目录

一、概述

1.1什么是JUC

1.2进程与线程

1.2.1线程的状态

1.2.2wait和sleep

1.2.3并发和并行

1.2.4管程

1.2.5用户线程和守护线程

二、Lock接口

2.1 Synchronized

2.2Lock接口,实现手动加锁和释放锁

2.2.1卖票的例子

2.3接口中的方法

三、线程间的通信

3.1synchronized实现

3.2Lock实现

3.3线程间定制化通信

四、集合的线程安全

4.1集合操作 Demo

4.2解决

4.2.1vector

4.2.2通过Collections工具类

 4.2.3通过JUC中CopyOnWriteArrayList

4.3HashSet和HashMap线程不安全

4.3.1Set

4.3.2Map

五、多线程锁

5.1公平锁与非公平锁 

5.1.1非公平锁

5.1.2公平锁

5.2可重入锁

5.2.1synchronized演示

5.2.2Lock演示

5.3死锁

5.3.1死锁的原因

5.3.2死锁的例子

六、Callable&Future 接口

6.1Callable 接口

6.2Future 接口

​编辑

6.3FutureTask 

七、JUC 三大辅助类

7.1 减少计数 CountDownLatch

7.1.1demo

7.2循环栅栏 CyclicBarrier 

7.2.1demo 

7.3 信号灯 Semaphore

7.3.1demo

八、读写锁

8.1 读写锁介绍

8.2 ReentrantReadWriteLock

8.3 入门案例

8.4 小结(重要)

九、阻塞队列

9.1BlockingQueue

9.1.1方法

9.1.2demo

9.2常见的 BlockingQueue

9.2.1 ArrayBlockingQueue(常用)

9.2.2 LinkedBlockingQueue(常用)

9.2.3 DelayQueue

9.2.4 PriorityBlockingQueue

9.2.5 SynchronousQueue

9.2.6 LinkedTransferQueue

9.2.7 LinkedBlockingDeque

十、线程池

10.1简介

10.2线程池的使用方式

10.2.1一池N线程

10.2.2一池一线程

10.2.3可扩容

10.3线程池参数

10.3.1 拒绝策略(重点)

10.4线程池底层工作原理(重要)

10.5自定义线程池

十一、Fork/Join分支合并框架

11.1简介

11.2Fork 方法的实现原理

11.3join 方法

11.4 Fork/Join 框架的异常处理

11.5案例

十二、异步调用

12.1CompletableFuture

12.1.1简介

12.1.2Future 与 CompletableFuture

12.2使用 CompletableFuture

12.2.1无返回值的异步任务

12.2.2有返回值的异步任务

12.2.3线程依赖

12.2.4消费处理结果

12.2.5异常处理

12.2.6handle

12.2.7结果合并


一、概述

1.1什么是JUC

在 Java 中,线程部分是一个重点,本篇文章说的 JUC 也是关于线程的。JUC 就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK1.5 开始出现的

1.2进程与线程

进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总结来说:
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

1.2.1线程的状态

1.2.2wait和sleep

  1. sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。
  2. sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。
  3. 它们都可以被 interrupted 方法中断。

1.2.3并发和并行

并发: 同一时刻多个线程在访问同一个资源,多个线程对一个点
           例子:春运抢票 电商秒杀...
并行: 多项工作一起执行,之后再汇总
           例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

1.2.4管程

管程(monitor) 是保证了同一时刻只有一个进程在管程内活动 , 即管程内定义的操作在同一时刻只被一个进程调用( 由编译器实现 ). 但是这样并不能保证进程以设计的顺序执行
JVM 中同步是基于进入和退出管程 (monitor) 对象实现的,每个对象都会有一个管程(monitor)对象,管程 (monitor) 会随着 java 对象一同创建和销毁执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程

1.2.5用户线程和守护线程

用户线程 : 平时用到的普通线程 , 自定义线程
守护线程 : 运行在后台 , 是一种特殊的线程 , 比如垃圾回收
当主线程结束后 , 用户线程还在运行 ,JVM 存活
如果没有用户线程 , 都是守护线程 ,JVM 结束


二、Lock接口

2.1 Synchronized

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
  1. 修饰一个代码块
    被修饰的代码块称为同步语句块,其作用的范围是大括号{}
    括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,
    被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方
    法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上
    synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,
    子类的方法也就相当于同步了。
  3. 修改一个静态的方法
    其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类
    其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。

2.2Lock接口,实现手动加锁和释放锁

synchronized比较

  • Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
  • Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,
    系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

2.2.1卖票的例子

package com.ms.juc.demo1.lock;

import java.util.concurrent.locks.ReentrantLock;

//第一步:创建资源类,创建属性和方法
class Ticket {
    private int number = 30;

    //创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();

    public void sale() {//会自动上锁
        //上锁
        lock.lock();
        //是否有余票
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出票,剩下:" + (--number));
            }
        } finally {//有无异常都要释放锁
            //解锁
            lock.unlock();
        }
    }
}

public class SaleTicket {
    public static void main(String[] args) {
        //第二步:创建爱你多个线程,调用资源类的操作方法
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "thread1").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "thread2").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "thread3").start();
    }
}

2.3接口中的方法

三、线程间的通信

3.1synchronized实现

package com.ms.juc.demo1.线程间通信;

class Share{
    private int number=0;

    public synchronized void incr() throws InterruptedException {
        if (number!=0){
            this.wait();
        }else{
            number++;
            System.out.println(Thread.currentThread().getName()+"+1操作");
            //通知其他线程
            this.notifyAll();
        }
    }
    public synchronized void decr() throws InterruptedException {
        if (number!=1){
            this.wait();
        }else {
            number--;
            System.out.println(Thread.currentThread().getName()+"-1操作");
            //通知其他线程
            this.notifyAll();
        }
    }
}
public class SyncDemo {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Thread1").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Thread2").start();
    }
}

效果:

 存在的问题:

 wait方法在哪里wait,就会在哪里醒,如果是if判断,等到被唤醒的时候就会跳过判断,所以要写到while循环中

3.2Lock实现

package com.ms.juc.demo1._1线程间通信;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Share0 {
    private int number = 0;

    private final Lock lock = new ReentrantLock();

    private final Condition condition = lock.newCondition();

    public void incr() throws InterruptedException {
        //加锁
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "+1操作,当前值为:" + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void decr() throws InterruptedException {
        //加锁
        lock.lock();
        try {
            while (number != 1) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "-1操作,当前值为:" + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

public class LockDemo {
    public static void main(String[] args) {
        Share0 share = new Share0();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread3").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread4").start();
    }
}

结果:

3.3线程间定制化通信

约定线程的执行顺序

案例

需求:

AA打印5次,BB打印10次,CC打印15次

重复10轮

为每个线程添加一个标志位

package com.ms.juc.demo1._1线程间通信.定制化通信;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ShareResource {
    //标志位
    private int flag = 1;//1 AA   2 BB   3CC
    private Lock lock = new ReentrantLock();

    //创建3个condition
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //各自标志位对应的方法
    public void print5(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            while (flag != 1) {
                c1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + "轮数:" + loop);
            }
            //通知下一个线程
            flag = 2;
            c2.signal();//通知BB线程
        } finally {
            lock.unlock();
        }
    }

    public void print10(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            while (flag != 2) {
                c2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + "轮数:" + loop);
            }
            //通知下一个线程
            flag = 3;
            c3.signal();//通知CC线程
        } finally {
            lock.unlock();
        }
    }

    public void print15(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            while (flag != 3) {
                c3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + "轮数:" + loop);
            }
            //通知下一个线程
            flag = 1;
            c1.signal();//通知AA线程
        } finally {
            lock.unlock();
        }
    }
}

public class PersonalDemo {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    shareResource.print5(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    shareResource.print10(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    shareResource.print15(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
    }
}

四、集合的线程安全

4.1集合操作 Demo

arrayList的add方法时线程不安全的

package com.ms.juc.demo1._4集合的线程安全.demo;

import java.util.ArrayList;
import java.util.UUID;

public class ThreadDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        //多线程操作这个list
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },"线程"+i).start();
        }
    }

}

 可能会报异常

4.2解决

4.2.1vector

List<String> list = new Vector<>();

查看一下vector的add方法

4.2.2通过Collections工具类

List<String> list = Collections.synchronizedList(new ArrayList<>());

 4.2.3通过JUC中CopyOnWriteArrayList

List<String> list = new CopyOnWriteArrayList<>();

该类使用写时复制技术
多个线程对集合可以并发读,但是每个线程对集合写的时候,都会拷贝一个新的集合,在新的集合上修改,最终合并

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();//上锁
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);//复制
        newElements[len] = e;//在新的中修改
        setArray(newElements);//覆盖之前
        return true;
    } finally {
        lock.unlock();//解锁
    }
}

4.3HashSet和HashMap线程不安全

4.3.1Set

Set<String> set=new HashSet<>();
for (int i = 0; i < 10; i++) {
    new Thread(()->{
        set.add(UUID.randomUUID().toString().substring(0,8));
        System.out.println(set);
    },"线程"+i).start();
}

可能报异常

解决:

Set<String> set=new CopyOnWriteArraySet<>();

4.3.2Map

Map<String,String> map=new HashMap<>();
for (int i = 0; i < 10; i++) {
    int finalI = i;
    new Thread(()->{
        map.put(String.valueOf(finalI), UUID.randomUUID().toString().substring(0,8));
        System.out.println(map);
    },"线程"+i).start();
}

同样会报上述异常

解决:

Map<String,String> map=new ConcurrentHashMap<>();

五、多线程锁

结论 :
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法,锁的是当前对象 this ,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized 方法
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。
synchronized 实现同步的基础: Java 中的每一个对象都可以作为锁。
具体表现为以下 3 种形式。
  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

5.1公平锁与非公平锁 

5.1.1非公平锁

多个线程抢占同一个资源,可能资源最终都被一个或几个线程获取,其他线程出现饿死的情况

private final ReentrantLock lock = new ReentrantLock();

默认就是非公平锁

5.1.2公平锁

private final ReentrantLock lock = new ReentrantLock(true);


每个线程基本可以等分的获取资源

优缺点:

非公平锁:可能造成线程饿死,但是效率该

公平锁:阳光普照,效率较低

5.2可重入锁

synchronized(隐式的,自动上锁解锁)和Lock都是可重入锁(显式的,手动上锁就,解锁)

可重入锁:锁住的内容之间,无论嵌套多少,对加锁的对象都是开放的,对其他线程都是封闭的,递归锁

5.2.1synchronized演示

Object o = new Object();
new Thread(() -> {
    synchronized (o) {
        System.out.println(Thread.currentThread().getName() + "外层");
        synchronized (o) {
            System.out.println(Thread.currentThread().getName() + "中层");
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + "内层");
            }
        }
    }
}, "t1").start();

public synchronized void add(){
    add();
}

5.2.2Lock演示

ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
    try {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "外层");
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "内层");
        } finally {
            lock.unlock();
        }
    } finally {
        lock.unlock();
    }
}, "t1").start();

每一层上完锁,最后一定要执行锁

5.3死锁

多个线程执行过程中,因为争夺资源造成相互等待的现象,需要外部干涉才能继续执行下去

5.3.1死锁的原因

  1. 系统资源不足
  2. 进程运行推进顺序不合适
  3. 资源分配不当

5.3.2死锁的例子

new Thread(()->{
    synchronized (a){
        System.out.println(Thread.currentThread().getName()+"持有锁a,试图获取锁b");
        synchronized (b){
            System.out.println(Thread.currentThread().getName()+"获取锁b");
        }
    }
},"A").start();
new Thread(()->{
    synchronized (b){
        System.out.println(Thread.currentThread().getName()+"持有锁b,试图获取锁a");
        synchronized (a){
            System.out.println(Thread.currentThread().getName()+"获取锁a");
        }
    }
},"B").start();

互相等待

六、Callable&Future 接口

创建线程的多种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. Callable接口
  4. 线程池的方式

6.1Callable 接口

线程有返回值

class Thread2 implements Callable{
    @Override
    public Object call() throws Exception {
        return 200;
    }
}

6.2Future 接口

当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Future 对象。将 Future 视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable 返回)。Future 基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写 5 种方法,这里列出了重要的方法,如下:
  • public boolean cancel(boolean mayInterrupt):
    用于停止任务。如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true 时才会中断任务。
  • public Object get()抛出 InterruptedException,ExecutionException:
    用于获取任务的结果。
    如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
  • public boolean isDone():
    如果任务完成,则返回 true,否则返回 false
    可以看到 Callable 和 Future 做两件事,Callable 与 Runnable 类似,因为它封
    装了要在另一个线程上运行的任务,而 Future 用于存储从另一个线程获得的结果。实际上,future 也可以与 Runnable 一起使用。要创建线程,需要 Runnable。为了获得结果,需要 future

6.3FutureTask 

Java 库具有具体的 FutureTask 类型,该类型实现 Runnable 和 Future,并方便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建Thread 对象。因此,间接地使用 Callable 创建线程

FutureTask<Integer> futureTask = new FutureTask<Integer>(new Thread2());
FutureTask<Integer> futureTask1=new FutureTask<>(()->{
    return 1024;
});
核心原理:(重点)
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成
  • 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
  • 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
  • 一旦计算完成,就不能再重新开始或取消计算
  • get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
  • get 只计算一次,因此 get 方法放到最后
FutureTask<Integer> futureTask = new FutureTask<>(() -> 200);
new Thread(futureTask, "A").start();
FutureTask<Integer> futureTask1 = new FutureTask<>(() -> 1000);
new Thread(futureTask1, "B").start();
while (!futureTask.isDone()) {
    System.out.println("A:wait...");
}
while (!futureTask1.isDone()) {
    System.out.println("B:wait...");
}
//主线程汇总
System.out.println(futureTask.get());
System.out.println(futureTask1.get());

七、JUC 三大辅助类

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时 Lock 锁的频繁操作。

7.1 减少计数 CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。
  •  CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
  • 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

7.1.1demo

场景: 6 个同学陆续离开教室后值班同学才可以关门
普通实现的问题
@Test
public void test01() {
    for (int i = 1; i <= 6; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "号同学离开教师");
        }, "" + i).start();
    }
    System.out.println(Thread.currentThread().getName()+"班长锁门");
}

人没走完班长就关门了

使用CountDownLatch

@Test
public void test02() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(6);
    for (int i = 1; i <= 6; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "号同学离开教室");
            latch.countDown();
        }, "" + i).start();
    }
    latch.await();
    System.out.println(Thread.currentThread().getName()+"班长锁门");
}

 

7.2循环栅栏 CyclicBarrier 

CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一 次障碍数会加1,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

7.2.1demo 

场景: 集齐 7 颗龙珠就可以召唤神龙
private static final int NUMBER = 7;
public static void main(String[] args) {
    CyclicBarrier barrier = new CyclicBarrier(NUMBER, () -> {
        System.out.println("集齐龙珠");
    });
    for (int i = 1; i <= 7; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "龙珠被收集");
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "" + i).start();
    }
}

7.3 信号灯 Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可

7.3.1demo

场景: 抢车位, 6 部汽车 3 个停车位
public static void main(String[] args) {
    //设置许可数量
    Semaphore semaphore = new Semaphore(3);
    for (int i = 1; i <= 6; i++) {
        new Thread(()->{
            try {
                //抢占车位
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + "----抢到车位");
                //停车时间
                TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                System.out.println(Thread.currentThread().getName() + "----离开车位");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放许可
                semaphore.release();
            }
        },""+i).start();
    }
}

八、读写锁

8.1 读写锁介绍

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景, JAVA 的并发包提供了读写锁 ReentrantReadWriteLock,
它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁
1. 线程进入读锁的前提条件:
  • 没有其他线程的写锁
  • 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)
2. 线程进入写锁的前提条件:
  • 没有其他线程的读锁
  • 没有其他线程的写锁
而读写锁有以下三个重要的特性:
  1. 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  2. 重进入:读锁和写锁都支持线程重进入。
  3. 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

8.2 ReentrantReadWriteLock

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    /**
     * 读锁
     */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /**
     * 写锁
     */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;

    /**
     * 使用默认(非公平)的排序属性创建一个新的
     * ReentrantReadWriteLock
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /**
     * 使用给定的公平策略创建一个新的 ReentrantReadWriteLock
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    /**
     * 返回用于写入操作的锁
     */
    public ReentrantReadWriteLock.WriteLock writeLock() {
        return writerLock;
    }

    /**
     * 返回用于读取操作的锁
     */
    public ReentrantReadWriteLock.ReadLock readLock() {
        return readerLock;
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
    }

    static final class NonfairSync extends Sync {
    }

    static final class FairSync extends Sync {
    }

    public static class ReadLock implements Lock, java.io.Serializable {
    }

    public static class WriteLock implements Lock, java.io.Serializable {
    }
}

8.3 入门案例

场景: 使用 ReentrantReadWriteLock 对一个 hashmap 进行读和写操作(做本地缓存)
package com.ms.juc.demo1._8读写锁;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

//资源类
class Cache {
    private volatile Map<String, Object> map = new HashMap<>();

    //读写锁
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //添加
    public void put(String key, Object value) {
        //加写锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在写操作,key:" + key);
            TimeUnit.SECONDS.sleep(1);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写完了,key:" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            readWriteLock.writeLock().unlock();
        }

    }

    //取数据
    public Object get(String key) {
        //加读锁
        readWriteLock.readLock().lock();
        Object res = null;
        try {
            System.out.println(Thread.currentThread().getName() + "读取key:" + key);
            TimeUnit.SECONDS.sleep(1);
            res = map.get(key);
            System.out.println(Thread.currentThread().getName() + "取完了key:" + key);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            //释放读锁
            readWriteLock.readLock().unlock();
        }
        return res;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        Cache cache = new Cache();
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.put("" + finalI, "" + finalI);
            }, "线程" + i).start();
        }

        for (int i = 6; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.get("" + finalI);
            }, "线程" + i).start();
        }
    }
}

8.4 小结(重要)

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

九、阻塞队列

当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
常用的队列主要有以下两种:
  • 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性
  • 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件(栈)
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

9.1BlockingQueue

9.1.1方法

9.1.2demo

package com.ms.juc.demo1._9阻塞队列;

import org.junit.Test;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class BlockingQueueDemo {
    @Test
    public void test01() {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));//true
        System.out.println(blockingQueue.add("b"));//true
        System.out.println(blockingQueue.add("c"));//true
        System.out.println(blockingQueue.element());//a
//        System.out.println(blockingQueue.remove());//a
        System.out.println(blockingQueue.add("d"));//抛出队列已满异常
    }

    @Test
    public void test02() {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));//true
        System.out.println(blockingQueue.offer("b"));//true
        System.out.println(blockingQueue.offer("c"));//true
        System.out.println(blockingQueue.offer("d"));//false

    }

    @Test
    public void test03() throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
//        blockingQueue.put("d");//被阻塞
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());//被阻塞
    }

    @Test
    public void test04() throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));//true
        System.out.println(blockingQueue.offer("b"));//true
        System.out.println(blockingQueue.offer("c"));//true
        System.out.println(blockingQueue.offer("d", 4, TimeUnit.SECONDS));//false
    }

}

9.2常见的 BlockingQueue

9.2.1 ArrayBlockingQueue(常用)

基于数组的阻塞队列实现,在 ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue 完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea 之所以没这样去做,也许是因为 ArrayBlockingQueue 的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue 和LinkedBlockingQueue 间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node 对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC 的影响还是存在一定的区别。而在创建 ArrayBlockingQueue 时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
一句话总结: 由数组结构组成的有界阻塞队列。

9.2.2 LinkedBlockingQueue(常用)

基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
ArrayBlockingQueue 和 LinkedBlockingQueue 是两个最普通也是最常用 的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个 类足以。
一句话总结: 由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列。

9.2.3 DelayQueue

DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的
操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
一句话总结: 使用优先级队列实现的延迟无界阻塞队列。

9.2.4 PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断通过构造函数传入的 Compator 对象来决定),但需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而 只会在没有可消费的数据时,阻塞数据的消费者 。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费 数据的速度 ,否则时间一长,会最终耗尽所有的可用堆内存空间。
在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是 公平锁
一句话总结: 支持优先级排序的无界阻塞队列。

9.2.5 SynchronousQueue

一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
  • 公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞多余的生产者和消费者,从而体系整体的公平策略
  • 非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者,
  • 而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
一句话总结: 不存储元素的阻塞队列,也即单个元素的队列。

9.2.6 LinkedTransferQueue

LinkedTransferQueue 是一个由链表结构组成的无界阻塞 TransferQueue 队列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和transfer 方法。
LinkedTransferQueue 采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为 null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。
一句话总结: 由链表组成的无界阻塞队列。

9.2.7 LinkedBlockingDeque

LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,这里的阻塞一般有两种情况
  • 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再讲该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException 异常
  • 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
一句话总结: 由链表组成的双向阻塞队列

十、线程池

10.1简介

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
它的主要特点为:
  • 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 ExecutorExecutorsExecutorServiceThreadPoolExecutor 这几个类

10.2线程池的使用方式

10.2.1一池N线程

ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "正在执行");
        });
    }
} finally {
    threadPool.shutdown();
}

 

10.2.2一池一线程

ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "正在执行");
        });
    }
} finally {
    threadPool.shutdown();
}

  

10.2.3可扩容

ExecutorService threadPool = Executors.newCachedThreadPool();
try {
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "正在执行");
        });
    }
} finally {
    threadPool.shutdown();
}

10.3线程池参数

在工具类快速构造线程池的时候,都调用了同一个构造方法

 

 ThreadPoolExecutor有七个参数

  • corePoolSize 线程池的核心线程数,常驻线程数
  • maximumPoolSize 能容纳的最大线程数
  • keepAliveTime 空闲线程存活时间
  • unit 存活的时间单位
  • workQueue 存放提交但未执行任务的队列
  • threadFactory 创建线程的工厂类
  • handler 等待队列满后的拒绝策略

10.3.1 拒绝策略(重点)

线程池中,有三个重要的参数,决定影响了拒绝策略:corePoolSize - 核心线程数,也即最小的线程数。workQueue - 阻塞队列 。 maximumPoolSize - 最大线程数
当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。
总结起来,也就是一句话, 当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略
CallerRunsPolicy :
当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy :
丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy :
直接丢弃,其他啥都没有
DiscardOldestPolicy :
当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

10.4线程池底层工作原理(重要)

  1. 在创建了线程池后,线程池中的线程数为零
  2. 当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
    2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
    4.2所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

10.5自定义线程池

实际中往往自己定义线程池

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
        2,
        5,
        2,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(5),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());
try {
    for (int i = 0; i < 10; i++) {
        int finalI = i;
        threadPool.execute(()->{
            System.out.println(Thread.currentThread().getName()+"执行任务"+ finalI);
        });
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    threadPool.shutdown();
}

十一、Fork/Join分支合并框架

11.1简介

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情:
Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
在 Java 的 Fork/Join 框架中,使用两个类完成上述操作
  • ForkJoinTask:
    我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。
    该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
    a.RecursiveAction:用于没有返回结果的任务
    b.RecursiveTask:用于有返回结果的任务
  • ForkJoinPool:
    ForkJoinTask 需要通过 ForkJoinPool 来执行
  • RecursiveTask:
    继承后可以实现递归(自己调自己)调用的任务
Fork/Join 框架的实现原理
ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成,ForkJoinTask 数组负责将存放以及将程序提交给 ForkJoinPool,而ForkJoinWorkerThread 负责执行这些任务。

11.2Fork 方法的实现原理

当我们调用 ForkJoinTask 的 fork 方法时,程序会把任务放在 ForkJoinWorkerThread 的 pushTask 的 workQueue 中,异步地执行这个任务,然后立即返回结果
pushTask 方法把当前任务存放在 ForkJoinTask 数组队列里。然后再调用ForkJoinPool 的 signalWork()方法唤醒或创建一个工作线程来执行任务。代码如下:

11.3join 方法

Join 方法的主要作用是阻塞当前线程并等待获取结果。代码如下
它首先调用 doJoin 方法,通过 doJoin()方法得到当前任务的状态来判断返回什么结果,任务状态有 4 种:
==已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)和出 现异常(EXCEPTIONAL)==
  • 如果任务状态是已完成,则直接返回任务结果。
  • 如果任务状态是被取消,则直接抛出 CancellationException
  • 如果任务状态是抛出异常,则直接抛出对应的异常
doJoin 方法的实现
在 doJoin()方法流程如下:
  1. 首先通过查看任务的状态,看任务是否已经执行完成,如果执行完成,则直接返回任务状态;
  2. 如果没有执行完,则从任务数组里取出任务并执行。
  3. 如果任务顺利执行完成,则设置任务状态为 NORMAL,如果出现异常,则记录异常,并将任务状态设置为 EXCEPTIONAL。

11.4 Fork/Join 框架的异常处理

ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的 getException 方法获取异常。
getException 方法返回 Throwable 对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回 null。

11.5案例

场景: 生成一个计算任务,计算 1+2+3.........+1000 , ==每 100 个数切分一个 子任务== 合并时差值超过10不能合并
package com.ms.juc.demo1._11分支合并;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/*
 生成一个计算任务,计算 1+2+3.........+1000,==每 100 个数切分一个子任务
 合并时差值超过10不能合并
 */
class Task extends RecursiveTask<Integer> {

    private static final Integer VALUE = 10;

    private int begin;

    private int end;

    private int result;

    public Task(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (end - begin > VALUE) {
            //直接计算
            for (int i = begin; i <= end; i++) {
                result += i;
            }
        } else {
            //继续拆分
            int mid = (begin + end) / 2;
            //左边
            Task task1 = new Task(begin, mid);
            //右边
            Task task2 = new Task(mid + 1, end);
            //分支拆分
            task1.fork();
            task2.fork();
            //合并结果
            result=task1.join()+task2.join();
        }
        return result;
    }
}

public class Demo {
    public static void main(String[] args) throws Exception {
        Task task = new Task(1, 100);
        //创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(task);

        //获取合并之后的结果
        System.out.println(forkJoinTask.get());

        //关闭池对象
        forkJoinPool.shutdown();
    }
}

十二、异步调用

12.1CompletableFuture

12.1.1简介

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类。

12.1.2Future 与 CompletableFuture

Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成。
Future 的主要缺点如下:
  1. 不支持手动完成
    我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成
  2. 不支持进一步的非阻塞调用
    通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
  3. 不支持链式调用
    对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
  4. 不支持多个 Future 合并比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
  5. 不支持异常处理
    Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的。

12.2使用 CompletableFuture

12.2.1无返回值的异步任务

CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
    System.out.println(Thread.currentThread().getName() + "----completableFuture");
});
completableFuture.get();

12.2.2有返回值的异步任务

CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + "----completableFuture");
    return 1024;
});
Integer res = completableFuture.whenComplete((t, u) -> {
    System.out.println("---t:" + t);//方法的返回值
    System.out.println("---u:" + u);//异常信息
}).get();//也可以获取返回值

12.2.3线程依赖

当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
//先加10再平方
private static Integer num = 10;
public static void main(String[] args) throws Exception {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        num += 10;
        return num;
    }).thenApply(integer -> num * num);
    Integer integer = future.get();
    System.out.println(integer);
}

12.2.4消费处理结果

thenAccept 消费处理结果, 接收任务的处理结果,并消费处理,无返回结果。
//消费处理结果
@Test
public void test03()throws Exception{
    System.out.println("主线程开始");
    CompletableFuture.supplyAsync(()->{
        return 10;
    }).thenApply(integer -> integer*integer)
            .thenAccept(new Consumer<Integer>() {
                @Override
                public void accept(Integer integer) {
                    System.out.println("主线程全部处理完成最后调用accept,结果为:"+integer);
                }
            });
}

12.2.5异常处理

exceptionally 异常处理,出现异常时触发
//异常处理
@Test
public void test04()throws Exception{
    System.out.println("主线程开始");
    CompletableFuture<Integer> future=CompletableFuture.supplyAsync(()->{
        int i=10/0;
        return 10;
    }).exceptionally(ex->{
        ex.printStackTrace();
        return -1;
    });
    System.out.println(future.get());//-1
}

12.2.6handle

handle 类似于 thenAccept/thenRun 方法,是最后一步的处理调用,但是同时可以处理异常
@Test
public void test05()throws Exception{
    System.out.println("主线程开始");
    CompletableFuture<Integer> future=CompletableFuture.supplyAsync(()->{
        return 10;
    }).handle((i,ex)->{
        if (ex!=null){
            System.out.println("发生异常");
            return -1;
        }else {
            System.out.println("未发生异常");
            return i;
        }
    });
    System.out.println(future.get());
}

12.2.7结果合并

thenCompose 合并两个有依赖关系的 CompletableFutures 的执行结果
@Test
public void test06()throws Exception{
    System.out.println("主线程开始");
    CompletableFuture<Integer> future=CompletableFuture.supplyAsync(()->{
        System.out.println("任务1开始");
        return 10;
    });
    //合并
    CompletableFuture<Integer> future1=future.thenCompose(i->{
        return CompletableFuture.supplyAsync(()->{
            return i*i;
        });
    });
    System.out.println(future.get());
    System.out.println(future1.get());
}

thenCombine 合并两个没有依赖关系的 CompletableFutures 任务

@Test
public void test07() throws Exception {
    CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> {
        return 10;
    });
    CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> {
        return 20;
    });
    //合并
    CompletableFuture<Object> future = job1.thenCombine(job2, new BiFunction<Integer, Integer, List<Integer>>() {
                @Override
                public List<Integer> apply(Integer integer, Integer integer2) {
                    ArrayList<Integer> list = new ArrayList<>();
                    list.add(integer);
                    list.add(integer2);
                    return list;
                }
            }
    );
    System.out.println("合并结果:"+future.get());
}
合并多个任务的结果 allOf 与 anyOf
allOf: 一系列独立的 future 任务,等其所有的任务执行完后做一些事情
@Test
public void test08() throws Exception {
    System.out.println("主线程开始");
    List<CompletableFuture> list = new ArrayList<>();
    CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("加 10 任务开始");
        num += 10;
        return num;
    });
    list.add(job1);
    CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("乘以 10 任务开始");
        num = num * 10;
        return num;
    });
    list.add(job2);
    CompletableFuture<Integer> job3 = CompletableFuture.supplyAsync(() -> {
        System.out.println("减以 10 任务开始");
        num = num * 10;
        return num;
    });
    list.add(job3);
    CompletableFuture<Integer> job4 = CompletableFuture.supplyAsync(() -> {
        System.out.println("除以 10 任务开始");
        num = num * 10;
        return num;
    });
    list.add(job4);
    //多任务合并
    List<Integer> collect =
            list.stream().map(CompletableFuture<Integer>::join).collect(Collectors.toList());
    System.out.println(collect);
}
anyOf : 只要在多个 future 里面有一个返回,整个任务就可以结束,而不需要等到每一个future 结束
@Test
public void test10() throws Exception {
    System.out.println("主线程开始");
    CompletableFuture<Integer>[] futures = new CompletableFuture[4];
    CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(5000);
            System.out.println("加 10 任务开始");
            num += 10;
            return num;
        } catch (Exception e) {
            return 0;
        }
    });
    futures[0] = job1;
    CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("乘以 10 任务开始");
            num = num * 10;
            return num;
        } catch (Exception e) {
            return 1;
        }
    });
    futures[1] = job2;
    CompletableFuture<Integer> job3 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(3000);
            System.out.println("减以 10 任务开始");
            num = num * 10;
            return num;
        } catch (Exception e) {
            return 2;
        }
    });
    futures[2] = job3;
    CompletableFuture<Integer> job4 = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(4000);
            System.out.println("除以 10 任务开始");
            num = num * 10;
            return num;
        } catch (Exception e) {
            return 3;
        }
    });
    futures[3] = job4;
    CompletableFuture<Object> future = CompletableFuture.anyOf(futures);
    System.out.println(future.get());
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值