jvm内存结构,多线程,线程池与Java中的锁

1.jvm内存结构

线程私有的:程序计数器,虚拟机栈,本地方法栈
线程共享的:堆,方法区
JVM内存主要分为:程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区。
在这里插入图片描述

1.1程序计数器

java代码最终都要编译成一条条的字节码,然后由字节码解释器一条条的执行的,可以看作是当前线程私有的所执行的字节码的行号计数器
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

1.2Java虚拟机栈------->栈

  • 线程私有的,生命周期和线程相同,描述的是 Java 方法执行的内存模型。

  • 连续的存储空间,遵循后进先出的原则,存放基本类型的变量数据和对象的引用(指向堆中的实例),但对象本身不存放在栈中,而是存放在堆或者常量池中;

  • 每当一个线程去执行方法时,就会同时在栈里面创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈等。每一个方法从被调用到执行完成的过程,都对应着一个栈帧从入栈到出栈的过程。
    在这里插入图片描述

  • 当在一段代码块定义一个变量时,Java在栈中为这个变量分配内存空间,当该变量退出其作用域Ⅰ后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
    注:Ⅰ:变量的作用域:从变量定义的位置开始,到该变量所在的那对大括号结束
    Ⅱ:变量周期性: 从变量定义的位置开始在内存中存在;到达它所在的作用域外的时从内存中消失;

  • StackOverFlowError:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时,就抛出StackOverFlowError。

  • OutOfMemoryError:若栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

局部变量表:(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
操作数栈:(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

经典案例1:int i= 88;分析一下i=i++;与i=++i的区别?
i = i++的字节码指令顺序:

 0 bipush 88		//先将88先压到栈中
 2 istore_1			//将88放到下标值为1的局部变量表,再把88弹出来赋值给i
 3 iload_1			//从局部变量表取出下标为1的值,压入栈中为88
 4 iinc 1 by 1		//执行i++ 操作,将局部变量表中数值为88的进行+1变为89
 7 istore_1			//将栈中的数据赋值到局部变量表中,所以这个时候局部变量表中的数据就是88了
 8 getstatic #2 <java/lang/System.out>
11 iload_1			//从局部变量表取出下标为1的值,放入栈中还是88
12 invokevirtual #3 <java/io/PrintStream.println>
15 return			//所以我们最后的结果就是88

++i的字节码指令顺序:

 0 bipush 88		//先将88先压到栈中
 2 istore_1			//将88放到下标值为1的局部变量表,再把88弹出来赋值给i
 3 iinc 1 by 1		//执行i++ 操作,将局部变量表中数值为88的进行+1变为89
 6 iload_1			//从局部变量表取出下标为1的值,压入栈中为89
 7 istore_1			//将栈中的数据赋值到局部变量表中,所以这个时候局部变量表中的数据就是88了
 8 getstatic #2 <java/lang/System.out>
11 iload_1			//从局部变量表取出下标为1的值,放入栈中还是89
12 invokevirtual #3 <java/io/PrintStream.println>
15 return			//所以我们最后的结果就是89

总结:

  • i++先将局部变量数压到操作栈中再对局部变量进行自增,++i先进行局部变量的自增然后再压入操作栈中
  • 赋值=,放在最后计算,若操作栈中有值则将操作栈中的值赋给局部变量
  • 自增、自减操作都是直接修改局部变量的值,不经过操作数栈
  • 最后的赋值之前,临时结果也是存储在操作数栈中
  • =右边的从左到右加载值依次压入操作数栈
  • 实际先算哪个,看运算符优先级
    详细说明 1.2 查看字节码

1.3本地方法栈

和虚拟机栈作用相似,区别是: 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)提供服务,而本地方法栈则为虚拟机使用到的Native方法(不是由java编写的代码,由C或C编写的一些方法)提供服务。

java虚拟机在调用一些本地方法的时候,需要给其提供的本地栈内存空间
本地方法:
定义: 不是由java编写的代码,由C或C编写的一些方法
作用: Java代码有时不能直接与我们的操作系统底层打交道,所以就需要由c++或C编写的代码进行操作,Java方法可以通过调用本地方法来实现与操作系统的操作 例如:Object类中的 wait() notify() hashcode()等方法.

1.4堆空间(heap)

  • 虚拟机启动时被创建,不连续的空间,用于存放类的实例,或者说new出的对象;
  • 数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器释放)。这也是 Java 比较占内存的原因。
    在这里插入图片描述

