并发编程的问题范围

synchronized的实现原理
CAS无锁化原理
AQS是什么
Lock锁
ConcurrentHashMap的分段加锁的原理
线程池的原理
Java内存模型
volatile关键词
Java并发包的了解

synchronized关键词的底层原理

简单来说

关键词

monitor,计数器,可重入锁,只有一个线程可以获取到锁

monitor

synchronized关键词在底层编译的jvm指令中,会有monitorenter和monitorexit两个指令。
每个对象都有一个关联的monitor,一个对象实例就有一个monitor,一个类的Class对象也有一个monitor,如果对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁。
monitor里面有个计数器,从0开始。如果一个线程要获取monitor的锁,就看monitor的计数器是否为0,如果是0,那么就说明没有人获取锁,该线程就能获取到锁,然后计数器加1。

可重入锁

monitor锁是支持重入加锁的。如果一个线程第一次synchronized获取到了myObject对象的monitor的锁,计数器加1,然后第二次synchronized再获取myObject对象的monitor的锁,计数器再加1,变成了2。这个就是重入加锁。
这个时候,其他线程再第一次synchronized那里,会发现myObject对象的monitor锁的计数器是大于0的,意味着该对象被其他线程加锁了,然后此时线程就会进入block阻塞状态,什么都干不了,就是等待锁。
接着如果离开了synchronized修饰的代码片段范围,就会有一个monitorexit的指令,在底层,此时获取锁的线程就会对那个对象的monitor的计数器减1,如果有多次重入加锁就会对应多次的减1,知道最后,计数器是0,锁才释放完毕。阻塞的线程才会再次尝试获取锁,但是只有一个线程可以获取到锁。

  1. // 线程1
  2. synchronized(myObject) { -> 类的class对象来走的
  3. // 一大堆的代码
  4. synchronized(myObject) {
  5. // 一大堆的代码
  6. }
  7. }

详细来说

#

CAS的理解以及其底层实现原理

简单来说

CAS: compare and set 先比较再设置
CAS在底层的硬件级别保证了一定是原子性,同一时间只有一个线程可以执行CAS,先比较再设置,其他的线程的CAS同时间去执行此时会失败。

  1. public class MyObject {
  2. int i = 0;
  3. // 针对MyObject对象,同一时间,只有一个线程可以进入这个方法
  4. public synchronized void increment() {
  5. i++;
  6. }
  7. }
  8. // 多个线程同时基于myObject这一个对象,来执行increment()方法。
  9. MyObject myObject = new Object();
  10. myObject.increment();

此时,synchronized是针对执行这个方法的myObject对象进行加锁。只有一个线程可以成功的对myObject加锁,将与对象关联的monitor的计数器加1,加锁。一旦多个线程并发的去进行synchronized加锁,就导致串行化,效率并不是很高,因为很多线程,都需要排队去执行increment方法。

  1. public class MyObject {
  2. // 底层基于CAS来进行实现
  3. AtomicInteger i = new AtomicInteger(0);
  4. // 多个线程此时来执行这段代码
  5. // 不需要synchronized加锁,也是线程安全的。
  6. public void increment() {
  7. i.incrementAndGet();
  8. }
  9. }

image.png
image.png

ConcurrentHashMap实现线程安全的底层原理

