Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇

金三银四面试题之并发编程篇

        • 1. volatile关键字有什么特性?
        • 2. 为什么会产生可见性的问题?
        • 3. 什么是JMM(Java内存模型)?
        • 4. 能谈一谈JMM是如何进行数据同步的吗?
        • 5. 为什么volatile关键字能够保证可见性?
        • 6. 锁的机制分为哪两种?
        • 7. volatile关键字为什么不能保证原子性?
        • 8. volatile关键字为什么会存在伪共享问题?
        • 9. 如何解决volatile关键字的伪共享问题?
        • 10. 能谈谈什么是重排序吗?
        • 11. 什么是重排序问题?
        • 12. 如何解决重排序问题?
        • 13. 双重检验锁为什么需要加上volatile关键字?
        • 14. synchronized和volatile的区别?
        • 15. Java中有哪些锁的分类呢?
        • 16. 谈谈乐观锁与悲观锁的区别?
        • 17. 谈谈公平锁与非公平锁的区别?
        • 18. 谈谈独占锁与共享锁的区别?
        • 19. 什么是锁的可重入性?
        • 20. 什么是CAS?
        • 21. CAS有什么优缺点?
        • 22. 如何解决CAS的ABA问题?
        • 23. synchronized锁如何使用?
        • 24. 能谈一谈synchronized锁的原理吗?
        • 25. 能谈一谈synchronized锁的升级过程吗?
        • 26. 使用synchronized锁时应该注意哪些问题?
        • 27. 产生死锁的四个必要条件是什么?
        • 28. 谈谈Lock锁的实现原理?
        • 29. 什么是AQS?
        • 30.什么是Condition?
        • 31. 什么是Semaphore?
        • 32. 什么是CountDownLatch?
        • 33. 什么是线程池?
        • 34. 为什么要使用线程池?
        • 35. 线程池有什么作用?
        • 36. 线程池有哪些创建方式?
        • 37.线程池底层是如何实现复用的?
        • 38. ThreadPoolExecutor核心参数有哪些?
        • 39. 线程池创建的线程会一直在运行状态吗?
        • 40. 为什么阿里巴巴不建议使用Executors?
        • 41. 能谈谈线程池的底层原理吗?
        • 42. 线程池队列满了,任务会丢失吗?
        • 43. 线程池如何合理配置参数?
        • 44. 什么是FutureTask?
        • 45. 什么是ForkJoin?
        • 46. 谈谈你对ThreadLocal的理解?
        • 47. 哪些地方使用到了ThreadLocal?
        • 48. ThreadLocal与synchronized的区别?
        • 49. 谈谈ThreadLocal的底层实现原理?
        • 50. ThreadLocal为什么会引发内存泄漏问题?
        • 51. 如何防止ThreadLocal发生内存泄漏问题?

1. volatile关键字有什么特性?

保证可见性、防止重排序、不能保证原子性。
volatile关键字能够保证线程的可见性,当一个线程修改共享变量时,能够对另外一个线程保证可见,但是它不能够保证共享变量的原子性问题。

2. 为什么会产生可见性的问题?

因为CPU在读取主内存共享变量的时候,效率非常低,所以每个CPU都会设置对应的高速缓存(L1、L2、L3),用来缓存共享变量主内存中的副本,而每个CPU对应共享变量的副本与副本之间可能会存在数据不一致性的问题。

补充:
多CPU:一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。
CPU寄存器:每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。
高速缓存cache:由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。
内存:一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。
运作原理:通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。
一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。
Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第1张图片

3. 什么是JMM(Java内存模型)?

Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
主内存:存放共享变量数据;
工作内存:每个CPU对应共享变量的副本。
Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第2张图片

4. 能谈一谈JMM是如何进行数据同步的吗?

JMM有八大同步规范:

  1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态;
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量;
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;
  8. write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中。

Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第3张图片

5. 为什么volatile关键字能够保证可见性?

因为volatile关键字底层会通过汇编lock前缀指令触发底层锁的机制来解决多个不同CPU之间缓存数据同步的问题。