如图所示,JVM内存主要由新生代、老年代、永久代构成。

1.4.1新生代(Young Generation):

新生代内分三个区:一个易电原区,两个幸存者区,对象在易电原区中生成。当易电原区满时,还存活的对象将被复制到两个幸存者区中的一个。当幸存者区满时, 此区存活且不满足“晋升”条件的对象将被复制到另外一个幸存者区。会对新生代进行Minor GC,且对象年龄加1,达到“晋升年龄阈值”(通过参数MaxTenuringThreshold设定,默认值为15。)后,被放到老年代。

1.4.2老年代(Old Generation):

当老年代空间不足,进行Full GC 回收整个堆中无引用指向的对象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space

1.5方法区(Perm Generation)

线程共享的内存,jdk8以前采用永久代的实现方式,用于存放类的代码信息、静态变量和方法、常量池(字符串常量和基本类型常量,具有共享机制)常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。 除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名、字段的名称和描述符、方法和名称和描述符。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。
JDK1.8废弃了永久代,替代的是元空间,与永久代类似,都是方法区的实现,区别是:常量池和静态变量存储到堆中,元空间并不在JVM中,而是使用本地内存。

1.6堆与栈对比

堆的优缺点:

  • 优点:可以动态地分配内存大小,生命存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会在没有变量指向这些数据时自动收走数据。
  • 缺点:由于要在运行时动态分配内存,存取速度较慢。

栈的优缺点:

  • 优点:存取速度比堆要快,仅次于寄存器,栈数据可以共享(int a = 3再int b = 3此时内存中值存在一个3,a,b两个引用同时指向同一个3)。
  • 缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的。

注:

  • 用new来生成的对象都是放在堆中的,直接定义的局部变量都是放在栈中的,全局和静态的对象是放在数据段的静态存储区,
    例如:Class People;People p;//栈上分配内存
    People* pPeople;pPeople = new People;//堆上分配内存
  • 对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的) 才确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

1.7GC的四种算法

1.7.1引用计数法

只要对象被引用就不进行回收
缺点:每次对对象赋值时均需要维护引用计数器,且计数器本身也有一定的消耗;且较难处理循环引用
jvm实现一般不采用这种非算法

1.7.2复制算法

年轻代的youngGC采用的就是复制算法

原理:从根集合开始通过Tracking从from中找到存活对象,拷贝到To中,From To交换身份下次GC从To开始
在这里插入图片描述

1.7.3标记清除

发生在老年代
在这里插入图片描述

1.7.4标记压缩

发生在老年代
在这里插入图片描述
发生在老年代的有两种GC时结合使用的
标记清除压缩法:
在这里插入图片描述

2.JMM–java内存模型(为了适应和解决多线程通信而产生的一种模型)

2.1内存模型简介

在这里插入图片描述

  • Java内存模型将内存分为了主内存和工作内存
  • Java内存模型规定所有的变量都存储在主内存中,每个线程有自己的工作内存
  • 主内存主要包括:堆和方法区,主内存是所有线程共享的
  • 工作内存主要包括:该线程私有的栈和对主内存部分变量拷贝的寄存器(包括程序计数器和cpu高速缓存区)
  • Java内存模型规定了所有变量都存储在主内存中,每个线程有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。

2.2Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性来建立的

原子性:指多个操作不存在只执行一部分的情况,要么全部执行要么全部失败
可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2…线程n能够立即读取到线程1修改后的值。
有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

2.3JMM关于同步的规定

线程解锁前,必须把共享变量的值刷新到主内存
线程加锁前必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁

2.3什么情况下线程栈中的数据会刷新主存中的变量?

  • 当变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立刻将其刷新到主内存中。
  • 通过synchronized关键字能够保证可见性,synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存当中。

3.Java 线程

3.1.进程和线程的区别是什么?

线程是操作系统能够进行运算调度的最小单位也是进程中的实际运作单位。一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而当前进程下的所有线程共享一片相同的内存空间。 每个线程都拥有单独的栈内存用来存储本地数据 .

3.2线程的几种可用状态。

线程在执行过程中,可以处于下面几种状态:
就绪(Runnable):调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
运行中(Running):于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体
等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。
睡眠中(Sleeping):线程被强制睡眠。
I/O 阻塞(Blocked on I/O):等待 I/O 操作完成。
同步阻塞(Blocked on Synchronization):运行的线程在获取对象的同步锁时,该锁被别的线程占用。
死亡(Dead):线程完成了执行,或者线程抛出一个未捕获的 Exception 或 Error。

