且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Java - 基础 - 锁与线程 - 高并发前述

更新时间:2022-08-28 07:40:45

锁与线程

文章目录
本文来自《Java高并发核心编程(2)》的学习笔记
JAVA入门中_说好不能打脸_CSDN博客-系统架构,javaer,系统间通信技术领域博主

一、进程/线程的基本介绍

进程

什么是进程?进程就是程序的一次启动执行

Java - 基础 - 锁与线程 - 高并发前述

线程

什么是线程?线程是进程的代码片段,一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源

Java - 基础 - 锁与线程 - 高并发前述

1 线程的调度与时间片

线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。

(1)分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”

(2)抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。

2 优先级

线程运行的优先级,Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会就越多

方法1:public final int getPriority(),获取线程优先级

方法2:public final void setPriority(int priority),设置线程优先级

3 生命周期

主要是Thread内部的枚举类public enum State

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

新建、可执行、阻塞、等待、限时等待、终止

进程与线程的区别

  • 通俗的讲,进程是大于线程的,一个进程相当于多个线程的组成,一个进程最少有一个线程,那就是它本身
  • 线程是CPU调度的最小单位
  • 线程之间资源共享,进程之间相互独立
  • 上下文切换的速度来说,线程更轻量、更快

二、线程的使用

2.1 Thread类的介绍

在Thread类中,通常有这些操作

  • 线程ID
  • 名称
  • 优先级

    Java线程的最大优先级值为10,最小值为1,默认值为5

  • 守护线程

    什么是守护线程呢?

    守护线程是在进程运行时提供某种后台服务的线程,比如垃圾回收(GC)线程

  • 状态

    新建 | 就绪、运行 | 阻塞 | 等待 | 计时等待 | 结束

  • 启动和运行
  • 获取当前线程

2.2 创建线程的方法

Thread

public class ThreadDemo extends Thread {
    @Override
    public void run() {
        System.out.println("ThreadDemo 多线程测试");
        System.out.println("线程名称 " + currentThread().getName());
        System.out.println("ID " + currentThread().getId());
        System.out.println("状态 " + currentThread().getState());
        System.out.println("优先级 " + currentThread().getPriority());
    }
}

-----------------------------------
    
public class Demo01 {
    public static void main(String[] args) {
        System.out.println("main 主线程运行=======");
        new ThreadDemo().start();
    }
}

Runnable

  • 方法一:匿名类
new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable 运行");
            }
        }).start();
  • 方法二:函数式编程
 new Thread(() -> {
            System.out.println("Runnable 运行");
        }).start();

使用实现Runnable接口这种方式是存在优缺点的

优点

(1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类

(2)逻辑和数据更好分离。通过实现Runnable接口实现多线程能更好地做到多个线程并发地完成同一个任务

缺点

(1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程

(2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程

Callable

前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。

这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。

为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程

实现Callable接口的案例

class CallableDemo implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("有返回值 并且受检异常");
        return null;
    }
}

Future

问题:Callable实例能否和Runnable实例一样,作为Thread线程实例的target来使用呢?答案是不行。Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。既然如此,那么该如何使用Callable接口创建线程呢?

答案就是Future

Thread中的target是什么?

Allocates a new Thread object. This constructor has the same effect as Thread (null, target, gname), where gname is a newly generated name. Automatically generated names are of the form "Thread-"+n, where n is an integer.
Params:
target – the object whose run method is invoked when this thread is started. If null, this classes run method does nothing.

分配一个新的Thread对象。这个构造函数的效果与Thread (null, target, gname)相同,其中gname是一个新生成的名称。自动生成的名称形式为“Thread-”+n,其中n是整数。

参数:
Target - 线程启动时调用其run方法的对象。如果为空,这个类运行方法什么也不做。

public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}

Future是一个泛型接口,用于异步的计算提供了检查计算是否完成、等待计算完成以及检索计算结果的方法

public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);

    // 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
    boolean cancel(boolean mayInterruptIfRunning);
    
    // 任务是否已经取消,任务正常完成前将其取消,则返回 true
    boolean isCancelled();

    // 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
    boolean isDone();

    // 等待任务执行结束,然后获得V类型的结果
    V get() throws InterruptedException, ExecutionException;

    // 参数timeout指定超时时间,uint指定时间的单位
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask类是实现Runnable的Future实现

利用线程池

使用方式一:Executors(官方不推荐),原因后面

// 开辟线程池,放入10个线程数
ExecutorService Service = Executors.newFixedThreadPool(10);

// 适用于 Runnable,将Runnable实现类对象放入
Service.execute(new NumberThread());
Service.execute(new NumberThread2());
        
// 适用于 Callable
// Service.submit();

// 关闭线程池
Service.shutdown();

