Java面试准备(三)——Java并发

Java并发

一、理论基础

1. 并发和并行区别

  • 并发concurrent:两个及两个以上的作业在同一 时间段 内执行。
  • 并行parallel:两个及两个以上的作业在同一 时刻 执行。 【记:行才是真的行】

2. 同步和异步

  • 同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
  • 异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
  • 例:
    • 比方说一个人边吃饭,边看手机,边说话,就是异步处理的方式。
    • 同步处理就不一样了,说话后在吃饭,吃完饭后在看手机,必须等待上一件事完了,才执行后面的事情。

(回答时候可以结合着项目中用到的业务场景来举例,不要干巴巴的只回复定义)

3. 进程, 线程及二者区别

  1. 何为进程

    • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

    • 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

  2. 何为线程

    • 线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。

    • 同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

      Java 运行时数据区域(JDK1.8 之后)
  3. 进程和线程的区别

    • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

    • 资源开销

      1. 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;
      2. 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
    • 内存分配同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

    • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响;但是一个线程崩溃整个进程都死掉,而且同一进程中的线程极有可能会相互影响。所以多进程要比多线程健壮。

    • ⚠️小结:首先二者的单位大小不同(根本区别),其次由于二者单位不同进而导致的二者共享的资源啥的不同,然后共享资源的不同导致二者的开销管理和保护不同。

4. 理解线程私有公有的东西

  1. 程序计数器为什么是私有的

    考虑到程序计数器的作用

    1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

    所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

  2. 虚拟机栈和本地方法栈为什么是私有的?

    • 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

    • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

      所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的

  3. 堆和方法区

    堆和方法区是所有线程共享的资源,其中

    • 是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),
    • 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

5. 为什么要使用多线程

  1. 一些业务场景的需要,提高接口的响应时间。比如查询用户信息的接口,如果同步调用三个接口来拿数据就很耗时,这就很有必要把三个接口调用改成异步调用最后汇总结果。
  2. 再从计算机底层来说,和进程相比, 线程间的切换和调度的成本远远小于进程。多CPU系统中,使用线程提高CPU利用率
  3. 线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用。当然,数据的共享也带来其他一些问题

6. 用多线程可能带来的问题

多线程可能带来的问题_Xixw的博客-CSDN博客_多线程或多进程导致的问题

  1. 内存泄漏:对于应用程序来说,当对象已经不再被使用,但是Java的垃圾回收器不能回收它们的
    时候,就产生了内存泄露。未引用对象将会被垃圾回收器回收,而引用对象却不会。未引用
    对象很显然是无用的对象。然而,无用的对象并不都是未引用对象,有一些无用对象也有可能是引用对
    象,这部分对象正是内存泄露的来源
  2. 死锁: 多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。
  3. 线程不安全: 指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

7. 导致多线程出问题的原因

并发三要素【 在多线程场景中要注意问题,一般主要从这三个方面考虑 】

【 并发编程,为了保证数据的安全,需要满足以下三个特性】

  1. 可见性: 就是说某个线程对于一个共享变量做了修改,其他线程能够立刻发现。

    boolean flag = true;
    while (flag) {
        // do something...
    }
    /**
    * 对于这段代码,线程 A 在执行while循环。此时,线程 B 将flag的值修改为false,线程 A 能够马上退出循环操作,就说明变量flag具有可见性。
    */
    
    • 共享变量未满足可见性可能会出现问题
  2. 原子性: 就是不可以再被分割。对于一个具有原子性操作来说,在执行这个操作的过程中不会插入其他操作

    int i = 5; //原子性操作
    i++; //非原子性操作
    /**
    * i++ 相当于 i = i + 1;
    * 其中包含了三个操作:
    *  1). 读取 i 的值
    *  2). 对 i 加 1
    *  3). 重新赋值给 i
    */
    
    • 非原子性操作可能会出现的问题:比如 i 的值为1,如果在 i++ 的第二个操作的时候,有其他的线程插入进来对变量 i 做了其他的操作,那么 i++ 之后,i 的值就不会是 2 了。
  3. 有序性: 通俗来讲就是,代码并不一定会按照顺序来执行,java虚拟机在执行的过程中可能会改变顺序来提高性能,但是不会改变程序整体的运行结果,称为“重排序”。这种重排序操作在多线程环境下就可能出问题

    //有两个线程 A B
    boolean isDone = false;
    //线程A
    new Thread(() -> {
      save(object); //假设做保存操作
      isDone = true;
    }).start();
    //线程B
    new Thread(() -> {
      while (true) {
        if (isDone) {
          Object obj = getObject(); 
          //使用obj对象做后续的工作 
        }
      }
    }).start();
    
    • 线程A做保存操作,然后设置标记变量isDone为true;线程B根据标记变量判断线程A是否保存完毕,当isDone == true,B就认为线程A已经保存完毕,然后取出线程A保存的对象,做后续的工作。

      如果此时,线程A中发生了重排序的情况,线程A里面的两行代码交换了执行的顺序。而此时,设置了isDone的值,还没有执行save操作的时候,线程B开始执行了,就会发生问题,线程B的get操作不会取到任何对象。