3.3.创建线程的几种不同的方式

3.3.1继承Thread类:不使用这种方式,若想使用多线程的类也继承的别的类,就无法继承Thread类;
public class ThreadTest extends Thread {

    private int ticket = 10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (this) {
                if (this.ticket > 0) {
                    try {
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName() + "卖票---->" + (this.ticket--));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] arg) {
        ThreadTest t1 = new ThreadTest();
        t1 .start();
    }
}
3.3.2实现 Runnable 接口
public class RunnableTest implements Runnable {
    private int ticket = 10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //添加同步快
            synchronized (this) {
                if (this.ticket > 0) {
                    try {
                        //通过睡眠线程来模拟出最后一张票的抢票场景
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName() + "卖票---->" + (this.ticket--));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] arg) {
        RunnableTest t1 = new RunnableTest();
        new Thread(t1, "线程1").start();
        new Thread(t1, "线程2").start();
    }
}
3.3.3 实现Callable接口:是Runnable接口的增强版,使用call()方法作为线程的执行体,增强了之前的run()方法,因为call()方法有返回值,也可以声明抛出异常;

MyTask.java类
在这里插入图片描述
FutureTask使用方法当线程结束才返回结果(也可用于闭锁的操作):
在这里插入图片描述

Callable接口与Runnable接口对比:
1.Callable规定的方法是call(),而Runnable规定的方法是run().
2.Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
3.call() 方法可抛出异常,而run() 方法是不能抛出异常的。
运行Callable任务可拿到一个FutureTask对象, FutureTask表示异步计算的结果

4.Java JUC 简介