使用方式二:ThreadPoolExecutor(官方推荐

将在下章节对这两种方式进行说明

总结

Java - 基础 - 锁与线程 - 高并发前述

2.3 为什么Executors被禁止使用

为什么阿里巴巴禁止使用 Executors 创建线程池?_singwhatiwanna-CSDN博客

阿里巴巴开发手册为什么禁止使用 Executors 去创建线程池


原因就是 newFixedThreadPool()newSingleThreadExecutor()两个方法允许请求的最大队列长度是 Integer.MAX_VALUE ,可能会出现任务堆积,出现OOM。

newCachedThreadPool()允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致发生OOM。


它建议使用ThreadPoolExecutor方式去创建线程池,通过上面的分析我们也知道了其实Executors 三种创建线程池的方式最终就是通过ThreadPoolExecutor来创建的,只不过有些参数我们无法控制,如果通过ThreadPoolExecutor的构造器去创建,我们就可以根据实际需求控制线程池需要的任何参数,避免发生OOM异常

2.4 线程的API操作

Java - 基础 - 锁与线程 - 高并发前述

2.5 线程间的通信

线程是程序调度的最小单位,有自己的栈空间,线程之间是共享内存空间的(抛开ThrealLocal),一个进程下的各个线程相互协作通信

线程之间主要还是依靠wait();notify();实现相互之间的工作协助

知识点:

  1. wait() / notify() 方法原理
  2. 通过各类同步对象定义线程状态
  3. 需要在synchronized同步块的内部使用wait和notify
重点在于 wait() / notify() 方法原理 以及它们之间的通信要点

三、线程池的深入不出

3.1 ThreadPoolExecutor

构造参数说明

Java - 基础 - 锁与线程 - 高并发前述

这个类 extendsAbstractExecutorService,下面针对它的有参构造参数进行说明

序号 名称 类型 含义
1 corePoolSize int 核心线程池大小
2 maximumPoolSize int 最大线程池大小
3 keepAliveTime long 线程最大空闲时间
4 unit TimeUnit 时间单位
5 workQueue BlockingQueue 线程等待队列
6 threadFactory ThreadFactory 线程创建工厂
7 handler RejectedExecutionHandler 拒绝策略

提交任务方式

  • 方式一:调用execute()方法

    Execute()方法只能接收Runnable类型的参数

  • 方式二:调用submit()方法

    submit()方法可以接收Callable、Runnable两种类型的参数

这个提交任务的方式就是说,你造了个线程池,然后用他对象的方法丢一个线程进去,这个方法就是我们说的提交任务的方式

阻塞队列

Java中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。

Java线程池使用BlockingQueue实例暂时接收到的异步任务,BlockingQueue是JUC包的一个超级接口,下面有很多比较常用的实现类

钩子方法

ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime,
                timeUnit, workQueue
        ){
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("任务执行前");
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("任务执行后");
            }

            @Override
            protected void terminated() {
                System.out.println("线程池终止时");
            }
        };

拒绝策略

在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝

总体来说,任务被拒绝有两种情况

(1)线程池已经被关闭

(2)工作队列已满且maximumPoolSize已满

无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution方法。RejectedExecutionHandler是拒绝策略的接口

JUC为该接口提供了以下几种实现

  • AbortPolicy:拒绝策略
  • DiscardPolicy:抛弃策略
  • DiscardOldestPolicy:抛弃最老任务策略
  • CallerRunsPolicy:调用者执行策略
  • 自定义策略
继承 RejectedExecutionHandler 接口实现 rejectedExecution 方法即可完成自定义策略

3.2 线程池的任务调度流程

Java - 基础 - 锁与线程 - 高并发前述

3.3 线程池工厂

首先看看接口

Java - 基础 - 锁与线程 - 高并发前述

里面只有一个方法,那就是造一个线程

线程池的工厂方法有四种实现,如下

Java - 基础 - 锁与线程 - 高并发前述

我们还可以按需定制自己的线程池工厂

Java并发编程:Java的四种线程池的使用,以及自定义线程工厂 - 鄙人薛某 - 博客园 (cnblogs.com)

3.4 线程池生命周期/状态

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

(1)RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。

