JUC并发编程

11 篇文章 0 订阅
10 篇文章 0 订阅

基础

1.并行和并发有什么区别?

线程时cpu分配的最小单位。

并行:同一时刻,两个线程都在执行,要求有两个cpu分别去执行两个线程

并发:同一时刻,只有一个执行,但是在一个时间段内,两个线程都执行了。通过不断切换线程实现

2.说说什么是进程,什么是线程

进程:进程是系统进行资源分配和调度的基本单位。

线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

操作系统在分配资源时是把资源分配给进程的,但是cpu资源比较特殊,它是被分配到线程的,因为真正要占用cpu运行的事线程,所以也说线程事cpu分配的基本单位。

例如在java中,当我们启动main方法其实就启动了一个jvm进程,而main方法在的线程就是这个进程中的一个线程,也称主线程。

一个进程中有多个线程,多个线程公用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。

3.说锁线程有几种创建方式?

java中创建线程的方式主要有三种:

(1)继承Thread类重写run()方法,调用start()方法启动线程

(2)实现Runnable接口,重写run()方法

(3)实现Callable接口,重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值

4.为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?

JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。如果直接调用Thread的run()方法,那么run方法还是运行在主线程,相当于顺序执行,起不到多线程的效果。

5.线程有哪些常用的调度方法?

1.等待:wait(),join()

2.通知:notify(),notifyAll()

3.让出优先权:yield()

4.中断:interrupt(),isinterrupted(),interrupted()

5.休眠:sleep()

线程等待与通知

在 Object 类中有一些方法可以用于线程的等待与通知。

①、wait():当一个线程 A 调用一个共享变量的 wait() 方法时, 线程 A 会被阻塞挂起,直到发生下面几种情况才会返回 :

  • 线程 A 调用了共享对象 notify()或者 notifyAll() 方法;
  • 其他线程调用了线程 A 的 interrupt() 方法,线程 A 抛出 InterruptedException 异常返回。

②、wait(long timeout) :这个方法相比 wait() 方法多了一个超时参数,它的不同之处在于,如果线程 A 调用共享对象的 wait(long timeout)方法后,没有在指定的 timeout 时间内被其它线程唤醒,那么这个方法还是会因为超时而返回。

③、wait(long timeout, int nanos),其内部调用的是 wait(long timout) 方法。

唤醒线程主要有下面两个方法:

①、notify():一个线程 A 调用共享对象的 notify() 方法后,会唤醒一个在这个共享变量上调用 wait 系列方法后被挂起的线程。

一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

②、notifyAll():不同于在共享变量上调用 notify() 方法会唤醒被阻塞到该共享变量上的一个线程,notifyAll 方法会唤醒所有在该共享变量上调用 wait 系列方法而被挂起的线程。

Thread 类还提供了一个 join() 方法,意思是如果一个线程 A 执行了 thread.join(),当前线程 A 会等待 thread 线程终止之后才从 thread.join() 返回。

#线程休眠

sleep(long millis):Thread 类中的静态方法,当一个执行中的线程 A 调用了 Thread 的 sleep 方法后,线程 A 会暂时让出指定时间的执行权。

但是线程 A 所拥有的监视器资源,比如锁,还是持有不让出的。指定的睡眠时间到了后该方法会正常返回,接着参与 CPU 的调度,获取到 CPU 资源后就可以继续运行。

#让出优先权

yield():Thread 类中的静态方法,当一个线程调用 yield 方法时,实际是在暗示线程调度器,当前线程请求让出自己的 CPU,但是线程调度器可能会“装看不见”忽略这个暗示。

#线程中断

Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程会根据中断状态自行处理。

  • void interrupt() :中断线程,例如,当线程 A 运行时,线程 B 可以调用线程 interrupt() 方法来设置线程的中断标志为 true 并立即返回。设置标志仅仅是设置标志, 线程 B 实际并没有被中断,会继续往下执行。
  • boolean isInterrupted() 方法: 检测当前线程是否被中断。
  • boolean interrupted() 方法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。

6.线程有几种状态?

在java中,线程共有六种状态:

new:初始状态:线程被创建,但还没有调用start()方法

runnable:运行状态:java线程将操作系统中的就绪和运行两种状态笼统称作运行

blocked:阻塞状态:表示线程阻塞于锁

waiting:等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)

time_waiting:超时等待状态:该状态不同于waiting,它是可以在制定的事件自行返回的