在 Java 5.0 提供了 java.util.concurrent (简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

5.volatile 关键字

简介:Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

出现的原因:Java内存模型规定了所有变量都存储在主内存中,每个线程有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存中的变量,不同线程之间也无法直接操作对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。线程A对共享的变量修改后,线程B读取变量时依然是未修改的变量
在这里插入图片描述
作用
1.可见性:volatile用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效。
2.有序性:即程序执行时按照代码书写的先后顺序执行。

指令重排: 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile会禁止指令重排
在这里插入图片描述
这段代码的书写顺序时1234但在重排序后可能的一个执行顺序是:语句2 -> 语句1 -> 语句3 -> 语句4
那么可不可能是这个执行顺序: 语句2 -> 语句1 -> 语句4 -> 语句3。
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。重排序不会影响单个线程内程序执行的结果,但在多线程处理器不能保证

volatile具有可见性、有序性,不具备原子性。
注意,volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异,这一点在面试中也是非常容易问到的点。

6.原子变量与CAS算法(乐观锁)

6.1volatile存在的问题

volatile虽然解决了可见性的问题但不能保证操作的原子性,例两个线程同时访问使用volatile修饰的共享变量i = 0,此时对i进行++操作,若线程A读取了主存中的变量后并在本地方法栈将要进行+1的操作,此时线程B开启且读取到的依然是0,但按照程序的预期效果应该是1;

6.2CAS算法

CAS算法简介:比较并修改(Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

CAS包含三个操作数:变量内存位置(V),预期的变量原值(A),变量的新值(B),对变量进行修改时,先会将内存位置的值与预期的变量原值进行比较,若一致则将内存位置更新为新值,否则不做操作,但最后都会返回内存位置当前的值。

ABA问题:如果V的值首先由A变成了B,再由B变成了A,虽然V中的值A好像没有变,但是在某些算法中,A的属性却是变了。对于保护的变量是数值类型是不需要关心ABA问题,但是如果是对象,就需要注意。
解决办法:jdk1.5后提供了原子工具类来解决volatile存在的问题不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变成了B,然后又变成了A,版本号也将是不同的。AtomicStampedReference和AtomicMarkableReference支持在两个变量上执行原子的条件更新。

6.3原子工具类

原子工具类:JDK并发包中提供的原子类很丰富,可以分为五个类别:标量类(基本数据类)、对象引用类、数组类、对象属性更新器类和累加器类。

6.3.1原子标量类(原子基本数据类)

相关实现类有AtomicBoolean、AtomicInteger和AtomicLong,提供的方法主如下:
在这里插入图片描述

6.3.2原子对象引用类

相关实现有AtomicReference、AtomicStampedReference和AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。
AtomicReference提供的方法和原子化的基本数据类型差不多。不过,需要注意,对象引用的更新需要重点关注ABA问题
AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上“版本号”,避免了ABA问题。
在这里插入图片描述
AtomicMarkableReference将更新一个“对象引用-布尔值”二元组,避免了ABA问题。
在这里插入图片描述

6.3.3原子数组类

相关实现有AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,利用这些原子类,我们可以原子化地更新数组中的每一个元素。
这些类提供的方法和原子化基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数。

6.3.4原子对象属性更新器类

相关实现有AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的,创建更新器的方法如下:
在这里插入图片描述

需要注意,对象属性必须是volatile类型的,只有这样才能保证可见性;如果对象属性不是volatile类型的,newUpdater()方法会抛出IllegalArgumentException这个运行时异常。
newUpdater()方法的参数中只有类的信息没有对象的引用,而更新对象的属性,需要对象的引用,那么这个参数是再哪里传入的呢?
是在原子类操作的方法参数中传入的。例如,compareAndSet()这个原子操作,相比原子化的基本数据类型参数多了一个对象引用obj。
在这里插入图片描述

6.3.5原子累加器类

DoubleAccumulator、DoubleAdder、LongAccumulator和LongAdder,这四个类仅仅用来执行累加计数操作,相比原子化的基本数据类型,速度更快,但是不支持compareAndSet()方法。
在实际情况中,如果仅需一个计数器或者序列生成器,那么可以直接使用AtomicInteger或者AtomicLong,它们能提供原子的递增方法以及其他算术方法。

6.4同步容器类

包括Vector和HashTable。后来在JDK1.2也引入一个功能与之类似的类,这些同步的封装容器类是由Collections.synchronizedXXX等工厂方法创建的。这些类实现线程安全的方式是:将他们的状态封装起来,对每个公有方法都进行同步(方法加synchronized),使得每次只有线程能访问容器的状态。
存在问题
 同步容器类虽然是线程安全的,但是某些情况下需要加同步来保护复合操作(也就是一个方法中对容器进行多个操作),有可能在两次操作的中间时间容器被修改造成异常
解决方法:对每个操作元素的方法进行同步(加synchronized)

6.5并发容器类

Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。

6.5.1ConcurrentHashMap并发容器类

ConcurrentHashMap:底层采用分段的数组+链表实现,线程安全

  • 1.7中(分段锁思想)
  1. 通过把整个Map分为N个Segment,保证了线程安全并且效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  2. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  3. 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  4. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
  • 1.8中数组+链表(hash冲突时形成链表,尾插法)+红黑树(链表节点数>=8时转成红黑树) 结构。
  1. CAS+Synchronized来保证并发更新的安全,
6.5.2CopyOnWriteArrayList :写入时复制(copy-on-write)

向一个容器添加元素时,不直接往当前容器添加,而是先将当前容器Copy出一个新的容器,往新的容器里添加元素,再将原容器的引用指向新容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
缺点就是每次修改容器都会复制底层数组,这需要一定的开销,特别是当容器规模较大的时候。仅当迭代操作远远多于修改操作,才应该使用"写入时复制"容器。

此包还提供了设计用于多线程上下文中的 Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

7.悲观锁

7.1synchronized关键字(隐式锁)

synchronized关键字:
Java 语言中,每个对象有一把锁。线程可以使用synchronized关键字获取对象上的锁。
同步代码块:
synchronized(锁对象){ 需要同步的代码 }
此处的所对象必须是存在堆中(多个线程的共享资源)的对象
同步方法:
权限关键字 synchronized 返回值 方法名(){ 需要被同步的代码块 }
同步方法的锁对象是this
静态方法及锁对象问题: 锁对象是类的字节码文件对象(类的class文件)

锁的释放时机:
① 当前线程的同步方法、代码块执行结束的时候释放
② 当前线程在同步方法、同步代码块中遇到break、return 终于该代码块或者方法的时候释放。
③ 当前线程出现未处理的error或者exception导致异常结束的时候释放。
④ 程序执行了同步对象wait方法,当前线程暂停,释放锁。

7.2lock和ReadWriteLock(显示锁):

两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。

7.2.1Lock相较synchronized的优点:

①synchronized实现同步线程(IO读文件时)阻塞不释放锁时,其他线程需一直等待,Lock可以通过只等待一定的时间 (tryLock(long time, TimeUnit unit)) 或者能够响应中断(lockInterruptibly())解决。

②多个线程读写文件时,读1操作与读2操作不会起冲突synchronized实现的同步的话也只有一个线程在执行读1操作.读2操作需等待,Lock可以解决这种情况 (ReentrantReadWriteLock)。

③可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

7.2.2lock中的方法: Lock lock = new ReentranLock;

lock():用来获取锁。如果锁已被其他线程获取,则进行等待;必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,

tryLock():尝试获取锁,获取成功返回true;获取失败(锁已被其他线程获取),返回false,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)

tryLock(long time, TimeUnit unit):拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。拿到锁,则返回true。

lockInterruptibly():当通过这个方法去获取锁时,如果其他线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

interrupt():方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。

unlock():释放锁在finally语句块中执行。

7.2.3ReadWriteLock中的方法:ReadWriteLock rl= new ReentrantReadWriteLock();**

维护了一对相关的锁,一个用 于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的。
rl.readLock();//返回Lock接口可通过Lock接口内方法获取锁
rl.writeLock();//返回Lock接口可通过Lock接口内方法获取锁

7.3等待唤醒机制

synchronized中:this.XXX调用下面方法
sleep(long millis):使当前线程停止执行,把cpu让给其他线程执行,不释放锁,睡眠期满,恢复为可运行状态时继续运行。
wait();调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
notify();唤醒等待的当前(同一个对象锁)线程中的任意一个,
notifyAll();唤醒所有等待当前(同一个对象锁)的线程.
join();进程里有主线程和子线程,子线程需要大量的耗时运算,主线程可能会在子线程结束之前结束,但是主线程结束前需要子线的结程果,那么就有硬性要求子线程必须赶在主线程结束之前结束,join()方法,让线程间从并行到串行。
yield();调用yield()意味着告诉虚拟机,当前线程让出cpu,但还会进行cpu资源争夺,能不能再次分配到,就不一定了,不释放锁。

Lock中:Condition condition =new ReentrantReadWriteLock().newCondition(),使用condition.XX(await,signal,signalAll方法)进行等待唤醒机制的操作

7.4synchronized与Lock的区别

  1. 原始构成
    synchronized时关键字属于JVM层面,底层依赖于Monitor类的Monitorenter方法,其实wait和notify等方法也是依赖于Monitor;
    Lock时一个接口,属于API层面;
  2. 使用方法
    synchronized不需要用户手动去释放锁,代码块执行完毕后自动释放;
    Lock下的实现类需要用户手动去释放锁,如果不释放就会出现死锁问题;
  3. 等待是否可中断
    synchronized不可中断除非抛异常或者执行完成;
    Lock可中断,1.通过设置超时时间使用tryLock的方式获取锁;2.lockInterruptibly()放入代码块中,调用interrupt()方法可以中断;
  4. 是否为公平锁
    synchronized是非公平锁
    Lock下的实现类可以通过构造参数指定是否为公平锁true为公平锁,false为非公平锁,默认为false
  5. 是否可以精确唤醒
    synchronized只能对正在等待的线程进行随机唤醒或者全部唤醒
    Lock可以使用Condition来实现分组唤醒需要被唤醒的线程

7.5锁的相关概念介绍

7.5.1公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
在高并发情况下有可能,会造成优先级反转(其它线程加塞)或者饥饿(一直被加塞自己一直待等待)现象。

对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
两者的区别:

  • 公平锁:在并发环境下,每个线程在获取锁的时候会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则会加入到等待队列中,以后会按照FIFO的规则从队列中获取到自己。
  • 非公平锁:上来就直接尝试占有锁,如果尝试失败就是采用公平锁的方式进入队列
7.5.2可重入锁(也被称为递归锁)

指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,ReentrantLock和Synchronized都是可重入锁。
在这里插入图片描述
对于ReentrantLock而言, 他的名字就可以看出是一个可重入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

7.5.3自旋锁

指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待(JDK中,自旋操作默认10次),然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。好处是减少线程上下文切换的性能损耗,缺点循环会消耗CPU;
在这里插入图片描述

7.5.2独享锁(写锁)/共享锁(读锁)

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

7.5.3闭锁(CountDownLatch)

CountDownLatch 一个同步辅助类。闭锁可以等待其他线程执行完毕再执行其他操作,如果没有闭锁则需要发送一个通知或者估计一个执行时间来保证其他线程的操作执行完成,这样效率会很低。(不过闭锁的原理也相当于发送一个通知,也就是计数器的值为0,代表操作已经完成)。

  • 闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:
  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
  • 等待直到某个操作所有参与者都准备就绪再继续执行。
    CountDownLatch
7.5.4CyclicBarrier

让一组线程到达一个屏障时被阻塞,直达最后一个线程到达屏障时才会开门,所有被阻塞的线程才会继续干活线程进入屏障通过await方法;
在这里插入图片描述

7.5.5Semaphore

多个线程抢多个资源:主要用于多个共享资源的互斥使用,还可以用于并发线程数的控制
场景:5个停车位 ,10辆汽车去抢车位
在这里插入图片描述

7.5.4悲观锁/乐观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断更新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

重入锁与公平/非公平锁详解

7.6锁的实现机理

7.6.1 synchronized的重入实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁线程的指针。
当一个线程执行monitorenter时,若目标锁对象的计数器为0,则该锁没有被其他对象所持有,JVM会将该锁对象的持有线程设置成当前线程,并将其计数器加1。
若目标锁计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么JVM将其计数器加1,就需要等待,直至持有锁线程释放锁。
当一个线程执行monitorexit时,JVM则需要将锁对象的计数器减1。计数器为0代表锁已释放。

7.6.2LockSupport

三种等待唤醒机制的方法

  • Object中的wait() / notify() / notifyAll()
    不能脱离synchronized代码块使用,否则会抛出IllegalMonitorStateException异常
    先wait后notify、notifyAll,等待中的线程才能被唤醒,顺序不能改变
  • Condition的await() / siginal() / siginalAll()
    必须配合lock()方法使用,否则抛出IllegalMonitorStateException异常
    等待唤醒调用顺序不能改变
  • LockSupport提供park()和unpark()方法实现阻塞和唤醒线程的过程
    LockSupport和每一个使用它的线程之间有一个许可(permit)进行关联,permit有0和1两种状态,默认为0,即无许可证状态
    调用一次unpark方法,permit加1变成1。每次调用park方法都会检查许可证状态,如果为1,则消耗掉permit(1 -> 0)并立刻返回;如果为0,则进入阻塞状态。permit最多只有一个,重复调用unpark也不会累积permit。

简单使用
LockSupport中的park()方法不仅可以不用在synchronized块中或者获取lock锁之后执行,而且unpark()解除阻塞可以在阻塞操作之前执行,这是wait()/notify()和await()/signal()所做不到的
在这里插入图片描述

  • 结论
    在这里插入图片描述
  • 两个问题的解答
    在这里插入图片描述

7.7AQS:AbstractQuenedSynchronizer抽象的队列式同步器(线程等待时的排队机制)

用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源线程的排队工作,并通过一个int类型变量表示持有锁的状态
在这里插入图片描述
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的虚拟双向队列(FIFO)来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS实现堆State值的修改;

7.5.7死锁编码的定位分析

死锁产生的原因:指两个或以上的进程在执行过程中,因抢夺资源而造成互相等待的一种情况。
定位分析:jps命令定位进程号;jstack找到死锁查看;

8.线程池

8.1线程池介绍

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

8.2线程池的工作机制

在线程池的工作模式下,任务是整个提交给线程池的,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

8.3使用线程池的原因

  • 降低资源的消耗:通过重复利用已经创建好的线程降低线程创建和销毁带来的损耗
  • 提高响应速度:线程池中的线程没有超过上限时,有线程处于等待分配任务的状态,当任务来时无需创建线程这一步骤就能直接执行。
  • 提高线程的可管理性:线程池里提供了操作线程的方法,这就为对管理线程提供了可能性。

8.4四种常见的线程池详解

线程池的返回值ExecutorService: 是Java提供的用于管理线程池的类。
该类的两个作用:控制线程数量和重用线程;将所有的多线程异步任务都交给线程池

8.4.1 原生线程池(实际应用中使用的)

创建
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor();

8.4.1.1原生线程池的七大参数

在这里插入图片描述

  • corePoolSize : 线程池中的常驻核心线程数,线程池创建好之后就等待来接受异步任务去执行。
  • maximumPoolSize : 线程能够同时容纳的最大线程数(包含核心线程数),这个值必须大于1
  • keepAliveTime : 多余的空闲线程的存活时间,如果当前线程数量大于corePoolSize,且线程空闲的时间大于keepAliveTime就会被销毁,直到剩下核心线程数的个数位置
  • unit : 指定存活时间keepAliveTime的时间单位
  • BlockingQueue workQueue : 阻塞队列的最大数量,如果任务数大于maximumPoolSize,就会将任务放在队列里,只要有线程空闲就会去队列里去取新的任务执行。
  • ThreadFactory threadFactory : 表示生成线程池中工作线程的线程工厂,用于创建线程一般使用默认的即可。
  • RejectedExecutionHandler handler : 拒绝策略,如果workQueue满了并且工作线程大于线程池的corePoolSize,就会按照指定的拒绝策略拒绝执行任务
8.4.1.2RejectedExecutionHandler(拒绝策略)

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
RejectedExecutionHandler的四个实现类:

  1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
  2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
  3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
  4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
    若以上策略仍无法满足实际需要,可以自己扩展RejectedExecutionHandler 接口
8.4.1.3线程池的原理

1. 线程池创建,准备好corePoolSize数量的核心线程,准备接受任务。
2. 当调用execute()方法添加一个请求任务时,线程池会做以下判断:
(1)如果正在运行的线程数小于corePoolSize,那么马上直接获取线程运行这个任务;
(2)如果正在运行的线程数大于或等于corePoolSize,那么将这个进来的任务放入阻塞队列中;
(3)如果队列满了,并且正在运行的线程数小于maximumPoolSize,那么立刻创建非核心线程来执行这个任务;
(4)如果队列满了,并且正在运行的线程数大于等于maximumPoolSize,那么线程池会启动饱和拒绝策略来处理这些任务;
3. 当一个线程完成任务后,它就会从队列中取下一个任务来执行;
4. 当一个线程空闲时间超过keepAliveTime时间,且当前运行的线程数量大于corePoolSize,这个线程会自动销毁。最终保持到core大小;
5. 所有的线程都是由指定的factory创建。

8.4.1.4核心线程数的设定标准

如果是cpu密集型的,尽量减少线程数;线程CPU时间所占比例越高,需要越少线程
一般公式:cpu核数+1

如果是IO密集型因为io密集型并不是一直执行任务,不占用cpu的资源,则尽量加大线程数;

  1. 公式一:cpu核数*2
  2. 公式二:cpu核数/(1-阻塞系数)
8.4.1.5线程池在不同场景下的使用

高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
  a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

面试题
在这里插入图片描述

8.4.2常用的线程池创建方式(实际应用中基本不用)
8.4.2.1 Executors.newCacheThreadPool():

可缓存线程池,core的数量为0,所有都可以回收, 先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务(运行结果见下汇总图).
在这里插入图片描述
在这里插入图片描述
线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程

8.4.2.2 Executors.newFixedThreadPool(int n):

创建一个可重用固定个数的线程池,core的数量为max,都不可以回收,以共享的无界队列方式来运行这些线程。(运行结果见下汇总图).
在这里插入图片描述
在这里插入图片描述

8.4.2.3 Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行(运行结果见下汇总图).

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.4.2.4 Executors.newSingleThreadExecutor():

创建一个单线程化的线程池,从阻塞队列里挨个获取任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO , LIFO,优先级)执行(运行结果见下汇总图).
在这里插入图片描述
在这里插入图片描述
以上的所有execute都可以使用submit代替,并且submit可以有返回值