(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。

(3)STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。

(4)TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。

(5)TERMINATED:执行完terminated()钩子方法之后的状态。

如何优雅的关闭线程池?

(1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。

(2)执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。

(3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。

(4)补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。

或者通过注册JVM钩子函数自动关闭线程池

3.5 线程任务分配原则

我们需要根据线程池所执行的任务类型,对线程池设定相应的线程数配置

大致任务类型分为三种

  • IO密集型

    主要是IO时间很长的任务,类似于Netty的IO操作

  • CPU密集型

    处理计算任务,不断地跑CPU

  • 混合型

    字面意思,混了上述两种

四、ThreadLocal

线程局部变量,用于多线程访问同一资源的时,每个线程对这个资源的修改都不会干扰到其他线程,对于线程来说,这个变量是局部的

4.1 首发测试案例

先看执行结果

runnableDemo02 :localVar :null
runnableDemo02 :normVar :runnableDemo01 - set
runnableDemo01 :localVar :runnableDemo01 - set
runnableDemo01 :normVar :runnableDemo01 - set

执行完成后发现,虽然都是静态变量,但是证明的一件事:

ThreadLocal作为线程局部变量,多个线程访问这个资源对他修改,不会影响到其他线程对这个资源的访问,是局部独立的

我用普通static的String变量被多个线程访问时,就成了共享变量了

public class TestOne {

    public static String normVar;
    public static ThreadLocal<String> localVar1 = new ThreadLocal<String>();
    // 如果想为参数设置默认值,让其他线程访问的时候不为null,就可以用这个工厂方法
    public static ThreadLocal<String> localVar2 = ThreadLocal.withInitial(() -> {
        return "withInitial的工厂弄出来的默认值";
    });

    static void print(String str) {
        // 打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :localVar1 :" + localVar1.get());
        System.out.println(str + " :localVar2 :" + localVar2.get());
        System.out.println(str + " :normVar :" + normVar);
        // 清除本地内存中的本地变量
//        localVar.remove();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 池参数
        int corePoolSize = 3;
        int maximumPoolSize = 5;
        long keepAliveTime = 10L;
        TimeUnit timeUnit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);

        // 线程池
        ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime,
                timeUnit, workQueue
        );

        Runnable runnableDemo01 = new Runnable() {
            @Override
            public void run() {
                localVar1.set("runnableDemo01 - set");
                normVar = "runnableDemo01 - set";
                print("runnableDemo01");
            }
        };

        Runnable runnableDemo02 = new Runnable() {
            @Override
            public void run() {
                print("runnableDemo02");
            }
        };

        threadPoolExecutor.submit(runnableDemo01);
        threadPoolExecutor.submit(runnableDemo02);
    }
}

基本方法

Java - 基础 - 锁与线程 - 高并发前述

4.2 使用价值

ThreadLocal的使用场景大致可以分为以下两类

线程隔离

Java - 基础 - 锁与线程 - 高并发前述

跨函数传递数据

Java - 基础 - 锁与线程 - 高并发前述

4.3 Thread的内部结构演进

ThreadLocal的内部结构是一个Map

早期

拥有者:ThreadLocal实例

Key:Thread实例

Value:---

JDK8后

拥有者:Thread实例

Key:ThreadLocal实例

Value:---

优势变化

(1)每个ThreadLocalMap存储的“Key-Value对”数量变少。早期版本的“Key-Value对”数量与线程个数强关联,若线程数量多,则ThreadLocalMap存储的“Key-Value对”数量也多。新版本的ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少

(2)早期版本ThreadLocalMap的拥有者为ThreadLocal,在Thread(线程)实例销毁后,ThreadLocalMap还是存在的;新版本的ThreadLocalMap的拥有者为Thread,现在当Thread实例销毁后,ThreadLocalMap也会随之销毁,在一定程度上能减少内存的消耗

4.4 ThreadLocal源码分析

ThreadLocal源码提供的方法不多,主要有:set(T value)方法、get()方法、remove()方法和initialValue()方法

源码分析也是围绕这四个方法进行说明

4.5 ThreadLocalMap源码分析

ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构(比HashMap简单)

Java - 基础 - 锁与线程 - 高并发前述

4.6 使用原则

1、用完以后,记得remove();

2、尽量使用private static final修饰ThreadLocal实例

五、synchronized

自增运算符是线程安全的吗

不是。因为,一个自增运算符是一个复合操作,至少包括三个JVM指令:

“内存取值”“寄存器增加1”和“存值到内存”

这三个指令在JVM内部是独立进行的,中间完全可能会出现多个线程并发进行。

5.1 基本使用

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外

    public synchronized void method(){
        可能会产生线程安全问题的代码
    }
  • 同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问

    synchronized(同步锁){
         需要同步操作的代码
    }

5.2 锁的状态分类

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

5.3 偏向锁

我觉得简单描述就是,这块代码只有一个线程在用,没人跟它抢,就是偏向锁了

描述

Java - 基础 - 锁与线程 - 高并发前述

如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,此时Mark Word的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完成)。以后该线程获取锁时判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能

膨胀与撤销

概念:假如有多个线程来竞争偏向锁,此对象锁已经有所偏向,其他的线程发现偏向锁并不是偏向自己,就说明存在了竞争,尝试撤销偏向锁(很可能引入安全点),然后膨胀到轻量级锁

5.4 轻量级锁

1、轻量锁就是自旋锁,它是在偏向锁状态下,有线程进行抢资源的时候升级的

2、主要目的是为了解决,重量级锁一旦启用,其它进行抢占的线程会成阻塞状态,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁地阻塞和唤醒对CPU来说是一件负担很重的工作

但是有的时候,这个线程拿了这个锁就用一会,就没必要让其他线程阻塞了,所以有自旋的概念

描述

Java - 基础 - 锁与线程 - 高并发前述

两个线程抢一个锁时,状态发生改变,升级为轻量锁,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录

当锁处于偏向锁,又被另一个线程企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能

自旋原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要进行内核态和用户态之间的切换来进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程和内核切换的消耗

但是,线程自旋是需要消耗CPU的,如果一直获取不到锁,那么线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。JVM对于自旋周期的选择,JDK 1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的。线程如果自旋成功了,下次自旋的次数就会更多,如果自旋失败了,自旋的次数就会减少

如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁

普通/自适应自旋锁

  • 普通

    所谓普通自旋锁,就是指当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁。

    说明

    锁在原地循环等待的时候是会消耗CPU的,就相当于在执行一个什么也不干的空循环。所以轻量级锁适用于临界区代码耗时很短的场景,这样线程在原地等待很短的时间就能够获得锁了。默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin选项来进行更改。

  • 自适应

    所谓自适应自旋锁,就是等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

    (1)如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间

    (2)如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源

    自适应自旋解决的是“锁竞争时间不确定”的问题。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定。总的思想是:根据上一次自旋的时间与结果调整下一次自旋的时间

JDK 1.6的轻量级锁使用的是普通自旋锁,且需要使用-XX:+UseSpinning选项手工开启。

JDK 1.7后,轻量级锁使用自适应自旋锁,JVM启动时自动开启,且自旋时间由JVM自动控制。

轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待。

轻量锁的膨胀