terminated:终止状态:表示当前线程已经执行完毕

7.什么是线程上下文切换

使用多线程的目的是为了充分利用cpu,但是我们知道,并发其实是一个cpu来应付多个线程。为了让用户感觉多个线程是在同时执行的,cpu资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用cpu执行任务。当线程使用完时间片后,就会处于就绪状态并让出cpu给其他线程占用,这就是上下文切换。

8.守护线程了解吗?

java中的线程分为两类,分别是daemon线程(守护线程)和user线程(用户线程)。

在JVM启动时会调用main方法,main方法所在的线程就是一个用户线程。其实在JVM内部同时还启动了很多守护线程,比如垃圾回收线程。

守护线程和用户线程的区别:当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否存在守护线程,守护线程不影响JVM退出。但是只要还有一个用户线程,JVM就不会退出。

9.线程间有哪些通信方式?

java中线程之间的通信主要是为了解决线程之间如何协作运行的问题。java提供了多种线程通信的方式,使得线程可以在合适的时间和地点进行同步。

(1)volatile和synchronized关键字

volatile:用来修饰成员变量,告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,保证所有线程对变量访问的可见性。

synchronized:可以修饰方法,或者以同步代码块的形式来使用,确保多个线程在同一时刻,只能有一个线程在执行某个方法或某个代码块。

(2)等待/通知机制

一个线程调用共享对象的wait()方法时,他会进入该对象的等待池,并释放已经持有的该对象的锁,进入等待状态,直到其他线程调用相同对象的notify()或notifyAll()方法。

一个线程带哦用共享对象的notify()方法时,它会唤醒在该对象等待池中等待的一个线程,使其进入锁池,等待获取锁。

Condition也提供了类似的方法,await()负责等待,signal()和signalAll()负责通知。

通常与锁(特别是ReentrantLock)一起使用,为线程提供了一种等待某个条件成真的机制,并允许其他线程在该条件变化时通知等待线程。更灵活,更强大。

(3)管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同,它主要用于线程之间的数据传输,而传输的媒介为内存。

管道输入/输出流主要包括了如下 4 种具体实现:PipedOutputStream、PipedInputStream、 PipedReader 和 PipedWriter,前两种面向字节,而后两种面向字符。

(4)使用Thread.join()

如果一个线程 A 执行了 thread.join()语句,其含义是:当前线程 A 等待 thread 线程终止之后才从 thread.join()返回。

(5)使用ThreadLocal

ThreadLocal时java中提供的一种用于实现线程局部变量的工具。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。threadLocal可以用于解决多线程中共享对象的线程安全问题。

ThreadLocal

10.ThreadLocal是什么?

ThreadLocal时线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

11.ThreadLocal的实战场景

用来做用户信息上下文的存储。

我们的系统应用是一个典型的 MVC 架构,登录后的用户每次访问接口,都会在请求头中携带一个 token,在控制层可以根据这个 token,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如 rpc 调用、更新用户获取等等,那应该怎么办呢?

一种办法是显式定义用户相关的参数,比如账号、用户名……这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢?

这时候我们就可以用到 ThreadLocal,在控制层拦截请求把用户信息存入 ThreadLocal,这样我们在任何一个地方,都可以取出 ThreadLocal 中存的用户数据。

很多其它场景的 cookie、session 等等数据隔离也都可以通过 ThreadLocal 去实现。

我们常用的数据库连接池也用到了 ThreadLocal:

  • 数据库连接池的连接交给 ThreadLoca 进行管理,保证当前线程的操作都是同一个 Connnection。

12.ThreadLocal怎么实现的呢?

  • Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,每个线程都有一个属于自己的 ThreadLocalMap。
  • ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 的弱引用,value 是 ThreadLocal 的泛型值。
  • 每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。
  • ThreadLocal 本身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值。

13.ThreadLocal内存泄露是怎么回事?

我们都知道,在 JVM 中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。所以呢,栈中存储了 ThreadLocal、Thread 的引用,堆中存储了它们的具体实例。

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用。

“弱引用:只要垃圾回收机制一运行,不管 JVM 的内存空间是否充足,都会回收该对象占用的内存。”

那么现在问题就来了,弱引用很容易被回收,如果 ThreadLocal(ThreadLocalMap 的 Key)被垃圾回收器回收了,但是 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap 的 key 没了,value 还在,这就会造成了内存泄漏问题

那怎么解决内存泄漏问题呢?