6. 锁的机制分为哪两种?

总线锁:当一个CPU(线程)访问到主内存中的数据时,会往总线发出一个Lock锁的信号,其他的线程不能对该主内存做任何操作,变为阻塞状态。该模式存在非常大的缺陷,会将并行的程序变为串行,没有真正发挥出CPU多核的好处。
MESI缓存一致性协议:

  1. M 修改(Modified):如果当前CPU副本数据与主内存数据不一致,则当前CPU的状态为M;
  2. E 独享、互斥(Exclusive):在只有一个CPU(线程)的情况下,如果CPU副本数据与主内存数据保持一致,则当前CPU的状态为E;
  3. S 共享(Shared):在多个CPU(线程)的情况下,如果每个CPU的副本数据都与主内存数据保持一致,则当前CPU的状态为S;
  4. I 无效(Invalid):如果当前CPU的状态为M,则会通过总线嗅探机制通知给其他CPU,让其他CPU都变成I状态,为I状态的CPU会主动获取主内存的数据进行同步更新。
    Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第4张图片
    Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第5张图片

7. volatile关键字为什么不能保证原子性?

volatile关键字为了能够保证数据的可见性,会及时的将工作内存中的数据刷新到主内存中,导致其它工作内存的数据变为无效状态。比如在做count++的操作时,有的线程会出现操作丢失的情况。

8. volatile关键字为什么会存在伪共享问题?

因为CPU默认会以缓存行(大小是2的幂次方,默认为64字节)的形式读取主内存中的数据,如果该变量共享到同一个缓存行,就会影响到整理性能。
例如:线程1修改了long类型变量A,long类型定义变量占用8个字节,由于缓存一致性协议,线程2的变量A副本会失效,线程2在读取主内存中的数据的时候,以缓存行的形式读取,无意间将主内存中的共享变量B也读取到内存中,而主内存中的变量B没有发生变化。
Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第6张图片

9. 如何解决volatile关键字的伪共享问题?

  1. 使用缓存行填充方案避免伪共享;(JDK1.6 可以写在同一个类中,从JDK1.7开始必须要另写一个类单独继承);
  2. 可以直接在类上加上该注解@sun.misc.Contended,启动的时候需要加上该参数-XX:-RestrictContended。

10. 能谈谈什么是重排序吗?

当我们的CPU写入缓存的时候发现缓存区正在被其他cpu站有的情况下,为了能够提高CPU处理的性能可能将后面的读缓存命令优先执行。
注意:不是随便重排序,需要遵循as-ifserial语义。
as-ifserial:不管怎么重排序(编译器和处理器为了提高并行的效率)单线程程序执行结果不会发生改变的,也就是我们编译器与处理器不会对存在数据依赖的关系操作做重排序。

11. 什么是重排序问题?

重排序问题是CPU指令重排序优化的过程存在的问题,在单线程的情况下是不会存在问题的,但是在多线程的情况下,指令逻辑无法分辨因果关系,可能指令会存在乱序的问题,导致执行结果发生错误。

12. 如何解决重排序问题?

  1. 手动插入内存屏障;
  2. 使用volatile关键字防止指令重排。

13. 双重检验锁为什么需要加上volatile关键字?

因为多个线程在获取实例时,创建实例的时候会存在重排序的问题。
创建一个对象一般会分为三个步骤:
step1:分配对象的内存空间;
step2:调用构造函数初始化;
step3:将对象赋值给变量;
但是由于CPU会进行指令重排,可能会先执行第三步再执行第二步,如果t1线程获取到锁准备创建实例时,这个时候发生了指令重排,先将对象赋值给了变量,而t2线程进来的时候,对象已经不为null了,所以t2线程可以自由访问该对象,可由于对象还没有调用构造函数进行初始化,t2线程获取到的是一个不完整的对象,访问时会发生异常。
Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第7张图片