8. Java如何解决并发问题

在并发编程中分析线程安全的问题时往往需要切入点,那就是两大核心:JMM抽象内存模型以及happens-before规则(在这篇文章中已经经过了),三条性质:原子性,有序性和可见性

1)关键字

1. volatile
  1. volatile作用

    1. 防止 JVM 的指令重排序。 有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时禁止指定的普通读/写和volatile读/写重排序)。

      • 例:比如说实例化一个对象要分三步,1分配内存空间2初始化对象3将内存空间的地址赋值给对应的引用,但是进行指令重排序之后这三个过程顺序就可能有所改变,若初始化对象放在最后执行,在多线程环境下就可能将一个未初始化的对象引用暴露出来,导致不可预料的结果。

        java-thread-x-key-volatile-3 java-thread-x-key-volatile-4
    2. 保证此变量对所有的线程的可见性

      • 可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。

      • 所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

        当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

    3. ⚠️ volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。

      • i++操作 包括三个步骤:读取操作写回, volatile是无法保证这三个操作是具有原子性的。我们可以通过AtomicInteger,Lock或者Synchronized来保证+1操作的原子性。
      • 共享的long和double变量为什么要用volatile:因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
      • 理解: volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i

    Java中Volatile关键字详解 - 郑斌blog - 博客园 (cnblogs.com)

    关键字: volatile详解 | Java 全栈知识体系 (pdai.tech)

2. synchronized
  1. synchronized的使用

    synchronized 翻译成中文是同步的的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

    synchronized 关键字最主要的三种使用方式:(分类锁和对象锁)

    1. 修饰实例方法
    2. 修饰静态方法
    3. 修饰代码块

    1、修饰实例方法 (锁当前对象实例)

    给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

    synchronized void method() {
        //业务代码
    }
    

    2、修饰静态方法 (锁当前类)

    给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

    这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

    synchronized static void method() {
        //业务代码
    }
    
    • 静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

    3、修饰代码块 (锁指定对象/类)

    对括号里指定的对象/类加锁:

    • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
    • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
    synchronized(this) {
        //业务代码
    }
    

    总结:

    • synchronized 加到 static 静态方法和 synchronized(class) 代码块上都是给 Class 类上锁;

    • synchronized 关键字加到实例方法上是给对象实例上锁;

    • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

    • **构造方法不能使用 synchronized 关键字修饰。**构造方法本身就属于线程安全的

  2. synchronized 和 volatile 的区别?

    synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

    • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定更好synchronized关键字 。但是 volatile 关键字只能用于变量synchronized 关键字可以修饰方法以及代码块 。
    • volatile 关键字能保证数据的可见性,但不能保证数据的原子性synchronized 关键字两者都能保证。
    • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
3. final

2)Java内存模型 JMM

讲的无敌好JMM https://www.bilibili.com/video/BV1F64y1B7sV/?spm_id_from=333.788.recommend_more_video.10&vd_source=8dde1febc4e745d509f003015e03723c

1. 什么是JMM
  1. 首先,从计算机内存模型说起,为了解决CPU和主存之间信息不对等的问题,引入了各级高速缓存(L1,L2,L3 Cache)。但是这样就会存在内存缓存不一致的问题。

    CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议来解决。 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。

    程序运行在操作系统之上,操作系统屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化。于是,操作系统也就同样需要解决内存缓存不一致性问题。操作系统通过 内存模型(Memory Model) 定义一系列规范来解决内存缓存不一致性问题。无论是 Windows 系统,还是 Linux 系统,它们都有特定的内存模型。

    缓存一致性协议
  2. Java 是最早尝试提供内存模型的编程语言。 一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型来屏蔽系统差异