很简单,使用完 ThreadLocal 后,及时调用 remove()方法释放内存空间。

那为什么 key 还要设计成弱引用?

key 设计成弱引用同样是为了防止内存泄漏。

假如 key 被设计成强引用,如果 ThreadLocal Reference 被销毁,此时它指向 ThreadLoca 的强引用就没有了,但是此时 key 还强引用指向 ThreadLocal,就会导致 ThreadLocal 不能被回收,这时候就发生了内存泄漏的问题。

14.ThreadLocalMap的结构了解吗?

ThreadLocalMap 虽然被叫做 Map,其实它是没有实现 Map 接口的,但是结构还是和 HashMap 比较类似的,主要关注的是两个要素:元素数组散列方法

元素数组

一个 table 数组,存储 Entry 类型的元素,Entry 是 ThreaLocal 弱引用作为 key,Object 作为 value 的结构

散列方法

散列方法就是怎么把对应的 key 映射到 table 数组的相应下标,ThreadLocalMap 用的是哈希取余法,取出 key 的 threadLocalHashCode,然后和 table 数组长度减一&运算(相当于取余)。

15.ThreadLocalmap怎么解决Hash冲突?

ThreadLocalMap 没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

16.ThreadLocalMap扩容机制了解吗?

在 ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:再着看 rehash()具体实现:这里会先去清理过期的 Entry,然后还要根据条件判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4来决定是否需要扩容。

17.父子线程怎么共享数据?

父线程能用 ThreadLocal 来给子线程传值吗?毫无疑问,不能。那该怎么办?

这时候可以用到另外一个类——InheritableThreadLocal 

使用起来很简单,在主线程的 InheritableThreadLocal 实例设置值,在子线程中就可以拿到了。

Java内存模型

18.说一下你对JaVa内存模型JMM的理解?

Java内存模型是一种抽闲的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

19.说说你对原子性,可见性,有序性的理解?

原子性:原子性指的是一个操作不可分割,不可中断的,要么全部执行成功,要么全部都不执行,中间不会被打断。

可见性:可见性指的是一个线程修改了某一个共享变量的值时,其他线程能够立即知道这个修改

有序性:有序性指的是对于一个线程的执行代码,从前后依次执行,但线程下可以认为程序是有序的,但是并发时有可能发生指令重排。

原子性、可见性、有序性都应该怎么保证呢?

  • 原子性:JMM 只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized 
  • 可见性:Java 是利用volatile关键字来保证可见性的,除此之外,finalsynchronized也能保证可见性。
  • 有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。

20.那说说什么是指令重排?

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分 3 种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

21.指令重排有限制吗?happens-before了解吗?

指令重排也是有一些限制的,有两个规则happens-beforeas-if-serial来约束。

happens-before 的定义:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么这种重排序并不非法

happens-before 和我们息息相关的有六大规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • start()规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
  • join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

22.as-if-serial又是什么?但线程的程序一定是顺序吗?

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

23.volatile实现原理了解吗?

volatile有两个作用,保证可见性和有序性。

相比 synchronized 的加锁方式来解决共享变量的内存可见性问题,volatile 就是更轻量的选择,它没有上下文切换的额外开销成本。

可见性

volatile 可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

有序性

重排序可以分为编译器重排序和处理器重排序,volatile保证有序性,就是通过分别限制这两种类型的重排序。

24.synchronized用过吗?怎么使用?

synchronized用来保证代码的原子性。主要有三种用法;

(1)修饰实例方法:作用于当前兑现实例枷锁,进入同步代码前要获得当前对象实例的锁

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

(2)修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。

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

(3)修饰代码块:指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized(类.class) 表示进⼊同步代码前要获得 当前 class 的锁

synchronized(this) {
 //业务代码
}

25.synchronized的实现原理?

synchronized怎么加锁?

我们使用 synchronized 的时候,发现不用自己去 lock 和 unlock,是因为 JVM 帮我们把这个事情做了。

1.synchronized 修饰代码块时,JVM 采用monitorentermonitorexit两个指令来实现同步,monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

2.synchronized 修饰同步方法时,JVM 采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

synchronized锁住的是什么?

monitorenter、monitorexit 或者 ACC_SYNCHRONIZED 都是基于 Monitor 实现的。

实例对象结构里有对象头,对象头里面有一块结构叫 Mark Word,Mark Word 指针指向了monitor