14. synchronized和volatile的区别?

  1. volatile只能修饰变量,而synchronized可以修饰方法、变量、类和代码块;
  2. volatile只能保证可见性,不能保证原子性,而synchronized既能保证可见性,又能保证原子性;
  3. volatile不会造成线程阻塞,而synchronized会造成线程阻塞;
  4. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

15. Java中有哪些锁的分类呢?

  1. 悲观锁和乐观锁;
  2. 公平锁和非公平锁;
  3. 独占锁和共享锁;
  4. 轻量级锁和重量级锁;
  5. 自旋锁;
  6. 重入锁;

16. 谈谈乐观锁与悲观锁的区别?

悲观锁:没有获取到锁的线程会阻塞等待;
站在MySQL的角度分析:悲观锁比较悲观,当多个线程对同一行数据实现修改的时候, 最终只有一个线程能够修改成功,只要谁能够获取到行锁,则其他线程是不能够对该数据做任何修改操作的,且是阻塞状态。
站在Java锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高,需要被CPU重新从就绪状态调度为运行状态。

乐观锁:如果没有获取到锁,当前线程不会阻塞等待,通过死循环控制。乐观锁属于无锁机制,没有竞争锁的流程。
乐观锁比较乐观,通过阈值或者版本号进行比较,如果不一致的情况则通过循环控制修改,当前线程不会被阻塞,效率比较高,但是乐观锁比较消耗CPU资源。

17. 谈谈公平锁与非公平锁的区别?

公平锁:比较公平,按照请求锁的顺序进行排列,先请求的则先获取锁,后请求的则后获取锁;-- new ReentrantLock(true)
非公平锁:通过争抢的方式获取锁,效率比公平锁高;-- new ReentrantLock(false)

18. 谈谈独占锁与共享锁的区别?

独占锁:在多线程中,只允许一个线程获取到锁,其他线程都会阻塞等待;
共享锁:多个线程可以同时持有锁,比如ReentrantLock读写锁:读读共享、写写互斥、读写互斥、写读互斥;

19. 什么是锁的可重入性?

在同一个线程中锁可以不断传递的,可以直接获取。

20. 什么是CAS?

CAS: Compare and Swap,翻译成比较并交换。执行函数CAS(V, E, N)
CAS有3个操作数:V(内存值)、E(旧的预期值)、N(要修改的新值)。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。
没有获取到锁的线程是不会阻塞的,通过循环控制一直不断的获取锁。
原子类:AtomicBoolean、AtomicInteger、AtomicLong等均使用CAS实现。
CAS的原理:当E(旧的预期值)===V(共享变量中值)时,才会修改V。
Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第8张图片
基于CAS实现锁机制原理:

  1. 定义一个锁的状态;
  2. 如果状态值为0,则表示没有线程获取到该锁;
  3. 如果状态值为1,则表示有线程已经持有该锁;

实现细节:
CAS获取锁:将该锁的状态从0改为1,如果能够修改成功,则表示获取锁成功,如果修改失败,则表示获取锁失败,但是没获取到锁的线程不会阻塞而是通过循环(自旋)来控制重试;
CAS释放锁:将该锁的状态从1改为0,如果能够修改成功,则表示释放锁成功。

21. CAS有什么优缺点?

优点:没有获取到锁的线程,会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制进行重试,效率高。
缺点:通过死循环控制,消耗CPU资源,需要控制循环次数,避免CPU飙升问题。

22. 如何解决CAS的ABA问题?

ABA问题:线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。ABA问题只是概念产生了冲突,并不影响结果。
解决ABA问题的方案就是给值加一个修改版本号,每次值发生变化,都会修改它的版本号,CAS操作时都对比此版本号。(AtomicMarkableReference)

23. synchronized锁如何使用?

  1. 如果在普通方法上加上synchronized锁,则使用this锁;
  2. 如果在静态方法上加上synchronized锁,则使用当前类的class字节码作为锁对象;
  3. 可以自定义锁对象,在同步代码块中使用;

24. 能谈一谈synchronized锁的原理吗?