8.5线程池的阻塞队列

阻塞队列在数据结构中起到的做用大致如下:
在这里插入图片描述
当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,直到其他的线程往空的队列中插入新的元素;
当阻塞队列为满时,往队列中添加元素的操作将会被阻塞,直到其他的线程从队列中移除元素或或清空队列后才能获取元素

在多程领域下:所谓阻塞在某些线程下会挂起线程即阻塞,一旦条件满足被挂起的线程又会被唤醒。

8.5.1BlockingQueue

BlockingQueue也是Collection接口下的子接口;
在concurrent包发布之前,多线程环境下这些阻塞唤起过程都要程序猿去自己控制细节,而且还要兼顾效率和线程安全,使用BlockingQueue能帮我们简单的做到这些事情;

BlockingQueue的几个实现类:
在这里插入图片描述

8.5.2核心方法

在这里插入图片描述
在这里插入图片描述
SynchronousQueue与其他BlockingQueue不同,它是一个不存储元素的BlockingQueue每一个put操作后必须等待一个take操作,否则不能 继续添加元素反之亦然;

9.CompletableFuture异步编排

在这里插入图片描述

9.1创建异步对象

CompletableFuture提供了四个静态方法来创建一个异步操作。
Supplier supplier : 参数为一个方法
Executor executor可以传入自定义线程池,否则使用自己默认的线程池