JMM 看作是 Java 定义的并发编程相关的一组规范,主要是

  • 抽象了线程和主内存之间的关系

    1668498643554
  • 还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,定义了很多东西:

    • 所有的变量都存储在主内存(Main Memory)中。
    • 每个线程都有一个本地内存(Local Memory),本地内存中存储了该线程的共享变量的拷贝副本
    • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
    • 不同的线程之间无法直接访问对方本地内存中的变量。
  • 在 Java 中提供了一系列和并发处理相关的关键字,比如 Volatile、Synchronized、Final、Concurren 包等。其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字
    在开发多线程的代码的时候,我们可以直接使用 Synchronized 等关键字来控制并发,这样就不需要关心底层的编译器优化、缓存一致性等问题。
    所以,Java 内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。

 其**主要目的是为了简化多线程编程,增强程序可移植性**的。 
2. JMM 是如何抽象线程和主内存之间的关系?

关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作

  • 作用于主存的4个指令:lock锁定,unlock解锁,read读取,write写入
  • 作用于工作内存的4个指令:load载入,use使用,assign赋值,store存储
3. Java 内存区域和 JMM 有何区别?

Java 内存区域和内存模型是完全不一样的两个东西

  • JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
  • Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

3)Happens-Before规则

happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。

happens-before 的规则就 8 条,重点了解下面列举的 5 条即可。

  1. 程序顺序规则 :一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作;
  2. 解锁规则 :解锁 happens-before 于加锁;
  3. volatile 变量规则 :对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。说白了就是对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。
  4. 传递规则 :如果 A happens-before B,且 B happens-before C,那么 A happens-before C;
  5. 线程启动规则 :Thread 对象的 start()方法 happens-before 于此线程的每一个动作。

如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序

二、线程基础

1. Java线程的几种状态转换

在java.lang.Thread类中的State枚举中定义了Java线程的几种状态

  • NEW: 初始状态,线程被创建出来,但没有被调用 start() 方法。

  • RUNNABLE: 运行状态, Java线程中将操作系统中的就绪(READY)和运行中(RUNNING)两种状态统一为 RUNABLE 。是 可能正在运行,也可能正在等待 CPU 时间片。

  • BLOCKED :阻塞状态,需要等待锁释放。

  • WAITING:等待状态,表示该线程需要 等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

  • TIME_WAITING:限时等待状态,可以在指定的时间后自行返回,而不是像 WAITING 那样一直等待。

  • TERMINATED:终止状态,表示该线程已经运行完毕。

    线程状态变化

2. 涉及Java线程状态转换的函数

1)sleep() 方法和 wait() 方法对比

共同点 :两者都可以暂停线程的执行。