在使用synchronized关键字进行同步操作时,JVM底层会将一个对象与一个对象监视器(Monitor)进行关联,当且仅当一个Monitor拥有所有者(_owner不为null)后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取当前对象对应的Monitor的所有权。如果Monitor的重入次数(_recursions)为0时,线程可以进入Monitor,并将重入次数置为1,当前线程成为Monitor的所有者,如果还需要重入Monitor的话,会将重入次数加1,当其他线程尝试获取Monitor对象时,会进行阻塞等待,直到Monitor的重入次数置为0时,才能重新尝试获取Monitor的所有权。如果持有Monitor的线程执行monitorexit指令时,会将Monitor的重入次数减1,直到Monitor的重入次数减为0时,表示当前线程退出Monitor,不再是Monitor的所有者,其他线程可以尝试获取Monitor。
Monitor内部有两个非常重要的成员变量:_recursions:记录线程重入锁的次数;_owner:记录当前持有锁的线程ID。
在进入synchronized修饰的方法/代码块时,会执行一次monitorenter指令,但是释放锁的时候,会有两个monitorexit指令(分别插在方法结束处和异常处),可实际上只会执行一个monitorexit指令,要么正常结束,要么抛出异常,所以在抛出异常的时候,synchronized会自动释放锁。

_cxq:存放竞争锁的线程的单向链表,当锁不被任何线程持有时,才会用到;
_WaitSet:等待池,处于wait状态的线程,会被加入到_WaitSet;
_EntryList:锁池:处于等待锁阻塞状态的线程,会被加入到该列表;