创建一个线程异步编排对象,前个没有返回值,后两个有返回值;

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

给出一个例子

public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService future= Executors.newFixedThreadPool(10);
        CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程------------------" + Thread.currentThread().getName());
            int i = 10 / 2;
            return i;
        }, future);
        //获取异步执行的结果在线程执任务行完之后返回
        Integer integer = integerCompletableFuture.get();
        System.out.println("结果为"+integer);//结果为5
    }

9.2whenComplete(计算完成时回调方法)

CompletableFuture提供了四个方法计算完成时回调方法

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)

下面给出一个例子

//方法执行完成后的感知
public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService future= Executors.newFixedThreadPool(10);
        CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程------------------" + Thread.currentThread().getName());
            int i = 10 / 0;//使用int i = 10 / 0;模拟有异常
            return i;
        }, future).whenComplete((ems,exception)->{
            //在出现异常时虽然可以感知异常但不能修改数据
            System.out.println("计算结果为:"+ems+"----异常为"+exception);
        }).exceptionally((throwable)->{
            //可以感知异常,并可以返回结果
            return 10;
        });
        Integer integer = integerCompletableFuture.get();
        System.out.println(integer);

    }

whenComplete可以感知正常和异常的计算结果,无异常时直接返回结果,在感知到异常时使用exceptionally处理异常情况
whenComplete和whenCompleteAsync的区别:
whenComplete : 是执行当前任务的线程执行继续执行whenComplete的任务
whenCompleteAsync :是执行把whenCompleteAsync这个任务继续提交给线程池来执行
方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池也可能会被同一个线程选中执行)