JDK1.8以前,多个数组,分段加锁,一个数组一个锁
JDK1.8以后,优化细粒度,一个数组,每个元素进行CAS,如果失败说明对应元素已有值,此时synchronized对数组元素加锁,链表+红黑树处理,对数组的每个元素加锁。
多个线程要访问同一个数据,synchroinzed加锁,CAS去进行安全的累加,去实现多线程场景下的安全更新一个数据的效果。
一个数组,数组里每个元素进行put操作,都是有不同的锁,刚开始进行put操作的时候,如果两个线程都在数组[5]这个位置进行put操作,则会采取CAS策略。
同一时间,只有一个线程能够执行CAS,先从内存中获取数组[5]位置的值(null),然后CAS,比较旧值是否被更改,没有改变,则put进行修改后的新值,同时间,其他线程的CAS操作,都会因为值更新导致设置新值失败。
分段加锁,通过对数组每个元素执行CAS策略,如果是很多线程对数组里的不同元素执行put,则各个线程之前并无关系,互不影响。如果多个线程对数组的同一个元素进行put操作,则同一时间只有一个线程执行成功,其他线程执行失败,对synchroized(数组[5])进行加锁,基于链表或者红黑树在这个数组位置上插入自己的数据。
如果是对数组同一个位置的元素操作,才会加锁进行串行化处理。如果是对数组的不同位置的元素操作,就不会有锁竞争的问题,此时线程是并发执行的。

AQS的实现原理

线程池的工作原理

系统是不可能无限制的创建很多线程的,会创建一个线程池,有一定数量的线程,让他们执行各种各样的任务,线程执行完任务之后,不会销毁自己,继续等待执行下一个任务。
corePoolSize 核心线程数量
提交任务,先看一下线程池里的线程数量是否小于corePoolSize,如果小于,直接创建一个线程执行任务。执行完任务之后,这个线程是不会死掉的,他会尝试从队列中获取新的任务,如果没有新的任务,此时线程就会阻塞,等待新的任务到来。
持续提交任务,上述流程反复执行,只要线程池的线程数量小于corePoolSize,都会直接创建新线程来执行这个任务,执行完就会尝试从队列中获取任务,知道线程池里有corePoolSize个线程。
接着再次提交任务,会发现线程数量已经跟corePoolSize一样大了,此时就直接把任务放入队列当中,线程会争抢获取队列中的任务执行,如果所有的线程此时都在执行任务,那么队列里的任务就会越来越多。
Executors.newFixedThreadPool(3) —> 队列为LinkedBlockingQueue 无界阻塞队列。

线程池的核心配置参数是什么

  1. new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAlieveTime, TimeUnit, Queue);
  2. - corePoolSize: 核心线程数
  3. - maxPoolSize 最大线程数
  4. - keepAliveTime 额外线程数的存活时间
  5. - TimeUnit 时间单位
  6. - Queue 任务队列

如果Queue队列是有界队列,如果corePoolSize个线程都在繁忙工作,大量工作进入有界队列,导致队列满了,则此时,如果maxPoolSize比corePoolSize大,此时会继续创建额外的线程放入线程池,来处理这些任务,然后超过corePoolSize数量的线程在处理完一个任务后,同样也会尝试从队列里获取任务来执行。如果此时额外的线程都创建完了去处理任务,队列还是满了,此时新进来的任务就会被拒绝策略reject掉。
拒绝策略: (1) AbortPolicy (2) DiscardPolicy (3)DiscardOldestPolicy (4)CallerRunsPolicy (5)自定义
如果后续慢慢的队列里没任务了,线程空闲了,超过corePoolSize的线程就会在keepAliveTime之后自动释放掉。

如果在线程中使用无界阻塞队列会发生什么

面试题: 在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存异常飙升?
回答:调用超时,队列会变得越来越大,此时会导致内存飙升,而且可能会导致OOM,内存溢出。

如果线程池队列满了,会发生什么事情

使用有界队列,可以避免内存溢出。
自定义一个reject拒绝策略,如果线程池无法执行更多的任务,此时可以自定义一个拒绝策略,将被拒绝的任务信息持久化写入磁盘,后台专门启动一个线程,后续等待你的线程池的工作负载低了,它可以慢慢的从磁盘里读取之前持久化的任务,重新提交到线程池里去执行。
如果无限制的不停的创建额外的线程,每个线程都有自己的栈内存,占用一定的内存资源,如果创建了太多的线程,就会导致内存资源耗尽,系统会崩溃掉。即使内存没有崩溃,也会导致机器的cpu负载变高。

如果线上机器突然宕机,线程池的阻塞队列的请求怎么办