锁的消除:当多个线程获取锁时,发现锁的对象就是每个线程私有的对象锁,编译器会做优化,消除synchronized锁;
锁的粗化:JVM检测到一连串的操作都对同一个对象加锁(while循环内执行100次append,没有锁粗化的就要进行100次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

25. 能谈一谈synchronized锁的升级过程吗?

偏向锁→轻量级锁(短暂自旋)→重量级锁
场景一:一直都是同一个线程在获取锁–偏向锁;
场景二:多个线程以间隔的形式获取锁–轻量级锁;
场景三:多个线程同时获取锁–重量级锁;

  1. 偏向锁:加锁和释放锁不需要额外的开销,只适合同一个线程访问同步代码块,如果多个线程竞争锁的时候,会撤销偏向锁;
  2. 轻量级锁:获取锁失败的线程不会阻塞,而是通过短暂自旋的方式进行重试,虽然提高了程序的响应速度,但是非常消耗CPU资源,适合于同步代码块执行速度非常快的情况下。
  3. 重量级锁:获取锁失败的线程会直接阻塞,效率低。

Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第9张图片

26. 使用synchronized锁时应该注意哪些问题?

  1. 减少同步代码块的范围,减少同步代码块执行的时间;
  2. 降低锁的粒度;
  3. 做读写分离,读的时候可以不用上锁;

27. 产生死锁的四个必要条件是什么?

(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

28. 谈谈Lock锁的实现原理?

Lock锁是基于AQS+LockSupport+CAS实现的,在获取锁的时候会把AQS类中state属性变为1,如果当前线程不断重入时,会进行state+1的操作,在释放锁的时候,会进行state-1的操作,直到state变为0时,表示该锁没有被任何线程获取到。另外,没有抢到锁的线程会存放在一个双向链表中,

Lock API:lock()—获取锁/unlock()—释放锁/tryLock()—非阻塞式获取锁;
LockSupport API:public static void park()—阻塞当前线程/public static void unpark(Thread thread)—唤醒当前线程。

29. 什么是AQS?

AQS(AbstractQueuedSynchronizer)是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
AQS底层是基于CAS compareAndSwapInt()方法实现的,底层采用了双向链表的数据结构,使用了模板方法设计模式。
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
也就是说:AQS是基于CLH队列,通过改变state状态符,表示获取锁的成功或者失败。
AQS 定义了两种资源共享方式:
Exclusive:独占,只有一个线程能执行,如ReentrantLock;
Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock、CyclicBarrier;
核心参数:
Node节点:采用双向链表的形式存放正在等待的线程;
state:锁的状态符;
exclusiveOwnerThread:记录持有锁的线程。

30.什么是Condition?

Condition是一个接口,其提供的就两个核心方法,await和signal方法。分别对应着Object的wait和notify方法。调用Object对象的这两个方法,需要在同步代码块里面,即必须先获取到锁才能执行这两个方法。同理,Condition调用这两个方法,也必须先获取到锁。注意:await()方法会释放锁。

31. 什么是Semaphore?

Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。一个线程获取许可证就调用acquire()方法,用完了释放资源就调用release()方法。可以简单理解为Semaphore信号量可以实现对接口限流,底层是基于AQS实现。

Semaphore工作原理:

  1. 可以设置Semaphore信号量的 状态state值为5
  2. 当一个线程获取到锁的情况下,将state-1,锁释放成功之后state+1;
  3. 当state<0,表示没锁的资源,则当前线程阻塞。

32. 什么是CountDownLatch?

CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。和join()方法非常类似,但是join()方法的底层是基于wait()方法实现,而CountDownLatch的底层是基于AQS实现的。
CountDownLatch countDownLatch = new CountDownLatch(2) AQS的state状态为2,调用countDownLatch.await()方法时线程变为阻塞状态且同时释放锁,调用countDownLatch.countDown();方法的时候状态-1 当AQS状态state为0的情况下,则唤醒正在等待的线程。

33. 什么是线程池?

线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。

34. 为什么要使用线程池?

因为频繁的开启线程或者停止,线程需要重新被CPU从就绪到运行状态调度,效率非常低。所以使用线程可以实现复用,从而提高效率。

35. 线程池有什么作用?

  1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

36. 线程池有哪些创建方式?

  1. Executors.newCachedThreadPool(); 可缓存线程池
  2. Executors.newFixedThreadPool();可定长度
  3. Executors.newScheduledThreadPool();可定时
  4. Executors.newSingleThreadExecutor(); 单例

底层都是基于ThreadPoolExecutor构造函数封装

37.线程池底层是如何实现复用的?

本质思想:创建一个线程,不会立马停止或者销毁,而是一直实现复用。

  1. 提前创建固定大小的线程一直保持在正在运行状态;(可能会非常消耗CPU资源);
  2. 当需要线程执行任务时,将该任务缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略;
  3. 正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题;

线程池核心点:复用机制

  1. 提前创建好固定的线程一直在运行状态(死循环实现);
  2. 提交的线程任务缓存到一个并发队列集合中,交给正在运行的线程执行;
  3. 正在运行的线程就从队列中获取该任务执行;

Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第10张图片

38. ThreadPoolExecutor核心参数有哪些?

corePoolSize:核心线程数量,一直正在保持运行的线程;
maximumPoolSize:最大线程数,线程池允许创建的最大线程数;
keepAliveTime:超出corePoolSize后创建的线程的存活时间;
unit:keepAliveTime的时间单位;
workQueue:任务队列,用于保存待执行的任务;
threadFactory:线程池内部创建线程所用的工厂;
handler:任务无法执行时的处理器;

39. 线程池创建的线程会一直在运行状态吗?

不会。
例如:配置核心线程数corePoolSize为2、最大线程数maximumPoolSize为5,我们可以通过配置超出corePoolSize核心线程数后创建的线程的存活时间为60s,在60s内线程一直没有任务执行,则会停止该线程。

40. 为什么阿里巴巴不建议使用Executors?

因为默认的Executors线程池底层是基于ThreadPoolExecutor构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务,从而容易发生内存溢出,会导致最大线程数失效。
在这里插入图片描述

41. 能谈谈线程池的底层原理吗?

核心原理:

  1. 提交任务的时候比较核心线程数,如果当前任务数量小于核心线程数的情况下,则直接复用线程执行;
  2. 如果任务量大于核心线程数,则缓存到队列中;
  3. 如果缓存队列满了,且任务数小于最大线程数的情况下,则创建线程执行;
  4. 如果队列且最大线程数都满的情况下,则走拒绝策略;
    注意:最大线程数,在一定时间没有执行任务 则销毁避免浪费CPU内存;

专业术语:

  1. 当线程数小于核心线程数时,创建线程;
  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列;
  3. 当线程数大于等于核心线程数,且任务队列已满;
    3.1. 若线程数小于最大线程数,创建线程;
    3.2. 若线程数等于最大线程数,抛出异常,拒绝任务;

42. 线程池队列满了,任务会丢失吗?

如果队列满了且任务总数 > 最大线程数,则当前线程走拒绝策略。
可以自定义拒绝异常,将该任务缓存到 redis、本地文件、mysql 中后期项目启动实现补偿。

  1. AbortPolicy 丢弃任务,抛运行时异常;
  2. CallerRunsPolicy 执行任务;
  3. DiscardPolicy 忽视,什么都不会发生;
  4. DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务;
  5. 实现RejectedExecutionHandler接口,可自定义处理器;

43. 线程池如何合理配置参数?

自定义线程池需要我们自己配置最大线程数maximumPoolSize,为了高效的并发运行,当然这个不能随便设置。这时需要看我们的业务是 IO 密集型还是 CPU 密集型。
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。CPU 密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核 CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:以保证每个 CPU 高效的运行一个线程。 一般公式:(CPU 核数+1)个 线程的线程池

IO密集型
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行I0密集型的任务会导 致浪费大量的 CPU 运算能力浪费在等待。 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核 CPU 上,这种加 速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻寒,故需要多配置线程数: 公式:CPU 核数 * 2 CPU 核数 / (1 - 阻塞系数) 阻塞系数 在 0.8~0.9 之间
查看 CPU 核数: System.out.println(Runtime.getRuntime().availableProcessors());

44. 什么是FutureTask?

FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。基于LockSupport实现。

45. 什么是ForkJoin?

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

  1. RecursiveAction:用于没有返回结果的任务;
  2. RecursiveTask:用于有返回结果的任务;

46. 谈谈你对ThreadLocal的理解?

ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保 存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。Threadlocal适用于在多线程的情况下,可以实现传递数据,实现线程隔离。ThreadLocal是每个线程的局部变量。

47. 哪些地方使用到了ThreadLocal?

  1. Spring事务模板类;
  2. 获取HttpRequest;
  3. Aop调用链;

48. ThreadLocal与synchronized的区别?

synchronized与ThreadLocal都可以实现多线程访问,保证线程安全的问题。

  1. synchronized采用当多个线程竞争到同一个资源的时候,最终只能够有一个线程访问,采用时间换空间的方式,保证线程安全问题;
  2. ThreadLocal在每个线程中都自己独立的局部变量, 空间换时间,相互之间都是隔离。

相比来说ThreadLocal效率比synchronized效率更高。

49. 谈谈ThreadLocal的底层实现原理?

  1. 每个线程中都有自己独立的ThreadLocalMap对象,ThreadLocalMap底层基于Entry实现;
  2. 如果当前线程对应的ThreadLocalMap对象为空的情况下,则创建该 ThreadLocalMap对象,并且赋值键值对。Key为当前new ThreadLocal()对象,value就是为 object 变量值。
    Mr. Cappuccino的第20杯咖啡——金三银四面试题之并发编程篇_第11张图片

50. ThreadLocal为什么会引发内存泄漏问题?

因为每个线程中都有自己独立的ThreadLocalMap对象,key为ThreadLocal,value为变量值。使用ThreadLocal作为Entry对象的key,是弱引用,当ThreadLocal的指向为null时,Entry对象中的key变为null,该对象一直无法被垃圾收集器回收,一直占用到了系统内存,有可能会发生内存泄漏的问题。

51. 如何防止ThreadLocal发生内存泄漏问题?

  1. 可以调用remove()方法将不要的数据移除,避免内存泄漏的问题;
  2. ThreadLocal每次在做set()操作的时候会清除之前key为null的数据;

你可能感兴趣的:(金三银四,mr,缓存,java,并发编程)