9.3handle方法(处理异常返回结果)

    public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
    public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
    public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)

下面给出一个例子

//方法完成后的处理
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService future= Executors.newFixedThreadPool(10);
        CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程------------------" + Thread.currentThread().getName());
            int i = 10 / 2;//使用int i = 10 / 0;模拟有异常
            return i;
        }, future).handle((result,throwable)->{
            if(result!=null){
                return result*2;
            }
            if(throwable!=null){
                return result*0;
            }
            return 0;
        });
        Integer integer = integerCompletableFuture.get();
        System.out.println(integer);
    }

和whenComplete一样,可以对结果做最后的处理(可处理异常),可改变返回值。

9.4线程串行化方法(完成上步执行其他任务)

public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor)
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)
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)

thenRun:处理完任务后执行thenRun后面的方法
thenAccept:消费处理结果。接受任务的执行结果并消费处理,无返回结果
thenApply:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值
以上所有都要前置任务完成

给出一个例子

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService future= Executors.newFixedThreadPool(10);
        CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程------------------" + Thread.currentThread().getName());
            int i = 10 / 2;
            return i;
        }, future).thenApplyAsync(res -> {
            return res+3;
        }, future);
        Integer integer = integerCompletableFuture.get();//此处结果为8
        System.out.println(integer);
    }