所谓的 Monitor 其实是一种同步工具,也可以说是一种同步机制。在 Java 虚拟机(HotSpot)中,Monitor 是由ObjectMonitor 实现的,可以叫做内部锁,或者 Monitor 锁。

Monitor 机制比较相似:

  • 门诊大厅:所有待进入的线程都必须先在入口 Entry Set挂号才有资格;
  • 就诊室:就诊室**_Owner**里里只能有一个线程就诊,就诊完线程就自行离开
  • 候诊室:就诊室繁忙时,进入等待区(Wait Set),就诊室空闲的时候就从**等待区(Wait Set)**叫新的线程

同步是锁住的什么东西:

  • monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。
  • monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。

26.除了原子性,synchronized可见性,有序性,可重入性怎么实现?

synchronized 怎么保证可见性?

  • 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
  • 线程加锁后,其它线程无法获取主内存中的共享变量。
  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。

synchronized 怎么保证有序性?

synchronized 同步的代码块,具有排他性,一次只能被一个线程拥有,所以 synchronized 保证同一时刻,代码是单线程执行的。

因为 as-if-serial 语义的存在,单线程的程序能保证最终结果是有序的,但是不保证不会指令重排。

所以 synchronized 保证的有序是执行结果的有序性,而不是防止指令重排的有序性。

synchronized 怎么实现可重入的呢?

synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁的临界资源,这种情况称为可重入锁。

synchronized 锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。

之所以,是可重入的。是因为 synchronized 锁对象有个计数器,会随着线程获取锁后 +1 计数,当线程执行完毕后 -1,直到清零释放锁。

27.锁升级?synchronized优化了解吗?

synchronized 做了哪些优化?

在 JDK1.6 之前,synchronized 的实现直接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为重量级锁。从 JDK6 开始,HotSpot 虚拟机开发团队对 Java 中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了 synchronized 的性能。

  • 偏向锁:在无竞争的情况下,只是在 Mark Word 里存储当前线程指针,CAS 操作都不做。

  • 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有 CAS 操作的开销。

  • 自旋锁:减少不必要的 CPU 上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式

  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

  • 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

锁升级的过程是什么样的?

锁升级方向:无锁-->偏向锁---> 轻量级锁---->重量级锁,这个方向基本上是不可逆的。

我们看一下升级的过程:

#偏向锁:

偏向锁的获取:

  1. 判断是否为可偏向状态--MarkWord 中锁标志是否为‘01’,是否偏向锁是否为‘1’
  2. 如果是可偏向状态,则查看线程 ID 是否为当前线程,如果是,则进入步骤'5',否则进入步骤‘3’
  3. 通过 CAS 操作竞争锁,如果竞争成功,则将 MarkWord 中线程 ID 设置为当前线程 ID,然后执行‘5’;竞争失败,则执行‘4’
  4. CAS 获取偏向锁失败表示有竞争。当达到 safepoint 时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块
  5. 执行同步代码

偏向锁的撤销:

  1. 偏向锁不会主动释放(撤销),只有遇到其他线程竞争时才会执行撤销,由于撤销需要知道当前持有该偏向锁的线程栈状态,因此要等到 safepoint 时执行,此时持有该偏向锁的线程(T)有‘2’,‘3’两种情况;
  2. 撤销----T 线程已经退出同步代码块,或者已经不再存活,则直接撤销偏向锁,变成无锁状态----该状态达到阈值 20 则执行批量重偏向
  3. 升级----T 线程还在同步代码块中,则将 T 线程的偏向锁升级为轻量级锁,当前线程执行轻量级锁状态下的锁获取步骤----该状态达到阈值 40 则执行批量撤销
#轻量级锁:

轻量级锁的获取:

  1. 进行加锁操作时,jvm 会判断是否已经时重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象 MarkWord 复制到该锁记录中
  2. 复制成功之后,jvm 使用 CAS 操作将对象头 MarkWord 更新为指向锁记录的指针,并将锁记录里的 owner 指针指向对象头的 MarkWord。如果成功,则执行‘3’,否则执行‘4’
  3. 更新成功,则当前线程持有该对象锁,并且对象 MarkWord 锁标志设置为‘00’,即表示此对象处于轻量级锁状态
  4. 更新失败,jvm 先检查对象 MarkWord 是否指向当前线程栈帧中的锁记录,如果是则执行‘5’,否则执行‘4’
  5. 表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分(Displaced Mark Word)为 null,并指向 Mark Word 的锁对象,起到一个重入计数器的作用。
  6. 表示该锁对象已经被其他线程抢占,则进行自旋等待(默认 10 次),等待次数达到阈值仍未获取到锁,则升级为重量级锁