轻量级锁的问题在哪里呢?虽然大部分临界区代码的执行时间都是很短的,但是也会存在执行得很慢的临界区代码。临界区代码执行耗时较长,在其执行期间,其他线程都在原地自旋等待,会空消耗CPU。因此,如果竞争这个同步锁的线程很多,就会有多个线程在原地等待继续空循环消耗CPU(空自旋),这会带来很大的性能损耗。

轻量级锁的本意是为了减少多线程进入操作系统底层的互斥锁(MutexLock)的概率,并不是要替代操作系统互斥锁。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁。

5.5 重量级锁

描述

Java - 基础 - 锁与线程 - 高并发前述

核心原理

有关于JVM对于监视器的内部实现,但是我不想知道就是了

5.6 三种锁状态的对比

执行过程

synchronized的执行过程

  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  6. 如果自旋成功则依然处于轻量级状态。
  7. 如果自旋失败,则升级为重量级锁。

Java并发编程之synchronized原理分析和执行流程_zp的CSDN博客

对比

Java - 基础 - 锁与线程 - 高并发前述

六、JUC(1.CAS)

6.1 什么是CAS?

:Compare And Swap ,即比较并交换,用于实现多线程同步的原子指令

:乐观锁是一种思想,而CAS是这种思想的一种实现

:CAS是一种无锁算法,该算法关键依赖两个值——期望值(旧值)和新值,底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作

:什么是原子性?

​ 线程是并发执行的,谁都有可能先执行。但是CAS是原子操作,对同一个内存地址的CAS操作在同一时刻只能执行一个。因此,在这个例子中,要么线程A先执行,要么线程B先执行。

6.2 Unsafe

这个类官方不建议用,所以咱们也不要用

但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,如直接访问系统内存资源、自主管理内存资源等。Unsafe大量的方法都是native方法,基于C++语言实现,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用

但是,这个类不安全,看名字就知道了

Java - 基础 - 锁与线程 - 高并发前述

在网上看的这个图很形象

文章目录
Java中的Unsafe - 简书 (jianshu.com)

6.3 Atomic

类似于代替 ++ 这种用法,以解决线程不安全问题

Atomic操作翻译成中文是指一个不可中断的操作,即使在多个线程一起执行Atomic类型操作的时候,一个操作一旦开始,就不会被其他线程中断

所谓 Atomic 类,指的是具有原子操作特征的类

原子类大致分为四种

  • 基本

    基本原子类的功能是通过原子方式更新Java基础类型变量的值。

    基本原子类主要包括以下三个:

    ·AtomicInteger:整型原子类。

    ·AtomicLong:长整型原子类。

    ·AtomicBoolean:布尔型原子类。

  • 数组

    数组原子类的功能是通过原子方式更数组中的某个元素的值。

    数组原子类主要包括以下三个:

    ·AtomicIntegerArray:整型数组原子类。

    ·AtomicLongArray:长整型数组原子类。

    ·AtomicReferenceArray:引用类型数组原子类。

  • 引用

    引用原子类主要包括以下三个:

    ·AtomicReference:引用类型原子类。

    ·AtomicMarkableReference:带有更新标记位的原子引用类型。

    ·AtomicStampedReference:带有更新版本号的原子引用类型。

    AtomicMarkableReference类将boolean标记与引用关联起来,可以解决使用AtomicBoolean进行原子更新时可能出现的ABA问题。

    AtomicStampedReference类将整数值与引用关联起来,可以解决使用AtomicInteger进行原子更新时可能出现的ABA问题。

  • 字段更新

    字段更新原子类主要包括以下三个:

    ·AtomicIntegerFieldUpdater:原子更新整型字段的更新器。

    ·AtomicLongFieldUpdater:原子更新长整型字段的更新器。

    ·AtomicReferenceFieldUpdater:原子更新引用类型中的字段。

归根结底,Atomic还是通过CAS自旋+volatile的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升

所有的核心都是 比较And交换

6.4 ABA

如果原子类(Atomic)使用不当的话,就会出现ABA问题

怎么解决呢?用乐观锁,用AtomicReference引用原子类

文章目录
探索CAS无锁技术 - Yrion - 博客园 (cnblogs.com)

6.5 LongAdder

在争用激烈的场景下,会导致大量的CAS空自旋。比如,在大量线程同时并发修改一个AtomicInteger时,可能有很多线程会不停地自旋,甚至有的线程会进入一个无限重复的循环中,大量的CAS空自旋会浪费大量的CPU资源,大大降低了程序的性能

在高并发场景下如何提升CAS操作的性能呢?可以使用Java 8提供的LongAdder类替代AtomicInteger

文章目录
阿里为什么推荐使用LongAdder,而不是volatile? - 知乎 (zhihu.com)

并不是说LongAdder比AtomicInteger更加安全,而是它能减少乐观锁的重试次数,提高性能

Java - 基础 - 锁与线程 - 高并发前述

和AtomicInteger的方法都差不多

6.6 CAS的弊端及规避措施

  1. ABA问题
  2. 只能保证一个共享变量之间的原子性操作
  3. 开销问题

七、JUC(2.锁)

与Java内置锁不同,JUC显式锁是一种非常灵活的、使用纯Java语言实现的锁,这种锁的使用非常灵活,可以进行无条件的、可轮询的、定时的、可中断的锁获取和释放操作。由于JUC锁加锁和解锁的方法都是通过Java API显式进行的,因此也叫显式锁