区别

  1. 所属类不同。sleep() 是Thread类的静态本地方法,wait() 是Object类的本地方法

    //java.lang.Thread#sleep
    public static native void sleep(long millis) throws InterruptedException;
    
    //java.lang.Object#wait
    public final native void wait(long timeout) throws InterruptedException;
    
  2. 使用语法不同

    • sleep() 方法可以让当前线程休眠,时间一到当前线程继续往下执行,在任何地方都能使用,但需要抛出或者捕获 InterruptedException 异常

    • wait() 方法则必须放在 synchronized 块里面,需要获取对象的锁,通过对象的锁去调用wait。 同样需要抛出/捕获 InterruptedException 异常。wait()notify()方法必须写在同步方法中,是为了防止死锁和永久等待,使线程更安全 )

      //sleep()用法
      try {
          Thread.sleep(3000L);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      
      //wait()用法
      synchronized (lock){
          try {
              lock.wait();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      
  3. 唤醒方式不同

    • sleep()方法必须要指定时间参数,时间到了线程自动唤醒。

    • wait()方法可传入时间参数也可以不传。若不传入时间参数,需要别的线程调用同一个对象上的notify()或者notifyAll()方法来唤醒;wait若传入时间那就时间到了唤醒。

      public static final Object lock1 = new Object();
      
      public static void main(String[] args) throws InterruptedException {
      
          new Thread(() -> {
              synchronized (lock1) {
                  //另一个线程调用同一个锁对象lock1上的notify()方法;
                  lock1.notify();
              }
          }).start();
      
          synchronized (lock1) {
              lock1.wait();
          }
      }
      
  4. 是否释放锁资源wait() 可以释放当前线程对 lock 对象锁的持有,而 sleep() 方法执行期间没有释放锁。

  5. 适用场景不同: sleep 一般用于当前线程休眠,或者暂停执行,wait 则多用于多线程之间的通信交互

2)为什么 wait() 方法定义在 Object 中?

类似的问题:为什么 sleep() 方法定义在 Thread 中?

  • 因为 sleep 是让当前线程休眠,不涉及到对象类,也不需要获得对象的锁,所以是线程类的方法。
  • wait 是让获得对象锁的线程实现等待前提是要先获得对象的锁,所以是类的方法。

3)可以直接调用 Thread 类的 run 方法吗?

  • new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
  • 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法不会以多线程的方式执行。

3. 创建线程的方式

  1. 实现Runnable接口

    • Runnable接口中只有一个run()方法
    • 怎么写:需要实现run()方法,然后通过Thread来调用start()方法来启动线程。(调用start()方法后线程就进入就绪状态,当分配到时间片后会开始运行自动执行run()的内容)
    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            // ...
            System.out.println("run()执行");
        }
    
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
        }
    }
    
  2. 实现Callable接口

    • 与 Runnable 相比,Callable 可以有返回值返回值通过 FutureTask 封装
    • Callable接口中只有一个call()方法,调用start()之后会自动执行实现的call()方法
    public class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() {
            return 123;
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            MyCallable myCallable = new MyCallable();
            FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
            Thread thread = new Thread(futureTask);
            thread.start();
            System.out.println(futureTask.get());
        }
    }
    
  3. 继承Thread类

    • 同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
    public class MyThread extends Thread {
        @Override
        public void run() {
            // ...
        }
    
        public static void main(String[] args) {
            MyThread mt = new MyThread();
            mt.start();
        }
    }
    

    实现接口 VS 继承 Thread

    实现接口会更好一些,因为:

    • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;

    • 类可能只要求可执行就行,继承整个 Thread 类开销过大

  4. 线程池方式

    • java提供了构建线程池的工具,java.util.concurrent包中的Executors可以创建线程池。

      但是 阿里编码规范中不允许使用这种方式构建线程池,这种方式对线程的控制粒度比较低。eg:线程名字不能改、线程数固定等

    • java手动创建线程池 ThreadPoolExecutor

       new ThreadPoolExecutor(……)
      
    • Spring提供的构建线程池的工具 ThreadPoolTaskExecutor

三、线程池

1. 创建线程池ThreadPoolExecutor的7个参数

public ThreadPoolExecutor(int corePoolSize,	//核心线程数
                          int maximumPoolSize,	//最大线程数
                          long keepAliveTime,	//最大空闲时间
                          TimeUnit unit,	//时间单位
                          BlockingQueue<Runnable> workQueue,	//阻塞队列
                          ThreadFactory threadFactory,	//线程工厂
                          RejectedExecutionHandler handler) {	//拒绝策略
    ……
}
  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式,可指定线程池名字等。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

2. 线程池的执行流程

  1. 任务提交到线程池,若核心线程数有空闲,则执行;若核心线程数没有空闲,则任务要放到阻塞队列中

  2. 若阻塞队列中没满,则放到队列中排队;若阻塞队列满了,则看工作线程数是否为最大线程数

  3. 若没有达到最大线程数,则创建非核心线程数;若已经达到最大线程数,则采用拒接策略

    线程池执行流程图
    img
  • 为什么要先进阻塞再去尝试创建非核心线程:

    1. 在创建新线程的时候,是要获取全局锁的,这时候其他线程会被阻塞,影响整体效率。
    2. 在核心线程已满时,如果任务继续增加那么放在队列中,等队列满了而任务还在增加那么就要创建临时线程了,这样代价低。
  • 创建线程池时具体每个参数设多少值怎么定

    1. 跑任务看运行时间理解各个参数

    2. 创建线程池具体参数设置要看的变量有:每秒的任务数、每个任务花费的时间等等

3. 线程池的使用示例

1)用java原生的线程池类 ThreadPoolExecutor

/**
 * ThreadPoolExecutor使用示例
 * 1.构造一个线程池
 * 2.往线程池中提交任务
 * 3.关闭线程池
 *
 * @author zhangna
 */
public class ThreadPoolExecutorDemo {
    private static int produceTaskSleepTime = 5;
    private static int consumeTaskSleepTime = 5000;
    private static int produceTaskMaxNumber = 20;