28.说说synchronized和ReentrantLock的区别?

可以从锁的实现、功能特点、性能等几个维度去回答这个问题:

  • 锁的实现: synchronized 是 Java 语言的关键字,基于 JVM 实现。而 ReentrantLock 是基于 JDK 的 API 层面实现的(一般是 lock()和 unlock()方法配合 try/finally 语句块来完成。)
  • 性能: 在 JDK1.6 锁优化以前,synchronized 的性能比 ReenTrantLock 差很多。但是 JDK6 开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
  • 功能特点: ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。 
    • ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()来实现这个机制
    • ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
    • synchronized 与 wait()和 notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock 类借助 Condition 接口与 newCondition()方法实现。
    • ReentrantLock 需要手工声明来加锁和释放锁,一般跟 finally 配合释放锁。而 synchronized 不用手动释放锁。

29.AQS了解多少?

AbstractQueuedSynchronizer 抽象同步队列,简称 AQS ,它是 Java 并发包的根基,并发包中的锁就是基于 AQS 实现的。

  • AQS 是基于一个 FIFO 的双向队列,其内部定义了一个节点类 Node,Node 节点内部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入 AQS 队列的, EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入 AQS 队列
  • AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性
  • 获取 state 的方式分为两种,独占方式和共享方式,一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过 CAS 的方式进行获取。
  • 如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中。

AQS 中的 CLH 变体等待队列拥有以下特性:

  • AQS 中队列是个双向链表,也是 FIFO 先进先出的特性
  • 通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
  • Head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程
  • 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好

30.ReentrantLock实现原理?

ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。

new ReentrantLock() 构造方法默认创建的是非公平锁 NonfairSync。

公平锁 FairSync

  1. 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁
  2. 公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大

非公平锁 NonfairSync

  • 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁
  • 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁

默认创建的对象 lock()的时候:

  • 如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 ,然后直接返回。如果当前线程之前己经获取过该锁,则这次只是简单地把 AQS 的状态值加 1 后返回。
  • 如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的话,则调用该方法线程会被放入 AQS 队列阻塞挂起。

31.Reentrantlock怎么实现公平锁的?

同时也可以在创建锁构造方法中传入具体参数创建公平锁 FairSync

非公平锁和公平锁的两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

32.cas呢?cas了解多少?

CAS 叫做 CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性的。

CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变量的新值 C。

只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的 。

33.cas有什么问题?如何解决?

ABA 问题

并发环境下,假设初始条件是 A,去修改数据时,发现是 A 就会执行修改。但是看到的虽然是 A,中间可能发生了 A 变 B,B 又变回 A 的情况。此时 A 已经非彼 A,数据即使成功修改,也可能有问题。

怎么解决 ABA 问题?

  • 加版本号

每次修改变量,都在这个变量的版本号上加 1,这样,刚刚 A->B->A,虽然 A 的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的 A 已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。

Java 提供了 AtomicStampReference 类,它的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。

#循环性能开销

自旋 CAS,如果一直循环执行,一直不成功,会给 CPU 带来非常大的执行开销。

怎么解决循环性能开销问题?

在 Java 中,很多使用自旋 CAS 的地方,会有一个自旋次数的限制,超过一定次数,就停止自旋。

#只能保证一个变量的原子操作

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

怎么解决只能保证一个变量的原子操作问题?

  • 可以考虑改用锁来保证操作的原子性
  • 可以考虑合并多个变量,将多个变量封装成一个对象,通过 AtomicReference 来保证原子性。

34.Java有哪些保证原子性的方法?如何保证多线程下i++结果正确?

(1)使用循环原子类例如AtomicInteger,实现i++原子操作

(2)使用juc包下的锁,如ReentrantLock,对i++操作加锁lock.lock()来实现原子性

(3)使用synchronized,对i++操作加锁

35.原子操作类了解多少?

当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量 i=1,A 线程更新 i+1,B 线程也更新 i+1,经过两个线程操作之后可能 i 不等于 3,而是等于 2。因为 A 和 B 线程在更新变量 i 的时候拿到的 i 都是 1,这就是线程不安全的更新操作,一般我们会使用 synchronized 来解决这个问题,synchronized 会保证多线程不会同时更新变量 i。