7.1 显示锁的分类

  • 重入锁不可重入锁

    从同一个线程是否可以重复占有同一个锁对象的角度来分,显式锁可以分为可重入锁与不可重入锁

    可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁

  • 悲观锁乐观锁

    从线程进入临界区前是否锁住同步资源的角度来分,显式锁可以分为悲观锁和乐观锁

  • 公平锁非公平锁

    公平锁是指不同的线程抢占锁的机会是公平的、平等的,反之亦然

  • 可中断锁不可中断锁

    什么是可中断锁?如果某一线程A正占有锁在执行临界区代码,另一线程B正在阻塞式抢占锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待,这种就是可中断锁

  • 共享锁独占锁

    独占锁指的是每次只有一个线程能持有的锁。独占锁是一种悲观保守的加锁策略,它不必要地限制了读/读竞争,如果某个只读线程获取锁,那么其他的读线程都只能等待,这种情况下就限制了读操作的并发性,因为读操作并不会影响数据的一致性

    JUC的ReentrantLock类是一个标准的独占锁实现类

    共享锁,又称为读锁,可以查看但无法修改和删除的一种数据锁

7.2 Lock接口

概述

Java - 基础 - 锁与线程 - 高并发前述

案例

Condition newCondition()

用于实现等待-通知,类似于Obj方法中的 wait() and notify() ,但是Obj方法中是随机唤醒线程,而Condition是选择性的通知

来看看官方介绍

Condition将Object监视器方法( wait 、 notify和notifyAll )分解为不同的对象,通过将它们与任意Lock实现的使用相结合,产生每个对象具有多个等待集的效果。 Lock代替了synchronized方法和语句的使用,而Condition代替了对象监视器方法的使用。
条件(也称为条件队列或条件变量)为一个线程提供了一种挂起执行(“等待”)的方法,直到另一个线程通知某个状态条件现在可能为真。 因为对这个共享状态信息的访问发生在不同的线程中,它必须受到保护,所以某种形式的锁与条件相关联。 等待条件提供的关键属性是它以原子方式释放关联的锁并挂起当前线程,就像Object.wait一样。
 
Condition实例本质上绑定到锁。 要获取特定Lock实例的Condition实例,请使用其newCondition()方法。
 
简单描述如下代码
 1、一个可以 put() 和 take() 的Obj数组
 2、 put() 的时候,会把当前线程锁住,如果数组满了则调用 notFull.await() 让线程等待,被通知后再继续往下走
 3、此时 put() 中的线程是没有释放锁的,还捏在手里
 4、另外一条线程此时调用 take() ,从数组取一个元素出来,数组现在没有满,调用 notFull.signal() 方法,通知刚才的线程
 
class BoundedBuffer {
 // 重入锁
 final Lock lock = new ReentrantLock();
 final Condition notFull = lock.newCondition();
 final Condition notEmpty = lock.newCondition();

 final Object[] items = new Object[100];
 int putptr, takeptr, count;

 public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
         while (count == items.length) {
             // 使当前线程等待,直到它收到信号或被中断
             notFull.await();
         }
         items[putptr] = x;
         if (++putptr == items.length) {
             putptr = 0;
         }
         ++count;
         // 唤醒一个等待线程
         notEmpty.signal();
     } finally {
         // 释放锁
         lock.unlock();
     }
 }

 public Object take() throws InterruptedException {
     lock.lock();
     try {
         while (count == 0) {
             notEmpty.await();
         }
         Object x = items[takeptr];
         if (++takeptr == items.length) {
             takeptr = 0;
         }
         --count;
         notFull.signal();
         return x;
     } finally {
         lock.unlock();
     }
 }
}

7.3 Condition接口

Condition可以看做是Obejct类的wait()notify()notifyAll()方法的替代品,与Lock配合使用

public interface Condition {

    void await() throws InterruptedException;

    long awaitNanos(long nanosTimeout) throws InterruptedException;
    
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}

7.4 编程中使用显示锁的模板代码

其实使用锁都是有套路的,按照这些套路走在编程中肯定不会有问题

使用lock()方法抢锁

Java - 基础 - 锁与线程 - 高并发前述

调用tryLock()方法非阻塞抢锁

Java - 基础 - 锁与线程 - 高并发前述

调用tryLock(long time,TimeUnit unit)方法抢锁

Java - 基础 - 锁与线程 - 高并发前述

7.5 ReentrantLock

ReentranmtLock是一个可重入的独占(或互斥)锁,也叫排他锁,同一时间只允许一个线程访问

什么是可重入锁?

​ 可以对一个资源重复上锁

lock.lock();
lock.lock();
try {
// coding
} finally {
lock.unlock();
lock.unlock();
}

什么是独占锁?

​ 在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁

public class ReentrantLock implements Lock, java.io.Serializable
  • 基于内置的抽象队列同步器(Abstract Queued Synchronized,AQS)实现
  • 但是拥有了限时抢占、可中断抢占等一些高级锁特性

LockSynchronized的区别

  • Lock(接口)是手动锁,手动上锁 手动释放
  • Synchronized(关键字)是自动锁,用完就放 自动去抢
其中还有一个读写锁的概念ReentrantReadWriteLock

锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁

7.6 ReentrantReadWriteLock

文章目录
Java并发编程--ReentrantReadWriteLock - 在周末 - 博客园 (cnblogs.com)

7.7 StampedLock

StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为

在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作

7.8 线程工具类

CountDownLatch