    public static void main(String[] args) {
        //1.构造一个线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3),
                new ThreadPoolExecutor.DiscardPolicy());
        try {
            for (int i = 1; i <= produceTaskMaxNumber; i++) {
                String work = "work@ " + i;
                System.out.println("put :" + work);
                //2.往线程池中提交任务
                threadPool.execute(new ThreadPoolTask(work));
                Thread.sleep(produceTaskSleepTime);
            }

            //3.关闭线程池
            threadPool.shutdown();
            threadPool.awaitTermination(1, TimeUnit.HOURS);
            System.out.println("线程池中启动的任务已全部完成");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 线程池执行的任务
     **/
    public static class ThreadPoolTask implements Runnable, Serializable {
        private static final long serialVersionUID = 0;
        //保存任务所需要的数据
        private Object threadPoolTaskData;

        ThreadPoolTask(Object works) {
            this.threadPoolTaskData = works;
        }

        @Override
        public void run() {
            //处理一个任务
            System.out.println("start------" + threadPoolTaskData);
            try {
                //便于观察,等待一段时间
                Thread.sleep(consumeTaskSleepTime);
                System.out.println("end------" + threadPoolTaskData);
            } catch (Exception e) {
                e.printStackTrace();
            }
            threadPoolTaskData = null;
        }

        public Object getTask() {
            return this.threadPoolTaskData;
        }
    }

}

2)用Spring推出的线程池工具 ThreadPoolTaskExecutor

  1. 配置一个线程池类,把线程池对象ThreadPoolTaskExecutor当作bean交给IOC容器,线程池叫taskExecutor

    /**
     * 线程池配置
     *
     * @author zhangna
     */
    @Configuration
    @EnableAsync    //开启对异步任务的支持
    public class ThreadPoolConfig {
    
        /**
         * 配置了一个线程池,通过spring给我们提供的ThreadPoolTaskExecutor就可以使用线程池。
         * 即把ThreadPoolTaskExecutor当作bean交给IOC管理,然后要使用线程池的时候,从IOC那拿ThreadPoolTaskExecutor即可使用线程池
         *
         * @return
         */
        @Bean("taskExecutor")
        public ThreadPoolTaskExecutor getAsyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            //设置核心线程数
            executor.setCorePoolSize(5);
            //设置最大线程数
            executor.setMaxPoolSize(20);
            //配置队列大小
            executor.setQueueCapacity(Integer.MAX_VALUE);
            //设置线程活跃时间(秒)
            executor.setKeepAliveSeconds(60);
            //设置默认线程名前缀
            executor.setThreadNamePrefix("blog_threadPool");
            //等待所有任务结束后再关闭线程池
            executor.setWaitForTasksToCompleteOnShutdown(true);
            //执行初始化
            executor.initialize();
            return executor;
        }
    }
    
  2. 定义一个执行异步任务的方法,并给该任务指定要执行它的线程池taskExecutor

    @Component
    public class ThreadService {
    
        /**
         * 异步操作:更新阅读次数
         * @Async 定义此方法为一个异步任务,使用自定义的线程池,即由@EnableAsync所标注的类下定义的线程池
         */
        @Async("taskExecutor")
        public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
    
            int viewCount = article.getViewCounts();
            //进行articleMapper.update()时,有的值都要设置,所以为了最小限度的更改,就new一个新的Article只设置viewCount
            Article updateArticle = new Article();
            updateArticle.setViewCounts(viewCount+1);
            LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.eq(Article::getId,article.getId());
            //设置一个 为了在多线程的环境下 线程安全
            updateWrapper.eq(Article::getViewCounts,viewCount);
            //update article set view_count=100 where view_count=99 and id=11;
            articleMapper.update(updateArticle,updateWrapper);
    
        }
    }
    
  3. 调用该异步方法

    @Service
    public class ArticleServiceImpl implements ArticleService {
    
        
        @Autowired
        private ThreadService threadService;
        
        ……
            
        threadService.updateArticleViewCount(articleMapper, article);
        
        ……
    }
    

4. CompletableFuture获取多线程运行结果

1)CompletableFuture概述

  1. CompletableFuture是对Feature的增强,Feature只能处理简单的异步任务,而CompletableFuture可以将多个异步任务的结果进行复杂的组合

  2. 当异步方法有返回值时,如何获取异步方法执行的返回结果呢?

    这时需要异步调用的方法带有返回值CompletableFuture