其实除此之外,还有更轻量级的选择,Java 从 JDK 1.5 开始提供了 java.util.concurrent.atomic 包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。

因为变量的类型有很多种,所以在 Atomic 包里一共提供了 13 个类,属于 4 种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。

Atomic 包里的类基本都是使用 Unsafe 实现的包装类。

使用原子的方式更新基本类型,Atomic 包提供了以下 3 个类:

  • AtomicBoolean:原子更新布尔类型。

  • AtomicInteger:原子更新整型。

  • AtomicLong:原子更新长整型。

通过原子的方式更新数组里的某个元素,Atomic 包提供了以下 4 个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。

  • AtomicLongArray:原子更新长整型数组里的元素。

  • AtomicReferenceArray:原子更新引用类型数组里的元素。

  • AtomicIntegerArray 类主要是提供原子的方式更新数组里的整型

原子更新基本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下 3 个类:

  • AtomicReference:原子更新引用类型。

  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)。

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic 包提供了以下 3 个类进行原子字段更新:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

36.AtomicInetger的原理?

一句话概括:使用 CAS 实现

compareAndSwapInt 是一个 native 方法,基于 CAS 来操作 int 类型变量。其它的原子操作类基本都是大同小异。

37.线程死锁了解吗?该如何避免?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

那么为什么会产生死锁呢? 死锁的产生必须具备以下四个条件:

  • 互斥条件:指线程对己经获取到的资源进行它性使用,即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个 线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其它线程占有,所以当前线程会被阻塞,但阻塞 的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 占用的资源,Tl1 正在等待 T2 用的资源,…… Tn 在等待己被 T0 占用的资源。

该如何避免死锁呢?答案是至少破坏死锁发生的一个条件

  • 其中,互斥这个条件我们没有办法破坏,因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?
  • 对于“请求并持有”这个条件,可以一次性请求所有的资源。
  • 对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
  • 对于“环路等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

38.那死锁问题怎么排查呢?

可以使用 jdk 自带的命令行工具排查:

  1. 使用 jps 查找运行的 Java 进程:jps -l
  2. 使用 jstack 查看线程堆栈信息:jstack -l 进程 id

并发工具类

39.CountDownLatch(倒计时器)了解吗?

场景 1:协调子线程结束动作:等待所有子线程运行结束

CountDownLatch 允许一个或多个线程等待其他线程完成操作。

场景 2. 协调子线程开始动作:统一各线程动作开始的时机

CountDownLatch 的核心方法也不多:

  • await():等待 latch 降为 0;
  • boolean await(long timeout, TimeUnit unit):等待 latch 降为 0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老。
  • countDown():latch 数量减 1;
  • getCount():获取当前的 latch 数量。

40.CyclicBarrier(同步屏障)了解吗?

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

它和 CountDownLatch 类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作,但是为什么要有 CyclicBarrier,自然是它有和 CountDownLatch 不同的地方。

CyclicBarrier 最最核心的方法,仍然是 await():

  • 如果当前线程不是第一个到达屏障的话,它将会进入等待,直到其他线程都到达,除非发生被中断屏障被拆除屏障被重设等情况;

41.CyclicBarrier 和 CountDownLatch 有什么区别?

两者最核心的区别:

  • CountDownLatch 是一次性的,而 CyclicBarrier 则可以多次设置屏障,实现重复利用;
  • CountDownLatch 中的各个子线程不可以等待其他线程,只能完成自己的任务;而 CyclicBarrier 中的各个线程可以等待其他线程

42.Semaphore(信号量)了解吗?

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

我们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,离开停车场就是线程执行完毕,看见红灯就表示线程被阻塞,不能执行,Semaphore 的本质就是协调多个线程对共享资源的获取

43.Exchanger 了解吗?

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

线程池

44.什么是线程池?

线程池: 简单理解,它就是一个管理线程的池子。

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走 GC 垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

45.能说说工作中线程池的应用吗?

之前我们有一个和第三方对接的需求,需要向第三方推送数据,引入了多线程来提升数据推送的效率,其中用到了线程池来管理线程。消息队列的生产者

线程池的参数如下:

  • corePoolSize:线程核心参数选择了 CPU 数 ×2

  • maximumPoolSize:最大线程数选择了和核心线程数相同

  • keepAliveTime:非核心闲置线程存活时间直接置为 0

  • unit:非核心线程保持存活的时间选择了 TimeUnit.SECONDS 秒

  • workQueue:线程池等待队列,使用 LinkedBlockingQueue 阻塞队列