CountDownLatch(倒数闩)是一个非常实用的等待多线程并发的工具类。调用线程可以在倒数闩上进行等待,一直等待倒数闩的次数减少到0,才继续往下执行。每一个被等待的线程执行完成之后进行一次倒数。所有被等待的线程执行完成之后,倒数闩的次数减少到0,调用线程可以往下执行,从而达到并发等待的效果

Java - 基础 - 锁与线程 - 高并发前述

举个例子

public static void main(String[] args) throws InterruptedException {

        // 倒数闩
        CountDownLatch countDownLatch = new CountDownLatch(5);

        // 正式操作
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":开始执行");

                    countDownLatch.countDown();

                    System.out.println(Thread.currentThread() + ":执行完成");
                }
            }).start();
        }
        countDownLatch.await();
        System.out.println("好了,主线程可以跑起来了");
    }

执行结果

Thread-2:开始执行
Thread-4:开始执行
Thread[Thread-2,5,main]:执行完成
Thread-3:开始执行
Thread-1:开始执行
Thread-0:开始执行
Thread[Thread-1,5,main]:执行完成
Thread[Thread-0,5,main]:执行完成
好了,主线程可以跑起来了
Thread[Thread-3,5,main]:执行完成
Thread[Thread-4,5,main]:执行完成

CyclicBarrier

一种同步辅助工具,它允许一组线程全部等待彼此到达公共屏障点

// 举个例子
public static void main(String[] args) {

        // CyclicBarrier最终触发后执行的任务
        Thread overThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是最终触发的任务线程");
            }
        });

        // 当五个线程执行完成,则触发overThread,所以是同步辅助工具,集体到达屏障
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, overThread);

        // 正式操作
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":开始执行");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + ":执行完成");
                }
            }).start();
        }
    }

执行结果

Thread-4:开始执行
Thread-2:开始执行
Thread-5:开始执行
Thread-3:开始执行
Thread-1:开始执行
我是最终触发的任务线程
Thread[Thread-1,5,main]:执行完成
Thread[Thread-4,5,main]:执行完成
Thread[Thread-3,5,main]:执行完成
Thread[Thread-5,5,main]:执行完成
Thread[Thread-2,5,main]:执行完成

Semaphore

在获得一个项目之前,每个线程必须从信号量中获得一个许可,以保证一个项目可供使用。 当线程处理完该项目后,它会返回到池中,并且将许可返回给信号量,从而允许另一个线程获取该项目

举个例子

    // 信号量 创造了三把锁
        Semaphore semaphore = new Semaphore(3);

        // 正式操作
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    try {
                        // 抢锁
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "抢锁");
                    } finally {
                        // 放开锁
                        semaphore.release();
                        System.out.println(Thread.currentThread().getName() + "放开锁");
                    }
                }
            }).start();
        }

我创建了三把锁,如果三个锁都被用了,而且线程不放开,那第四个线程过来就拿不到锁,就得等着

LockSupport

类似于代替Object的线程方法

LockSupport是JUC提供的一个线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法

Java - 基础 - 锁与线程 - 高并发前述

7.9 死锁的检测方法

随便举两个

(1)findDeadlockedThreads:用于检测由于抢占JUC显式锁、Java内置锁引起死锁的线程

(2)findMonitorDeadlockedThreads:仅仅用于检测由于抢占Java内置锁引起死锁的线程

八、AQS 抽象同步器

AQS全称为AbstractQueuedSynchronizer,AQS的诞生是为了解决CAS中所产生的问题(如总线风暴、恶行自旋),一般解决CAS恶行空自旋常见的两种方案:

  1. 分散操作热点
  2. 队列削峰

而JUC并发包使用的解决方案就是队列削峰。

8.1 不同的锁与队列之间的关系

  • CLH锁内部队列

    是一个单向的、FIFO(先进先出)队列,在独占锁中资源在同一个时间内只能被一个线程锁访问,所以队列的头部代表了握着锁的线程节点,新进来的线程要插入尾部排队,进行自旋去访问前一个节点的状态。

    Java - 基础 - 锁与线程 - 高并发前述

  • 分布式锁内部队列

    文章目录
    Zookeeper 分布式锁 - 图解 - 秒懂_架构师尼恩-CSDN博客
  • AQS内部队列

    是JUC提供的一个用于构建锁和同步容器的基础类,JUC包里面很多类都是基于它去进行构建的,用来解决设计容器时的一些问题,同时也是CLH队列的一个变种,但是它是一个双向链表,正因为它双向链表的特性,当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

    Java - 基础 - 锁与线程 - 高并发前述

8.2 AQS的内部组成

Java - 基础 - 锁与线程 - 高并发前述

8.3 使用AQS实现简单的独占锁

文章目录
深入浅出AQS之独占锁模式 - 凌风郎少 - 博客园 (cnblogs.com)

九、JUC(3.容器类)

这里主要包括线程安全的集合结构 BlockingQueue 以及JUC下的 实现该集合内的线程安全性方法

9.1 线程安全的同步容器类

主要通过Collections.synchronizedSortedSet()方法将不安全的集合转换为线程安全的集合,这是一个同步的包装集合的方法,例子如下

// 这是一个非线程安全集合
TreeSet<Integer> treeSet = new TreeSet<>();

// 好了 现在我成线程安全的了
SortedSet<Integer> sortedSet = Collections.synchronizedSortedSet(treeSet);

当然在这个包下还有其他的同步集合的包装方法,如下

Java - 基础 - 锁与线程 - 高并发前述