thenRun不能获取到上一步执行结果
thenAccept:能接受上一步执行结果但没返回值
thenApply:既能接受上一步执行结果也有返回值

9.5将两任务组合-(都完成时,才执行作为参数传入的方法)

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

两个任务必须都完成,触发该任务:
runAfterBoth:组合两个future,不需要获取future的结果,只需要两个future处理完,处理该任务。
thenAcceptBoth:组合两个future,获取两个future的返回结果,然后处理任务,没有返回值。
thenCombine:组合两个future,获取两个future的返回结果,然后处理任务,并返回当前任务的返回值。

给出一个例子
在这里插入图片描述

9.6将两任务组合-(一个完成,才执行作为参数传入的方法)

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

当两个future中有一个完成,触发该任务:
runAfterEither:两个future有一个执行完成,不需要获取future的结果,直接处理当前任务,也没有返回值。
acceptEither:两个future有一个执行完成,获取它的返回值,并处理当前任务,没有新的返回值。
applyToEither:两个future有一个执行完成,获取它的返回值,并处理当前任务,有新的返回值。

给出一个例子
在这里插入图片描述

9.7多任务组合

    public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) 
    public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) 

allOf : 阻塞线程,等待所有任务完成,才继续往下进行否则阻塞
anyOf : 阻塞线程,只要有一个任务完成,就继续往下进行

给出一个例子
在这里插入图片描述

10.ForkJoinPool 分支/合并框架 工作窃取

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。
在这里插入图片描述
Fork/Join 框架与线程池的区别

  • 采用 “工作窃取”模式(work-stealing):
    当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加 到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
  • 相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务
    的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。
    这里只做了解,jdk1.8做了优化
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值