46.能简单说一下线程池的工作流程吗?

上面的这个流程几乎就跟 JDK 线程池的大致流程类似,

  1. 营业中的 3 个窗口对应核心线程池数:corePoolSize
  2. 总的营业窗口数 6 对应:maximumPoolSize
  3. 打开的临时窗口在多少时间内无人办理则关闭对应:unit
  4. 排队区就是等待队列:workQueue
  5. 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler
  6. threadFactory 该参数在 JDK 中是 线程工厂,用来创建线程对象,一般不会动。

所以我们线程池的工作流程也比较好理解了:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。

47.线程池主要参数有哪些?

线程池有七大参数,需要重点关注corePoolSizemaximumPoolSizeworkQueuehandler这四个。

1.corePoolSize

此值是用来初始化线程池中核心线程数,当线程池中线程池数< corePoolSize时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize 时,新任务会追加到 workQueue 中。

2.maximumPoolSize

maximumPoolSize表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 < maximumPoolSize时候就会再次创建新的线程。

3.keepAliveTime

非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。

4.unit

线程池中非核心线程保持存活的时间的单位

  • TimeUnit.DAYS; 天
  • TimeUnit.HOURS; 小时
  • TimeUnit.MINUTES; 分钟
  • TimeUnit.SECONDS; 秒
  • TimeUnit.MILLISECONDS; 毫秒
  • TimeUnit.MICROSECONDS; 微秒
  • TimeUnit.NANOSECONDS; 纳秒

5.workQueue

线程池等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize 时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,等待队列应该尽量用有界的。

6.threadFactory

创建一个新线程时使用的工厂,可以用来设定线程名、是否为 daemon 线程等等。

7.handler

corePoolSizeworkQueuemaximumPoolSize都不可用的时候执行的饱和策略。

48.线程池的拒绝策略有哪些?

  • AbortPolicy :直接抛出异常,默认使用此策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务
  • DiscardPolicy :当前任务直接丢弃

想实现自己的拒绝策略,实现 RejectedExecutionHandler 接口即可。

49.线程池有哪几种阻塞队列?

  • ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
  • LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)是基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE,吞吐量通常要高于 ArrayBlockingQuene;newFixedThreadPool 线程池使用了这个队列
  • DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool 线程池使用了这个队列。
  • PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列
  • SynchronousQueue:SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene,newCachedThreadPool 线程池使用了这个队列。

50.线程池提交 execute 和 submit 有什么区别?

  1. execute 用于提交不需要返回值的任务
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get()方法来获取返回值

51.线程池怎么关闭知道吗?

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。

shutdown() 将线程池状态置为 shutdown,并不会立即停止

  1. 停止接收外部 submit 的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 等到第二步完成后,才真正停止

shutdownNow() 将线程池状态置为 stop。一般会立即停止,事实上不一定

  1. 和 shutdown()一样,先停止接收外部提交的任务
  2. 忽略队列里等待的任务
  3. 尝试将正在跑的任务 interrupt 中断
  4. 返回未执行的任务列表

shutdown 和 shutdownnow 简单来说区别如下:

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown()只是关闭了提交通道,用 submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

52.线程池的线程数应该怎么配置?

线程在 Java 中属于稀缺资源,线程池不是越大越好也不是越小越好。任务分为计算密集型、IO 密集型、混合型。

  1. 计算密集型:大部分都在用 CPU 跟内存,加密,逻辑操作业务处理等。
  2. IO 密集型:数据库链接,网络通讯传输等。