9.2 高并发容器类介绍

在高并发的场景下,原先的ArraysList等原生容器已经不能满足我们目前的场景需要,因为它们都是非线程安全类,所以引入了能够支撑住高并发的线程安全容器类

以下的线程安全容器类的实现原理都是通过无锁编程CAS+Volatile来实现线程安全,相较于Synchronized,它们更轻量,性能更强

Java - 基础 - 锁与线程 - 高并发前述

9.3 理性分析

从高并发容器类的领域进行大致划分,大概有下面几种类型

  1. CopyOnWrite 写时复制
  2. Concurrent 并发分段锁
  3. BlockingQueue 阻塞队列

好了,以后再说,over

十、CPU缓存

为了缓解内存速度和CPU内核速度差的问题,现代计算机会在CPU上增加高速缓存,每个CPU内核都只有自己的一级、二级高速缓存,CPU芯片板上的CPU内核之间共享一个三级高速缓存

每个CPU的处理过程为:先将计算需要用到的数据缓存在CPU的高速缓存中,在CPU进行计算时,直接从高速缓存中读取数据并且在计算完成之后写回高速缓存中。在整个运算过程完成后,再把高速缓存中的数据同步到主存

Java - 基础 - 锁与线程 - 高并发前述

9.1 原子/可见/有序性问题

简单解释

  • 原子性

    不可中断的一个或一系列操作,不会被线程调度机制打断的操作

  • 可见性

    一个线程对共享变量的修改,另一个线程能够立刻可见,我们称该共享变量具备内存可见性

  • 有序性

    指程序按照代码的先后顺序执行,如果程序执行的顺序与代码的先后顺序不同,并导致了错误的结果,即发生了有序性问题。这里可能会发生指令重排序的问题

9.2 硬件层MSI协议

硬件层的MESI协议是一种用于解决内存的可见性问题的手段,但是对于现阶段来说没什么帮助,所以我先跳过

9.3 Volatile

文章目录
Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)
阿里不建议使用这个修饰方法,建议使用LongAdder代替

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
  2. 禁止进行指令重排序
// 使用方法
public volatile int inc = 0;

使用Volatile关键字能够保证并发编程中的可见性/有序性问题,但是无法保证原子性,所以它无法代替锁

使用Volatile必须具备以下两个条件

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

十一、高并发设计模式

11.1 线程安全的单例模式

主要是有两个代表性的单例模式

  • 饿汉式 - 类加载时就初始化
  • 懒汉式 - 要用的时候才初始化

如果在懒汉式单例模式下,就会存在,两个线程都是进行获取实例操作,可能会拿到两个实例,这就违背了单例模式的原则

  1. 可以采取内置锁的方式保护懒汉式单例
  2. 双重检查锁模式
  3. 双重检查锁+Volatile
  4. 静态内部类

Java - 基础 - 锁与线程 - 高并发前述

11.2 Master-Worker

也是基于模块拆分的理念,Client -> Controller -> Worker ,客户端的请求传达给控制器,控制器分配给下面的打工仔

Java - 基础 - 锁与线程 - 高并发前述

业界也有许多出名的例子,例如Netty的Reactor模式,Nginx的工作进程,这里不详细阐述,关于Netty我有另外一篇笔记

11.3 ForkJoin

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务最终汇总每个小任务结果后得到大任务结果的框架

就像一个大的软件产品,大领导将模块拆分给各个模块的架构师,由架构师指派给下面的程序员进行处理,最终编写完成一个完整的软件产品

JUC包的ForkJoin框架包含如下组件

  • ForkJoinPool:执行任务的线程池
  • ForkJoinWorkerThread:执行任务的工作线程(ForkJoinPool里面的线程)
  • ForkJoinTask:用于ForkJoinPool的任务抽象类,实现了Future接口
  • RecursiveTask:带返回结果的递归执行任务,是ForkJoinTask的子类,在子任务带返回结果时使用
  • RecursiveAction:不返回结果的递归执行任务,是ForkJoinTask的子类,在子任务不带返回结果时使用

下面举个例子进行说明

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

/**
 * 计算相加数测试
 *
 * @author lijiamin
 */
class CountTask extends RecursiveTask<Integer> {

    /**
     * 设置分割阈值
     */
    private static final int THREAD_HOLD = 2;

    /**
     * 起始值和终止值
     */
    private int start, end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    /**
     * 这段代码看不懂
     * @return
     */
    @Override
    protected Integer compute() {

        int sum = 0;
        // 如果任务足够小就计算
        boolean canCompute = (end - start) <= THREAD_HOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            int middle = (start + end) / 2;
            CountTask left = new CountTask(start, middle);
            CountTask right = new CountTask(middle + 1, end);
            // 执行子任务
            left.fork();
            right.fork();
            // 获取子任务结果
            int lResult = left.join();
            int rResult = right.join();
            sum = lResult + rResult;
        }
        return sum;
    }
}


/**
 * @author lijiamin
 */
public class Test04AndForkJoin {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 任务池
        ForkJoinPool pool = new ForkJoinPool();

        // 具体要执行的任务
        CountTask countTask = new CountTask(1, 100);

        // 提交任务
        Future<Integer> result = pool.submit(countTask);

        // 任务结果
        System.out.println(result.get());

    }
}
框架的核心API、工作原理等,以后我用到了才回来记录

11.4 生产/消费模式

不作阐述,很多中间件都是这种模式

11.5 Future模式