必然会导致线程池里积压在队列中的任务丢失。
解决办法:如果要提交一个任务到线程里去,在提交之前,现在数据库里插入这个任务信息,更新它的状态:未提交,已提交,已完成。提交成功之后,更新它的状态为已提交。系统重启之后,后台线程去扫描数据库里的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池里去,继续进行执行。

谈谈对Java内存模型的理解

read->load->use->assign->store->write
每一个线程都有自己的工作内存(实际上是CPU级别的缓存)。对数据data进行更新时,每个线程都会从主内存里面读取数据,然后read读缓冲区,load线程自己的工作内存,然后use使用数据data,更细完data后,会assign至工作内存,然后store写缓存区,最后写入主内存。

Java内存模型的原子性,有序性,可见性

原子性:一个操作或多个操作,在一个线程里时被完整执行,不可被中断的。
有序性:编译器和指令器,有的时候为了提高代码的执行效率。会将指令重排序。
可见性:两个线程访问共享变量,如果一个线程更改了变量,另外一个线程就能够获取到最新的值。

聊聊volatile关键词的原理

连环提问:内存模型 —> 原子性,有序性,可见性 -> volatile关键词

volatile关键词可以用来解决可见性和有序性,在罕见条件下,可以有限的保证原子性。所以volatile关键词不是用来保证原子性的。

你知道指令重排以及happens-before原则是什么吗

java中有一个happens-before原则
编译器、指令器可能对代码重排序,要遵守happens-before原则,只要符合happens-before原则的,就不能胡乱重排,如果不符合这些排序的话,就可以自己排序。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定操作规则:一个unlock操作先行发生于后面对同一个锁的lock锁,比如:lock.lock(),lock.unlock(),lock.lock()。
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
  • 线程启动规则:Thread对象的start()方法先行发生于线程的每一个动作,thread.start(),thread.interrupt()。
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

volatile底层是如何基于内存屏障保证可见性和有序性的

面试连环炮:内存模型->原子性,可见性,有序性->volatile+可见性 -> volatile+有序性(指令重排+happens-before) -> volatile+原子性 -> volatile底层原理(内存屏障级别的原理)

volatile+原子性: 不能够保证原子性。保证原子性可以通过 synchronized,lock加锁。

(1)lock指令: volatile保证可见性
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU会进行嗅探,自己本地缓存中的数据是否被别人修改,如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在对读取变量的时候,就会从主内存重新加载最新的数据。

lock前缀指令 + MESI缓存一致性协议

(2)内存屏障:volatile禁止指令重排序

  • LoadLoad屏障
  • SotreStore屏障
  • LoadStore屏障
  • StoreLoad屏障 | 屏障类型 | 指令示例 | 说明 | | —- | —- | —- | | LoadLoadBarriers | Load1; LoadLoad; Load2 | 确保Load1数据的装载,提前于Load2及Load2后的所有后续装载指令的装载。 | | StoreStoreBarriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存),提前于Store2及Store2后的所有后续存储指令的存储。 | | LoadStoreBarriers | Load1;LoadStore;Store2 | 确保Load1数据装载,提前于Store2及Store2后的所有后续的存储指令刷新到内存。 | | StoreLoadBarriers | Store1;StoreLoad;Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存),提前于Load2及Load2后的所有后续装载指令的装载。StoreLoadBarriers会使该屏障之前的所有内存访问指令(装载和存储指令)完成之后,才执行该屏障之后的内存访问指令。 |

volatile的作用是什么

对volatile修改变量的读写操作,都会加入内存屏障
每个volatile写操作, 前面加入 StoreStore屏障,禁止前面的普通写和他重排。
每个volatile写操作, 后面加入 StoreLoad屏障,禁止跟下面的volatile读/写重排。
每个volatile读操作,后面加入 LoadLoad屏障,禁止下面的普通读和volatile读重排。
每个volatile读操作,后面加入 LoadStore屏障,禁止下面的普通写和volatile读重排序。