一般的经验,不同类型线程池的参数配置:

  1. 计算密集型一般推荐线程池不要过大,一般是 CPU 数 + 1,+1 是因为可能存在页缺失(就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的 进行线程上下文切换跟任务调度。获得当前 CPU 核心数代码如下:
  2. IO 密集型:线程数适当大一点,机器的 Cpu 核心数*2。
  3. 混合型:可以考虑根绝情况将它拆分成 CPU 密集型和 IO 密集型任务,如果执行时间相差不大,拆分可以提升吞吐量,反之没有必要。

53.有哪几种常见的线程池?

面试常问,主要有四种,都是通过工具类 Excutors 创建出来的,需要注意,阿里巴巴《Java 开发手册》里禁止使用这种方式来创建线程池。

54.能说一下四种常见线程池的原理吗?

前三种线程池的构造直接调用 ThreadPoolExecutor 的构造方法。

newSingleThreadExecutor的线程池特点
  • 核心线程数为 1
  • 最大线程数也为 1
  • 阻塞队列是无界队列 LinkedBlockingQueue,可能会导致 OOM
  • keepAliveTime 为 0

工作流程:

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,将任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程执行任务。

适用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

newFixedThreadPool的线程池特点:
  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即 keepAliveTime 为 0
  • 阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM

工作流程:

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

使用场景

FixedThreadPool 适用于处理 CPU 密集型的任务,确保 CPU 在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

newCachedThreadPool的线程池特点:
  • 核心线程数为 0
  • 最大线程数为 Integer.MAX_VALUE,即无限大,可能会因为无限创建线程,导致 OOM
  • 阻塞队列是 SynchronousQueue
  • 非核心线程空闲存活时间为 60 秒

工作流程:

  • 提交任务
  • 因为没有核心线程,所以任务直接加到 SynchronousQueue 队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活 60 秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

适用场景

用于并发执行大量短期的小任务。

newScheduledThreadPool线程池特点
  • 最大线程数为 Integer.MAX_VALUE,也有 OOM 的风险
  • 阻塞队列是 DelayedWorkQueue
  • keepAliveTime 为 0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

工作机制

  • 线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任务是指 ScheduledFutureTask 的 time 大于等于当前时间。
  • 线程执行这个 ScheduledFutureTask。
  • 线程修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间。
  • 线程把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

使用场景

周期性执行任务的场景,需要限制线程数量的场景

使用无界队列的线程池会导致什么问题吗?

例如 newFixedThreadPool 使用了无界的阻塞队列 LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致 OOM。

55.线程池异常怎么处理知道吗?

在使用线程池处理任务的时候,任务代码可能抛出 RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

56.能说下线程池有几种状态吗?

线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。

RUNNING

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的 shutdown()方法,可以切换到 SHUTDOWN 状态;
  • 调用线程池的 shutdownNow()方法,可以切换到 STOP 状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入 TIDYING 状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入 TIDYING 状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为 0。
  • terminated()执行完毕,进入 TERMINATED 状态

TERMINATED

  • 该状态表示线程池彻底终止

57.线程池如何实现参数的动态修改?

这里主要有两个思路:

  • 在我们微服务的架构下,可以利用配置中心如 Nacos、Apollo 等等,也可以自己开发配置中心。业务服务读取线程池配置,获取相应的线程池实例来修改线程池的参数。

  • 如果限制了配置中心的使用,也可以自己去扩展ThreadPoolExecutor,重写方法,监听线程池参数变化,来动态修改线程池参数。

线程池调优了解吗?

上线之前也要进行充分的测试,上线之后要建立完善的线程池监控机制。

事中结合监控告警机制,分析线程池的问题,或者可优化点,结合线程池动态参数配置机制来调整配置。

事后要注意仔细观察,随时调整。

58.你能设计实现一个线程池吗?

我们自己的实现就是完成这个核心流程:

  • 线程池中有 N 个工作线程
  • 把任务提交给线程池运行
  • 如果线程池已满,把任务放入队列
  • 最后当有空闲时,获取队列中任务来执行

59.单机线程池执行断电了应该怎么处理?

我们可以对正在处理和阻塞队列的任务做事务管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。

也就是说,对阻塞队列持久化;正在处理任务事务控制;断电之后正在处理任务的回滚,通过日志恢复该次操作;服务器重启后阻塞队列中的数据再加载。

60.Fork/Join 框架了解吗?

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

要想掌握 Fork/Join 框架,首先需要理解两个点,分而治之工作窃取算法

分而治之

Fork/Join 框架的定义,其实就体现了分治思想:将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

工作窃取算法

大任务拆成了若干个小任务,把这些小任务放到不同的队列里,各自创建单独线程来执行队列里的任务。

那么问题来了,有的线程干活块,有的线程干活慢。干完活的线程不能让它空下来,得让它去帮没干完活的线程干活。它去其它线程的队列里窃取一个任务来执行,这就是所谓的工作窃取

工作窃取发生的时候,它们会访问同一个队列,为了减少窃取任务线程和被窃取任务线程之间的竞争,通常任务会使用双端队列,被窃取任务线程永远从双端队列的头部拿,而窃取任务的线程永远从双端队列的尾部拿任务执行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值