异步调用,不会立刻返回我们要的结果,但是会有一个异步的任务,执行完一定会给我们结果,但是要去get一下

CompletableFuture

异步编排技术(组合式异步编程)

Java - 基础 - 锁与线程 - 高并发前述

先谈谈这里面的方法命名

独立线程启动

  • Supply 有return
  • Run 没有return

依赖线程启动

Then

  • Apply 有return
  • Accept 没有return

是否异步

  • Async 异步
  • Sync 默认同步

其他常用的

  1. WhenComplate 当线程任务完成后执行
  2. Exceptionally 当线程任务出现异常后执行
  3. Allof(线程1,线程2,线程4) 全部线程执行完后,主线程结束阻塞,继续执行
  4. Anyof(线程1,线程2,线程4) 任意线程执行完后,主线程结束阻塞,继续执行

提前总结

这玩意就是类似于给了我们一个异步线程池的玩法,既可以异步执行线程操作拿到返回结果,也可以异步级联编排的方式进行编程

漏了一个点,它可以统一处理异常

T 是上一个任务返回结果的类型,U 是当前任务的返回值类型

/**
* handle 是执行任务完成时对结果的处理
* handle 是在任务完成后再执行,还可以处理异常的任务
*/
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);

当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法

Java - 基础 - 锁与线程 - 高并发前述

还有另外一种玩法,结合函数式接口+线程池进行操作

@Resource
    ThreadPoolExecutor threadPoolExecutor;
    
    @Test
    public void d1() {
        CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("first exec to = CompletableFuture.supplyAsync(() =");
            return "one";
        }, threadPoolExecutor);
    }

其中的参数传递

Java - 基础 - 锁与线程 - 高并发前述

光说不练假把式,接下来看真正的案例代码

@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    ThreadPoolExecutor threadPoolExecutor;

    @Override
    public Map<String, Object> item(Long skuId) {
        long start = System.currentTimeMillis();

        Map<String, Object> map = new HashMap<>();

        // 商品sku
        CompletableFuture<SkuInfo> completableFutureSku = CompletableFuture.supplyAsync(new Supplier<SkuInfo>() {
            @Override
            public SkuInfo get() {
                SkuInfo skuInfo = productFeignClient.getSkuById(skuId);// db
                return skuInfo;
            }
        },threadPoolExecutor);

        // 商品分类(一级二级三级)
        CompletableFuture<Void> completableFutureView = completableFutureSku.thenAcceptAsync(new Consumer<SkuInfo>() {
            @Override
            public void accept(SkuInfo skuInfo) {
                BaseCategoryView baseCategoryView = productFeignClient.getCategoryViewByCategory3Id(skuInfo.getCategory3Id());
                map.put("categoryView",baseCategoryView);
            }
        },threadPoolExecutor);

        // 商品图片
        CompletableFuture<Void> completableFutureImage = completableFutureSku.thenAcceptAsync(new Consumer<SkuInfo>() {
            @Override
            public void accept(SkuInfo skuInfo) {
                List<SkuImage> skuImages = productFeignClient.getImageBySkuId(skuId);
                skuInfo.setSkuImageList(skuImages);
                map.put("skuInfo", skuInfo);
            }
        },threadPoolExecutor);

        // 商品销售属性
        CompletableFuture<Void> completableFutureSaleAttr = completableFutureSku.thenAcceptAsync(new Consumer<SkuInfo>() {
            @Override
            public void accept(SkuInfo skuInfo) {
                List<SpuSaleAttr> spuSaleAttrList = productFeignClient.getSpuSaleAttrList(skuInfo.getSpuId(), skuId);
                map.put("spuSaleAttrList",spuSaleAttrList);
            }
        },threadPoolExecutor);

        // 商品价格
        CompletableFuture<Void> completableFuturePrice = CompletableFuture.runAsync(new Runnable() {
            @Override
            public void run() {
                BigDecimal price = productFeignClient.getSkuPriceBySkuId(skuId);
                map.put("price", price);
            }
        },threadPoolExecutor);


        // 查询销售属性值对应sku的hash
        CompletableFuture<Void> completableFutureValuesJson = completableFutureSku.thenAcceptAsync(new Consumer<SkuInfo>() {
            @Override
            public void accept(SkuInfo skuInfo) {
                List<Map<String, Object>> valuesMap = productFeignClient.getSaleAttrValuesBySpu(skuInfo.getSpuId());

                // 将mybatis的数据结构转化成页面需要的jsonMap数据结构
                Map<String, String> jsonMap = new HashMap<>();
                for (Map<String, Object> stringObjectMap : valuesMap) {
                    Integer sku_id = (Integer) stringObjectMap.get("sku_id");
                    String saleAttrValues = (String) stringObjectMap.get("saleAttrValues");
                    jsonMap.put(saleAttrValues, sku_id + "");
                }
                String valuesSkuJson = JSON.toJSONString(jsonMap);
                map.put("valuesSkuJson", valuesSkuJson);
            }
        },threadPoolExecutor);
       CompletableFuture.allOf(completableFutureSku,completableFutureView,completableFutureImage,completableFutureSaleAttr,completableFuturePrice,completableFutureValuesJson).join();

        long end = System.currentTimeMillis();

        System.out.println("执行时间:"+(end-start));

        return map;
    }
}
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码

AsyncTool

文章目录
京东零售多线程并行框架 - AsyncTool_lijiamin-的CSDN博客

结束