2)CompletableFuture使用示例如下

  1. 定义线程池

  2. 定义异步方法

    @Async("taskExecutor")
    //异步方法的返回值是CompletableFuture<String>
    public CompletableFuture<String> doSomethingComp(String message) throws InterruptedException {
        log.info("do something1: {}", message);
        Thread.sleep(1000);
        return CompletableFuture.completedFuture("do something1: " + message);
    }
    
    
  3. 调用该异步方法,并对方法的返回值做一些操作,eg:可以3个方法并发调用,然后对结果进行合并处理,阻塞主线程;s1执行完成后并发执行s2和s3,然后消费相关结果,不阻塞主线程等等。可以对不同任务之间的结果做很多有用的处理

    @SneakyThrows
    @GetMapping("/open/somethingComp")
    public String somethingComp() {
        int count = 10;
        CompletableFuture[] futures = new CompletableFuture[count];
        // 开启待返回值得异步方法
        for (int i = 0; i < count; i++) {
            futures[i] = asyncService.doSomethingComp("index = " + i);
        }
        try {// 等待所有任务都执行完
            CompletableFuture.allOf(futures).join();
        } catch (Exception e) {
            System.out.println("CompletableFuture error");
        }
    
        System.out.println("Get all return value! ");
        return "success";
    }
    

3)CompletableFuture进阶使用(结合lambda表达式???)

具体看看XXX代码怎么用???

四、其他及JUC

1. ThreadLocal

java.lang.ThreadLocal

1)ThreadLocal是什么

  • ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝。多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
  • THreadLocal的应用场景主要有:session会话管理和数据库连接池中使用

2)如何使用ThreadLocal

//1.创建一个ThreadLocal变量,指定了localVariable的类型为整数。
private ThreadLocal<Integer> localVariable = new ThreadLocal<>();

//2设置和获取这个变量的值(只有当前线程自己看得见)
localVariable.set(8);
localVariable.get();

//3.统一初始化所有线程的ThreadLocal的值
private ThreadLocal<Integer> localVariable = ThreadLocal.withInitial(() -> 6);

3)ThreadLocal实现原理

  1. 先看一下Thread类的源码

    public class Thread implements Runnable {
        //......
        //与此线程有关的ThreadLocal值。由ThreadLocal类维护
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
        //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
        //......
    }
    
    • 可以看出 Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量。我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。
  2. 再来看ThreadLocal类源码中的get,set方法

    public class ThreadLocal<T> {
        //……
        public T get() {
            //获得当前线程
            Thread t = Thread.currentThread();
            //每个线程都有一个自己的ThreadLocalMap,ThreadLocalMap里就保存着所有的ThreadLocal变量
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                //ThreadLocalMap的key就是当前ThreadLocal对象实例,
                //多个ThreadLocal变量都是放在这个map中的
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    //从map里取出来的值就是我们需要的这个ThreadLocal变量
                    T result = (T)e.value;
                    return result;
                }
            }
            // 如果map没有初始化,那么在这里初始化一下
            return setInitialValue();
        }
        
        ThreadLocalMap getMap(Thread t) {
            //返回的就是当前线程t的ThreadLocalMap对象
            return t.threadLocals;
        }
        
        public void set(T value) {
            //获取当前请求的线程    
            Thread t = Thread.currentThread();
            //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
            ThreadLocalMap map = getMap(t);
            if (map != null)
                // 将需要存储的值放入到这个哈希表中
                map.set(this, value);
            else
                createMap(t, value);
        }
    
    
    • 可以发现: 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。

    • 每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

      ThreadLocal 数据结构

4)ThreadLocal内存泄漏问题

  • 原因:ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候, key会被清理掉,而value不会被清理掉。这样一来ThreadLocalMap中就会出现key为null的Entry,如此,value将永远无法被回收,这个时候就可能产生内存泄漏。

  • 解决:ThreadLocalMap实现中的get() set() remove() 方法,会清理掉key为null的记录。使用完ThreadLocal后最好手动调用remove()方法

  • ⚠️java的四种引用:强软弱虚

    • 强引用(Strong Reference):就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就表明该对象还活着,垃圾回收器就不会回收这种对象。 即使当前内存空间不足,JVM也不会回收它,而是抛出OutOfMemoryError。
    • 软引用:在使用软引用时,如果内存空间足够,软引用就能继续使用,而不会被垃圾回收器回收,只有在内存不足时才会被回收
    • 弱引用(Weak Reference):遭GC一定被回收,且无论当前堆内存是否够用。
    • 虚引用:如果一个对象仅持有虚引用,那么相当于没有引用,形同虚设,在任何时候都有可能被垃圾回收器回收

待看文章

血泪教训,线程池引发的内存泄露 - 腾讯云开发者社区-腾讯云 (tencent.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值