Java并发之问

1、什么是Volatile?
Volatile是Java当中的一个关键字。
2、他有哪些作用?
保证内存可见性
防止指令重排序
3、Volatile会在处理器上声明什么信号?
在处理器上声明lock#指令。
4、lock#信号有什么用?
将当前处理器缓存行的数据写回到系统内存。
这个写回内存的操作使得其他CPU里缓存了该内存地址的缓存行无效。
5、lock#信号是如何保证处理器独占共享内存的?
锁总线(开销太大)
锁缓存(阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效)
6、如何实现一个处理器的缓存回写到内存导致其他处理器的缓存无效?
使用MESI协议维护内部缓存和其他处理器缓存的一致性。
MESI缓存协议使用嗅探技术,如果通过嗅探一个处理起来检测其他处理器打算写内存地址,而这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,下次访问相同地址是,强制执行缓存行填充。
7、为什么追加64字节能够提高并发编程的效率?
例如JDK7中的LinkedTransferQueue,对于一些处理器如酷睿I7,他们的L1、L2或L3缓存的高速缓存行是64个字节宽,由于队列的头节点和尾节点不足64字节,例如头节点32字节,尾节点32字节,那么他会把头节点和尾节点一同读入一个缓存行,造成的结果可能是:处理器A修改头节点,锁定缓存行,处理器就无法访问尾节点了。所以可以提高效率
8、什么情况不需要追加到64字节?
共享变量不会被频繁写
缓存行非64字节宽的处理器
9、什么是伪共享问题?
在处理器A上运行的线程想更新变量X,同时处理器B上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果处理器A获得了所有权,缓存子系统将会使处理器B中对应的缓存行失效。当处理器V获得了所有权然后执行更新操作,处理器A就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。
10、如何解决为共享问题?
填充字节
Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。
11、synchronized锁的是什么?
对于普通同步方法,锁的是当前实例对象
对于静态同步方法,锁的是当前类的Class对象
对于同步方法块,锁的是Synchonized括号里配置的对象
12、锁存在哪里?
锁在对象头里面的。
如果对象是数组类型,那么对象头大小为3字宽;如果对象是非数组类型,那么对象头大小为2字宽(32位JVM中,1字宽为4字节;64位为8字节)
13、Mark Word存储的是什么?分别占多少字节
默认存储对象的HashCode、分代年龄和锁标记为。
32位JVM下,25bit用于对象的hashcode,4bit用于对象分代年龄、1bit用于是否是偏向锁、2bit用于锁标志位。
64位JVM下,25bit为unused、31bit为hashcode,1bit为cms_free,4bit为分代年龄,1bit用于是否是偏向锁、2bit用于锁标志位。
14、Mark Word存储的内容会发生变化吗?怎么变?
会。
32位32位MarkWord
64位
https://img-blog.csdnimg.cn/20200802171848152.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDk4NTM0,size_16,color_FFFFFF,t_7015、锁的状态?
无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
16、什么是锁升级?
锁从低级别变为高级别
17、锁可以降级吗?
不行。
18、为什么会有偏向锁?
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程获得,为了让线程获得锁的代价更低而引入了偏向锁。
19、偏向锁会在对象头里面存放什么?
偏向线程ID
20、偏向锁加锁流程?

  1. 首先获取锁对象头中的MarkWord,判断MarkWord中的锁标志位是否为01
  2. 如果是01,同时判断偏向锁位是0或者是1
  3. 如果是0,说明当前是无锁状态,通过CAS原子操作,把当前线程的ID写入到MarkWord中,如果CAS成功,表示获得偏向锁成功,会将偏向锁锁位设置为1,如果CAS失败,说明当前存在竞争,撤销偏向锁,并升级为轻量级锁。
  4. 如果Markword中偏向锁位为1,说明已经有线程获得了偏向锁,判断线程ID是否是自己,如果在虚拟机栈中放置一个空的LockRecord,表示重入,否则需要撤销偏向锁并升级到轻量级锁。
    空的lock record

21、偏向锁的撤销?
偏向锁的撤销,需要等待全局安全点(这个时间点上没有正在执行的字节码)。首先,它会暂停拥有偏向锁的线程,然后检测持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置为无锁状态;如果线程仍然活着,那么会将偏向锁升级为轻量级锁,如果不活着那么会把对象头设置为无锁状态。在这里插入图片描述
22、轻量级锁加锁流程?
线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,这个过程叫Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁(这里已经是重量级锁了)。如果出现两个以上线程竞争同一个锁的情况,那轻量级锁就不再有效,必须膨胀为重量级锁。
操作之前
操作之后
23、在displaced Mark Word这个过程中,如果CAS失败,为什么虚拟机还要检查对象的Mark Word是否指向当前线程的栈帧?
如果这个线程对这个锁进行了重入,由于Lock Record不是同一个,所以JVM并不能知道当前占据锁的线程是谁,所以还必须要检查一下Mark Word的指针是否是指向当前这个线程。
24、轻量级解锁是个什么流程?
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
25、问题22、24讲到了两种场合升级为重量级锁,那么这个锁升级流程到底是在哪里发生的?
问题22,24都是准确的,(以下函数为CPP)
1,刚进入ObjectSynchronizer::slow_enter来cas争夺轻锁
2,不成功进入ObjectSynchronizer::inflate(THREAD, obj())进行膨胀,看代码只会膨胀一次就被记录下来,不会多次膨胀。这就是问题22的膨胀,实际建了很多对象来记录膨胀信息。
3,膨胀之后进入enter(THREAD)
4,进入之后会继续尝试设置重锁的owner为自己,如果失败,进入5.
5,进入到问题24说的自旋,如代码。

if (Knob_SpinEarly && TrySpin_VaryDuration(Self) > 0) {
     Self->_Stalled = 0 ;
     return ;
}

这个自旋是一个优化操作,为了是如果线程执行都很快,那么在自旋期间内没准其他线程就离开了临界区,就不必须把自己丢入cxq队列并self_park住,如果自旋期间其他线程没有离开临界区,那么继续下一步,自旋次数在jvm有固定数值为

static int Knob_PreSpin            = 10 ;      // 20-100 likely better
int ObjectMonitor::Knob_SpinLimit  = 5000 ;    // derived by an external tool

即便如此,这个自旋也是在膨胀之后成了重锁的优化动作了,不再是轻锁了。
6,把自己丢入cxq队列并self_park,在这期间还进行过几次trylock,也是为了优化。(详细代码参考HotSpot虚拟机源代码vm目录下的synchronizer.cpp)

根据以上我们可以知道问题24失败表示的是,这个锁已经被升级为了重量级锁,所以会失败。
26、线程尝试获取重量级锁失败后应该被阻塞,但是为什么问题25第5步时并没有发生阻塞而是进入了自旋?
(以下答案基于JDK1.8)
在膨胀inflate的时候会把轻锁的持有者otherThread放在锁的_owner字段上,然后进入enter之后会cas这个字段为thisThread必然不成功,然后检查是否重锁重入,然后才进入5000次自旋阶段,这个自旋是膨胀之后,真正进入重锁之后的过程中的一个优化。
进入自旋代码后,会看到里面有很多for循环,里面的limit可能在运行过程中被改变,这样自旋次数就会变化,不知道这是不是书中的自适应。
在《Java并发编程的艺术》中所介绍的阻塞应该是一种没有被应用或者已经被抛弃了的实现方式。
27、总结一下具体流程
当只有一个锁在进行锁资源的争夺时,也就是这个锁被一个线程使用,此时还有一个线程在进行争夺,那么此时还是轻量级锁状态,争夺锁的线程进行自旋,当自旋到一定次数,或者有另外一线程来争夺锁时,会升级为重量级锁。之后,重量级锁也会进行自旋,因为争夺造成的锁升级可能出现,同步代码块很小,线程执行非常快,所以进行一定自旋,然后自旋到一定程度后才进行阻塞。
28、处理器如何实现原子性?
总线锁
缓存锁
29、哪些情况下处理器不使用缓存锁定?
当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁。
有些处理器本身不支持缓存锁定。
30、什么是ABA问题?
如果原来的一个值是A,变成了B,又变成了A,那么使用CAS进行检查时就会发现他的值没有变化,其实却变化了。
31、如何解决ABA问题?
使用版本号。在变量面前追加版本号,每次变量更新时版本号加1。
32、CAS除了ABA问题还有什么问题?
循环长开销大,只能保证一个共享变量的原子操作。
33、pause指令有什么用?
延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间为0;避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提高CPU执行效率。
34、线程之间的通信机制有哪两种?
共享内存和消息传递。
35、什么是消息传递?
消息传递是用消息队列来进行传递消息。
消息队列是操作系统维护的以字节序列为基本单位的间接通信机制。
36、什么是共享内存?
共享内存是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制。
对于线程来说,同一进程的线程总是共享相同的内存地址空间。
35、共享内存和消息传递有什么区别?
在这里插入图片描述
队列通信中的消息有明显的生命周期,消息有传递的过程,有通知的过程,消息有失效性,有先后关系。
而共享内存没有上面这些特点。共享内存在使用时要解决互斥的问题。
36、除了共享内存和消息队列进程还有哪些传递信息的方式?
管道、信号。
37、什么是JMM?JMM有什么用?
JMM是Java内存模型。JMM决定一个线程对共享变量的写入何时对另一个线程可见。
38、线程和主内存之间有何抽象关系?
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程以读/写共享变量的副本。
39、什么是本地内存?
本地内存是JMM的一个抽象概念,并不真实存在。涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
40、什么是重排序?有哪几种重排序?
为了提高性能,编译器和处理器常常会对指令做重排序。
重排序分3种,编译器优化的重排序、指令级并行的重排序、内存系统的重排序。
41、JMM是如何避免由于重排序而导致的内存可见性问题?
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
42、有哪几种内存屏障?说明一下?
内存屏障
StoredLoad Barriers是最厉害的,具有其他三个屏障的效果。
43、什么是happens-before?
JSR-133使用happens-before的概念来阐述操作之间的内存可见性
44、happens-before有哪些规则?
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
Volatile变量规则:对一个Volatile域的写,happens-before于任意后续对这个volatile域的读。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
线程终止规则:线程中的所有操作都先行发生于于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
如果一个对象x synchronized-with另一个对象y,有hb(x,y)
对象终结规则:一个对象的初始化完成,先行于他的finalize()方法的开始。
一个对象的初始化完成,先于这个对象的其他操作。
传递性:如果A happens-beforeB,且B happens-before C,那么A happens-before C。
注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作的结果对后一个操作可见。
补充:什么是Synchronization Order?

  • 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步
  • 对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步
  • 启动线程的操作与线程中的第一个操作同步。
  • 对于每个属性写入默认值(0, false,null)与每个线程对其进行的操作同步。尽管在创建对象完成之前对对象属性写入默认值有点奇怪,但从概念上来说,每个对象都是在程序启动时用默认值初始化来创建的。
  • 线程 T1 的最后操作与线程 T2 发现线程 T1 已经结束同步。线程 T2 可以通过 T1.isAlive() 或 T1.join() 方法来判断 T1 是否已经终结。
  • 如果线程 T1 中断了 T2,那么线程 T1 的中断操作与其他所有线程发现 T2 被中断了同步(通过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted )

45、什么是as-if-serial
不管怎么重排序,单线程程序的执行结果不能被改变。
46、什么是顺序一致性
一个线程中的所有操作必须按照程序的顺序来执行。
(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
47、JMM的基本方针为
在不改变程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。例如JMM中,临界区内的代码可以重排序。
48、对于未同步或未正确同步的多线程程序,JMM只提供最小安全性是什么意思?
线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值。
49、为什么JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致?
因为如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化。
50、未同步程序在两个模型中的执行特性有哪些差异?
顺序一致性模型保证单线程内的操作会按照程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行。
顺序一致性模型保证所有线程都只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
JMM不保证对64位的long型和double型变量的写操作具有原子性,但顺序一致性模型保证对所有的内存/读写操作都具有原子性。
51、Volatile具有哪些特性
可见性。
原子性(对单个volatile变量的读/写具有,但是类似于volatile++就不具有)
52、Volatile写-读的内存语义
写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
读:当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效。线程接下来从主内存读取共享变量。
53、Volatile内存语义的实现?
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
当第一个操作时写,第二个操作是读,不能重排序。
由此可以得到:
在每个volatile写操作前插入StoreStore屏障(保证前面的普通写不会和volatile写重排序)。
在每个volatile写操作后面插入StoreLoad屏障(保证上面的volatile写不会与下面的volatile读/写重排序)。
在每个volatile读操作后面插入LoadLoad屏障(保证下面的普通读操作和上面的volatile读重排序)。
在每个volatile读操作后面插入LoadStore屏障(保证下面的普通写操作不会和上面的volatile读重排序)。
54、JSR-133为什么要增强volatile的内存语义?
为了提供一种比锁更轻量的线程之间的通信的机制。
补充、有哪八种内存交互操作?

lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
JMM对8种内存交互操作制定的规则:
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
不允许线程将没有assign的数据从工作内存同步到主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

55、锁的释放和获取的内存语义?
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
总结:
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
线程B获取一个锁,实质上是线程B接受了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
56、使用公平锁时,加锁方法lock()的调用轨迹?
ReentrantLock.lock()
FairSync.lock()(现目前的JDK这个方法已被改为tryAcquire)
AbstractQueuedSynchronizer.acquire(int arg)
ReentrantLock.tryAcquire(int acquires)
57、使用公平锁时,解锁方法unlock()的调用轨迹?
ReentrantLock.unlock()
AbstractQueuedSynchronizer.release(int arg)
Sync.tryRelease(int releases)
58、CAS是如何具有Volatile的读写语义的?
如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀, 反之,如果是在单处理器上运行,就省略lock前缀。
lock前缀说明:
确保对内存的读-改-写操作原子执行。
禁止该指令,与之前和之后的读和写指令重排序。
把写缓冲区的所有数据刷新到内存中
59、非公平锁获取流程?
首先调用ReetrantLock的lock方法,其中,会调用Sync的lock方法,这是一个抽象方法,他会调用NonfairSync的lock方法,首先会使用CAS修改volatile变量尝试获取锁,如果成功设置为当前线程独占,否则,调用AQS的acquire方法,接着调用自己的nonfairTryAcquire方法,首先判断是否有线程获取到了锁,如果有,判断自己是否为获取到锁的那个线程,如果没有,使用CAS尝试获取锁,返回;如果自己不是获取到锁的那个线程,同时又有线程获取到了锁,把线程放入FIFO队列尾部,并且自旋进行获取锁,直到被中断或者获取到锁
调用的方法

60、说一下lock前缀?
确保对内存的读-改-写操作原子执行。
禁止该指令与之前和之后的读和写指令重排序。
把写缓冲区中的所有数据刷新到内存中。
61、非公平锁的获取和公平锁的获取有什么不同?
公平锁的获取,在获取锁的时候会判断自己有无前节点。
非公平锁的获取不会判断。
62、实现锁-获取的内存语义实现有哪些?
利用volatile变量的写-读所具有的内存语义。
利用CAS所附带的volatile读和volatile写的内存语义。
63、concurrent包的源代码的通用化的实现模式?
首先声明共享变量volatile,然后使用CAS的原子条件更新来实现线程之间的同步,同时配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类,这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。
64、对于final,编译器和处理器需要遵守的规则?
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
65、写final域的重排序规则?
JMM禁止编译器把final域的写重排序到构造函数之外。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
66、写final域的重排序规则有什么用?
在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保证。
67、读final域的重排序规则?
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(仅仅针对处理器)。
68、如果final域是一个引用类型的话,有什么不同?
对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个构造对象的引用赋值给一个引用变量,这两个操作不能重排序。
69、为什么final域不能从构造函数溢出?
在构造函数返回前,如果final域从构造函数溢出,会导致final域未被初始化。
70、在X86处理器中,如何实现final域的?
不加任何屏障。因为X86不会对写-写做重排序,也不会对存在间接依赖的操作做重排序。
71、为什么final语义要被增强?
比如,前后读同一个String,但是没有对String做任何操作,String却发生了改变。这就是一个BUG。
72、JMM基本原则?
不改变程序的执行结果,怎么重排序都行。
73、happens-before的定义?
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序在第二个操作之前。
两个操作之间存在happens-before关系,不意味着Java平台具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序后的结果,与按happens-before关系来执行的结果一致,那这种重排序并不非法。
74、as-if-serial和happens-before的区别?
as-if-serial保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial创造了一个幻境:单线程程序是按照程序的顺序执行的;happens-before关系给编写正确同步的多线程程序创造了一个环境:正确同步的多线程程序是按照happens-before指定的顺序来执行的。
75、以下代码有什么问题?

public static Singleton getInstance()
	{
	  if (instance == null)
	  {
	    synchronized(Singleton.class) {  //1
	      if (instance == null)          //2
	        instance = new Singleton();  //3
	    }
	  }
	  return instance;
	}
依旧可能造成instance对象没初始化。

76、为什么会这样?
在第三步中,这一行代码其实是三步,分配内存,初始化对象,设置对象内存地址。但是可能造成初始化对象和设置对象内存地址重排序,所以会出问题。
77、如何解决?
使用volatile或者利用类初始化锁。
78、两种分发的原理?
volatile禁止重排序,类初始化锁使得线程无法看到重排序。

public class Singleton静态内部类 {
    private Singleton静态内部类() {
    }

    private static class Singleton静态内部类Holder{
        private final static Singleton静态内部类 INSTANCE = new Singleton静态内部类();
    }

    public static Singleton静态内部类 getInstance(){
        return Singleton静态内部类Holder.INSTANCE;
    }
}

79、类初始化锁原理?

AB
获取类初始化锁,设置state = initializing
获取锁发现A获取了锁,释放锁。进入阻塞
释放锁
1:分配内存空间。3:赋值给引用变量。2:初始化对象。(虽然重排序了但是线程B无法看到)获取锁,发现state = initializing,释放锁,阻塞
设置完毕,设置state = initialized
获取锁,发现state - initialized,释放锁

80、什么是线程?
现代操作系统调度的最小单元是线程,也叫轻量级进程。
81、为什么需要线程来实现并行?进程不可以吗?
使用进程实现并行执行的问题有两个。第一,创建进程占用资源太多;第二,进程之间的通信需要数据在不同的内存空间传来传去,无法共享。
对于线程来说,一个进程内部的资源天生就是共享的不存在资源共享问题,第二相对于进程的创建,线程的创建所需要耗费的资源要少很多。
82、进程和线程是什么关系?
线程 = 进程 - 共享资源
83、比较一下线程和进程?
进程是资源分配单位,线程是CPU调度单位。
进程拥有一个完整的资源平台,而线程只独享指令流执行的必要资源,如寄存器和栈。
线程具有就绪、等待和运行三种基本状态和状态间的转换关系。
线程能减少并发执行的时间和空间开销。
84、为什么要使用多线程?
更多的处理器核心,更快的响应时间,更好的编程模型。
85、什么是线程优先级?
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些出去里资源的线程属性。
86、程序正确与否是否可以依赖线程优先级?
不可以,因为操作系统可以完全不用理会Java线程对于优先级的设定。
87、Java线程的生命周期?
共有六种状态,分别是new,runnable,blocked,wating,time_waiting,terminated
Java线程的生命周期?
88、操作系统线程的生命周期?
操作系统线程的生命周期
89、什么是Daemon线程?
一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
90、在构建Daemon线程时,可以否依靠finally块中的内容来确保执行关闭或清理资源?
不可以,main线程在启动了Daemone线程后随着main方法执行完毕而终止,而此时JVM中已经没有非Daemon线程,虚拟机需要退出,JVM中所有的Daemon线程都需要立即停止,所以不会执行finally块中的内容?
91、线程是如何构造的?
一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent线程是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。
92、Java中有哪些方法可以创建线程?
继承Thread类,实现Runnable接口,实现callable接口。
93、什么是中断?
可以理解为线程的一个标识为属性。
94、中断的原理?
每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
95、中断的处理?
显然,作为一种协作机制,不会强求被中断线程一定要在某个点进行处理。实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理,这时候在任务处理层面,就跟没有调用中断方法一样。“合适的时候”与线程正在处理的业务逻辑紧密相关,例如,每次迭代的时候,进入一个可能阻塞且无法中断的方法之前等,但多半不会出现在某个临界区更新另一个对象状态的时候,因为这可能会导致对象处于不一致状态。
处理时机决定着程序的效率与中断响应的灵敏性。频繁的检查中断状态可能会使程序执行效率下降,相反,检查的较少可能使中断请求得不到及时响应。如果发出中断请求之后,被中断的线程继续执行一段时间不会给系统带来灾难,那么就可以将中断处理放到方便检查中断,同时又能从一定程度上保证响应灵敏度的地方。当程序的性能指标比较关键时,可能需要建立一个测试模型来分析最佳的中断检测点,以平衡性能和响应灵敏性。
96、** 中断的响应?**
有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用interrupt会发生些什么都是事先知道的,如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的interrupt后该线程会做出什么样的响应,那就不应当中断该线程。
97、interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态(清不清楚由JVM判定)清除而后者不会。
98、** 怎么检测一个线程是否拥有锁?**
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
99、为什么suspend(),resume()和stop()方法不被建议使用?
以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此可能导致程序可能工作在不确定状态下。正是因为这些副作用,这些方法才被标注为不建议使用。
100、如何安全地终止线程?
通过中断操作或者cancel()方法均可使得CountThread得以终止。
101、对于同步块用了哪两个指令完成的同步?同步方法呢?
同步块时monitorenter和monitorexit指令。
同步方法是依靠修饰符上的ACC_SYNCHRONIZED来完成的。
102、为什么需要有等待通知机制?
可以确保及时性
可以降低开销
103、使用wait(),notify(),notifyAll()方法有什么注意?
使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
调用 wait()方法后,线程由RUNNING编程WAITING,并将当前线程放置到对象的等待队列。
notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或者notifyAll()的线程释放锁之后,等待线程才有机会返回。
notify()方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移动到同步队列,被移动的线程状态由WAITING变为BLOCKED。
从wait()方法返回的前提是获得了调用对象的锁。
104、等待/通知的经典范式?
等待方遵循:
获取对象锁
如果条件不满足,调用对象的wait()方法,被通知后仍要检查条件。
条件满足则执行对应的逻辑。
通知方遵循:
获取对象锁
改变条件
通知所有等待在对象上的线程
105、ThreadLocal是如何实现线程之间互不干扰的?
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
threadlocal.threadlocalmap
ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
threadlocal模型
106、当发生GC后,是否会出现key为null?
看情况,如果threadlocal的强引用还存在,例如:
threadlocal强应用存在
那么key不会被回收,但是如果这个threadLocal的强引用不存在了,由于key是一个弱引用,所以必然会被回收,因此造成了key不存在但value存在的内存泄漏问题。
107、threadlocal的哈希初始值为什么选取0x61c88647?
这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
108、ThreadLocalMap是怎么处理哈希冲突的?
ThreadLocalMap中并没有链表结构,所以这里不能适用HashMap解决冲突的方式了。当出现哈希冲突时就会线性向后查找,一直找到Entry为null的槽位才会停止查找,将当前元素放入此槽位中。
解决哈希冲突
109、如果在向后寻址的过程中,遇到了key为空的情况,怎么办?
执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑。以当前staleSlot开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标slotToExpunge。for循环迭代,直到碰到Entry为null结束。
110、ThreadLocalMap的探测式清理过期key?
ThreadLocalMap的探测式清理过期key
如果,从staleSlot开始向后遍历,如果发现key为null的entry,就把他清理掉,如果key不为null,重新计算当前entry的key的hash值,把他放在它该放的地方,如果那个地方有值,就向后遍历直到为null为止。
111、什么是启发式扫描?
启发式地对Entry[]进行扫描,并清理无效的slot.

  • 从下面的while循环表达式可以知道,第一次扫描的单元是i ~ i+log2(n),
  • 如果在这期间发现了无效slot,那么把n变大到数组的长度,此时扫描单元数为log2(length)。
  • 即,在扫描的期间,如果发现了无效slot,就不断增大扫描范围。因此称之为启发式扫描。
    启发式扫描

112、何时会触发ThreadLocalMap扩容?
在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:
rehash()条件
113、ThreadLocalMap扩容流程?
首先执行expungeStaleEntries()方法
expungeStaleEntries()方法
这个方法会清理掉所有的过期的entry
然后判断当前大小是否大于阈值的3/4,如果大于就resize
resize条件
扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值。
resize()方法
114、ThreadLocalMap,get流程?
首先计算key的哈希值,然后去table中获得,如果key与我们要找的key相同,返回value;否则向后寻找,如果在向后寻找的过程中,发现有entry过期,那么执行探测式扫描,此时后面的一部分key会往前移,直到遍历完,如果没找到,返回null。
115、ReentrantLock与synchronized有什么区别?
从底层上来讲,ReentrantLock是Java层面的锁,借助AQS中的FIFO队列,以及volatile变量,还有CAS技术来实现的;synchronized是JVM层面的锁,是通过对monitor对象来完成的,它涉及到偏向锁,轻量级锁以及重量级锁。
从功能上来讲,synchronized不能够手动释放,不能中断,非公平锁,不能绑定Condition条件。
从锁的对象上来讲,synchronized锁的是对象,锁的东西保存在MarkWord中,ReentrantLock锁的是线程。
116、你更喜欢使用哪种?ReentrantLock还是synchronized?
对我而言,在一般场合下,灵活性没那么高的时候,我会选择synchronized;如果要求实现公平锁或可中断,会选择ReentrantLock。
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而ReentrantLock使用CAS,CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
117、什么是悲观锁?什么是乐观锁?
锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。
悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。
悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
118、乐观锁的实现方式?
CAS、加版本号。
119、乐观锁有哪些问题?
ABA问题,循环时间长开销大,只能保证一个共享变量的原子操作
120、什么时候选择使用乐观锁?悲观锁呢?
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
121、AQS的原理?
加锁,解锁:在AQS中,存在一个volatile变量,它的作用是用来记录当前锁的加锁状况,0表示无锁,1表示有一个线程持有锁。
state变量
同时在AbstractOwnableSynchronizer类中,存在一个exclusiveOwnerThread变量,他的作用是用来判断是哪个线程持有锁
持有锁变量
如果在获取锁的过程中,一直没有获取到锁,就会被设置为取消状态,取消状态的节点会从队列中释放。
acquireQueued方法
从上图可以看出,一但现场有任何异常就会尝试取消线程。
在这里插入图片描述

在这里插入图片描述
互斥原理:上面讲到了exclusiveOwnerThread变量来保存哪个线程独占锁,此外在AQS中还存在FIFO队列,来实现没有获取到锁的线程进入队列。
入队时,尝试调用addWaiter()方法,这个方法使用一次CAS来快速进行入队操作,失败调用enq()方法,自旋入队(JDK1.8这样,JAVA12有所变化,不使用enq,而是直接自旋)
addwaiter方法
122、在unparkSuccessor()方法中,为什么要从前往后找?
unparkSuccessor方法
节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。
综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行
addWaiter()
123、什么是读写锁?
synchronized和ReentrantLock实现的锁是排他锁,所谓排他锁就是同一时刻只允许一个线程访问共享资源,但是在平时场景中,我们通常会碰到对于共享资源读多写少的场景。对于读场景,每次只允许一个线程访问共享资源,显然这种情况使用排他锁效率就比较低下。
这个时候读写锁就应运而生了,读写锁是一种通用技术,并不是Java特有的。从名字来看,读写锁拥有两把锁,读锁和写锁。读写锁的特点是:同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞。对于读锁而言,由于同一时刻可以允许多个线程访问共享资源,进行读操作,因此称它为共享锁;而对于写锁而言,同一时刻只允许一个线程访问共享资源,进行写操作,因此称它为排他锁。
在Java中通过ReadWriteLock来实现读写锁。ReadWriteLock是一个接口,ReentrantReadWriteLock是ReadWriteLock接口的具体实现类。在ReentrantReadWriteLock中定义了两个内部类ReadLock、WriteLock,分别来实现读锁和写锁。ReentrantReadWriteLock底层是通过AQS来实现锁的获取与释放的,因此ReentrantReadWriteLock内部还定义了一个继承了AQS类的同步组件Sync,同时ReentrantReadWriteLock还支持公平与非公平性,因此它内部还定义了两个内部类FairSync、NonfairSync,它们继承了Sync。
124、读写锁实现原理?
在AQS中,通过int类型的全局变量state来表示同步状态,即用state来表示锁。ReentrantReadWriteLock也是通过AQS来实现锁的,但是ReentrantReadWriteLock有两把锁:读锁和写锁,它们保护的都是同一个资源。
由于state是int类型的变量,在内存中占用4个字节,也就是32位。将其拆分为两部分:高16位和低16位,其中高16位用来表示读锁状态,低16位用来表示写锁状态。当设置读锁成功时,就将高16位加1,释放读锁时,将高16位减1;当设置写锁成功时,就将低16位加1,释放写锁时,将第16位减1。如下图所示。
在这里插入图片描述
125、为什么读锁存在时,写锁不能被获取?
读写锁要确保写锁的操作对读锁可以见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他线程就无法感知到当前线程的操作。
126、什么是锁降级?
把持住当前写锁,然后获取到读锁,最后释放写锁的过程。
127、为什么读写锁支持锁降级但是不支持锁升级?

public void lockUpgrade(){
    ReadWriteLock lock = new ReentrantReadWriteLock();
    // 创建读锁
    Lock readLock = lock.readLock();
    // 创建写锁
    Lock writeLock = lock.writeLock();
    readLock.lock();
    try{
        // ...处理业务逻辑
        writeLock.lock();   // 代码①
    }finally {
        readLock.unlock();
    }
}

由以上代码可知,假如T1线程先获取到了读锁,然后执行后面的代码,在执行到代码①的上一行时,T2线程也去获取读锁,由于读锁是共享锁,且此时写锁还没有被获取,所以此时T2线程可以获取到读锁,当T1执行到代码①时,尝试去获取写锁,由于有T2线程占用了读锁,所以T1线程是无法获取到写锁的,只能等待,当T2也执行到代码①时,由于T1占有了读锁,导致T2无法获取到写锁,这样两个线程就一直等待,即获取不到写锁,也释放不掉读锁。因此锁是不支持锁升级的。总的来说是为了防止死锁。
128、为什么要锁降级?
锁的降级是为了保证可见性。让T1线程对数据的修改对其他线程可见。
129、什么是LockSupport?
LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。
130、什么是Condition
任何一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait(),wait(long timeout),notify(),notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
131、Condition的原理
Condtion原理
Condition是依赖于Lock的一个工具,当调用 Condition 的 await() 方法(或者以 await开头的方法),会使得当前线程进入等待队列,并且释放锁。调用 Condition.signal() 方法,将会唤醒等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中(需要注意的是,调用该方法的前置条件是当前线程必须获得了锁,可以看到 Signal() 方法进行了 isHeldExclusively 检查,判断是否获得了锁,接着获取等待队列的首节点,将其移动到同步队列并使用 LockSupport 唤醒节点中的线程。)。
132、Condition的使用场景?
线程需要等待一个或者多个条件 才能确定是否进行下一步业务流程的情况,比如工厂里面每一个生产环节必须等待上一个生产环节任务完成才能执行任务,自己执行完任务后又需要通知下一个环节的线程。
133、在Condition对节点的更新时并没有使用CAS,为什么?
在调用await()方法时的线程一定是获取了锁的线程,也就是说该过程是由锁来保证的。
未使用CAS
134、HashMap一般在多线程状态下会存在并发问题,如何解决?
使用Collections.synchronizedMap(Map)创建线程安全的map集合
Hashtable
ConcurrentHashMap
135、Collections.synchronizedMap是怎么实现线程安全的?
在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex,如图

在这里插入图片描述
我们在调用这个方法的时候就需要传入一个Map,可以看到有两个构造器,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。
如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象,就是上面的Map。
创建出synchronizedMap之后,再操作map的时候,就会对方法上锁,如图全是
在这里插入图片描述
136、Hashtable跟HashMap不一样的地方?
Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。
实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。Dictionary 是 JDK 1.0 添加的,貌似没人用过这个,我也没用过。
初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。
所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。
(具体对比在Java基础之问中。)
137、JDK1.7中ConcurrentHashmap是怎样一个模型?
在这里插入图片描述

如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。每个segment数组中存在一个HashEntry型的数组,HashEntry类似于HashMap,不同的是HashEntry为value和next加了volatile修饰![HashEntry](https://img-blog.csdnimg.cn/20200818163824684.png#pic_center)

138、为什么HashEntry要使用volatile来修饰next?
主要还是为了解决并发安全问题,比如JDK1.7中的死循环问题
139、JDK1.7中ConcurrentHashMap,put()流程
1、根据key算出一个hash值
2、根据(hash>>>segmentShift)&segementMask(哈希值的高四位),定位到段,调用segment的put()方法
3、根据第一个hash值,进行定位到hashEntry(低四位),
4、尝试获取锁,如果失败自旋获取锁,获取锁后,类似于hashMap的定位方法,先定位到槽,然后寻找
在这里插入图片描述
在这里插入图片描述
140、JDK1.7中get流程?
在这里插入图片描述
1、首先根据key计算出hash值
2、二次哈希定位到segment
3、根据第一个hash定位到槽
4、遍历获取
141、为什么ConcurrentHashMao读操作不加锁?有什么坏处?
不加锁的原因是因为使用了volatile来修饰变量,所以当去获取时,一定可以看到最近的数据,但是这个数据不一定会是最新的数据,比如调用putAll()方法时,另一个线程只能获取到已经put进了的,还没有put进入的不能获取到。
142、请问这一行代码是干嘛的?
在这里插入图片描述
这是一种优化。
ConcurrentHashMap中,获取数组中的元素时,都不是使用的array[index]的形式,而是直接从内存中获取,采用这样的公式来定位内存中,这个元素的位置:数组在内存中的起始地址 + (元素在数组中的索引 * 每个元素的尺寸).为什么采用这种方式呢?因为在Java中,对数组通过索引获取元素时,需要检查有没有范围越界的问题,没有采用直接访问内存的这种方式的效率高.在ConcurrentHashMap中,为了进一步提高效率,就采用了直接从内存访问的方式.
我们先看一下SSHIFT这个实例变量是怎么得到的,从ConcurrentHashMap的底部可以得到:
在这里插入图片描述
其中,ss变量表示的是Segment[]数组中,增量的长度,也就是说,数组中相邻的两个索引在内存地址上的距离,那么,31 - Integer.numberOfLeadingZero(ss)表示什么意思呢?
我们查看Integer中的numberOfLeadingZeros方法的源码:
在这里插入图片描述

这里我们并不深入这个方法的实现,而是看它的注释,从注释中,我们很清晰的就能发现,有这么一个公式:

在这里插入图片描述
其中floor的意思是,小于等于并且最接近以二为底的x的对数.
在这里,由于ss是4(通过在ConcurrentHashMap中加一条打印语句来查看.因为在32bit机器上或者最大堆内存小于32Gb的机器上,一个对象的引用占4个字节,所以ss是4),所以,上面的公式就等于:
在这里插入图片描述
所以,我们有:
在这里插入图片描述
在结合(index << SSHIFT) + SBASE,我们可以得到下面的式子:
在这里插入图片描述
其中index就是哈希为h的那个Segment在segments[]数组中的索引,SBASE这个变量表示的是,segment[]数组在内存中的其实地址.总的来说是为了优化

143、JDK1.8中put()流程?
put()代码
判断传进来的key和value是否为空,在ConcurrentHashMap中key和value都不允许为空,然而在HashMap中是可以为key和val都可以为空,这一点值得注意一下;
对key进行重hash计算,获得hash值;
如果当前的数组为空,说明这是第一插入数据,则会对table进行初始化;
插入数据,这里分为3中情况:
1). 插入位置为空,直接将数据放入table的第一个位置中;
2). 插入位置不为空,并且改为是一个ForwardingNode节点,说明该位置上的链表或红黑树正在进行扩容,然后让当前线程加进去并发扩容,提高效率;
3). 插入位置不为空,也不是ForwardingNode节点,若为链表则从第一节点开始组个往下遍历,如果有key的hashCode相等并且值也相等,那么就将该节点的数据替换掉,否则将数据加入到链表末段;若为红黑树,则按红黑树的规则放进相应的位置;
数据插入成功后,判断当前位置上的节点的数量,如果节点数据大于转换红黑树阈值(默认为8),则将链表转换成红黑树,提高get操作的速度;
数据量+1,并判断当前table是否需要扩容;
144、在JDK1.7中,为什么确定segment的下标时使用(hash >>> segmentShift) & segmentMask 而计算 hashEntry的下标时使用 (tab.length - 1) & hash?
这是为了尽量避免当前 hash 值计算出来的 Segment 数组下标和计算出来的 HashEntry 数组下标趋于相同。简单说,就是为了避免分配到同一个 Segment 中的元素扎堆现象,即避免它们都被分配到同一条链表上,导致链表过长。同时,也是为了减少并发。
145、看下图,请问为什么要进行三次判空?
在这里插入图片描述
在多线程环境下,因为不确定是什么时候会有其它线程 CAS 成功,有可能发生在以上的任意时刻。所以,只要发现一旦内存中的对象已经存在了,则说明已经有其它线程把Segment对象创建好,并CAS成功同步到主内存了。此时,就可以直接返回,而不需要往下执行了。这样做,是为了代码执行效率考虑。
146、JDK1.7中ConcurrentHasmp rehash()流程?
在这里插入图片描述
在这里插入图片描述
当 put 方法时,发现元素个数超过了阈值,则会扩容。需要注意的是,每个Segment只管它自己的扩容,互相之间并不影响。换句话说,可以出现这个 Segment的长度为2,另一个Segment的长度为4的情况(只要是2的n次幂)。
在扩容时,会扩大为当前的两倍,然后会进行一次循环,找到一个lastRun的位置,lastRun是用来表示他后面的值都与这个值的hash后在新数组的位置相同,然后把lastRun和后面的节点一次性给放到新的数组中,然后逐一复制头结点到lastRun的前一个节点到新数组。
147、JDK1.7中size()方法的流程?
在这里插入图片描述
先采用乐观的方式,认为在统计 size 的过程中,并没有发生 put, remove 等会改变 Segment 结构的操作。 但是,如果发生了(判断这次统计的值和上次是否相等),就需要重试。如果重试2次都不成功(执行三次,第一次不能叫做重试),就只能强制把所有 Segment 都加锁之后,再统计了,以此来得到准确的结果。
148、initTable()方法流程?
在这里插入图片描述
首先进行判断是否数组为空,如果为空进入自旋,在自旋中,如果发现sizeCtl < 0(其实也就是-1)表示有其他线程正在对他进行初始化,暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程,然后把sizeCtl设置为-1,初始化数组。
149、addCount()方法流程?
addCount()方法
首先判断counterCell数组是否为空且尝试把baseCount+1即容量+1,如果不为空或者修改不成功尝试把当前线程分配到一个counterCell格子,然后再次尝试+1。

	//线程被分配到的格子
@sun.misc.Contended static final class CounterCell {
	//此格子内记录的 value 值
    volatile long value;
    CounterCell(long x) { value = x; }
}

//用来存储线程和线程生成的随机数的对应关系
static final int getProbe() {
	return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

// x为1,check代表链表上的元素个数
private final void addCount(long x, int check) {
	CounterCell[] as; long b, s;
	//此处要进入if有两种情况
	//1.数组不为空,说明数组已经被创建好了。
	//2.若数组为空,说明数组还未创建,很有可能竞争的线程非常少,因此就直接 CAS 操作 baseCount
	//若 CAS 成功,则方法跳转到 (2)处,若失败,则需要考虑给当前线程分配一个格子(指CounterCell对象)
	if ((as = counterCells) != null ||
		!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
		CounterCell a; long v; int m;
		//字面意思,是无竞争,这里先标记为 true,表示还没有产生线程竞争
		boolean uncontended = true;
		//这里有三种情况,会进入 fullAddCount 方法
		//1.若数组为空,进方法 (1)
		//2.ThreadLocalRandom.getProbe() 方法会给当前线程生成一个随机数(可以简单的认为也是一个hash值)
		//然后用随机数与数组长度取模,计算它所在的格子。若当前线程所分配到的格子为空,进方法 (1)。
		//3.若数组不为空,且线程所在格子不为空,则尝试 CAS 修改此格子对应的 value 值加1。
		//若修改成功,则跳转到 (3),若失败,则把 uncontended 值设为 fasle,说明产生了竞争,然后进方法 (1)
		if (as == null || (m = as.length - 1) < 0 ||
			(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
			!(uncontended =
			  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
			//方法(1), 这个方法的目的是让当前线程一定把 1 加成功。情况更多,更复杂,稍后讲。
			fullAddCount(x, uncontended);
			return;
		}
		//(3)能走到这,说明数组不为空,且修改 baseCount失败,
		//且线程被分配到的格子不为空,且修改 value 成功。
		//但是这里没明白为什么小于等于1,就直接返回了,这里我怀疑之前的方法漏掉了binCount=0的情况。
		//而且此处若返回了,后边怎么判断扩容?(存疑)
		if (check <= 1)
			return;
		//计算总共的元素个数
		s = sumCount();
	}
	//(2)这里用于检查是否需要扩容(下边这部分很多逻辑不懂的话,等后边讲完扩容,再回来看就理解了)
	if (check >= 0) {
		Node<K,V>[] tab, nt; int n, sc;
		//若元素个数达到扩容阈值,且tab不为空,且tab数组长度小于最大容量
		while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
			   (n = tab.length) < MAXIMUM_CAPACITY) {
			//这里假设数组长度n就为16,这个方法返回的是一个固定值,用于当做一个扩容的校验标识
			//可以跳转到最后,看详细计算过程,0000 0000 0000 0000 1000 0000 0001 1011
			int rs = resizeStamp(n);
			//若sc小于0,说明正在扩容
			if (sc < 0) {
			    //sc的结构类似这样,1000 0000 0001 1011 0000 0000 0000 0001
				//sc的高16位是数据校验标识,低16位代表当前有几个线程正在帮助扩容,RESIZE_STAMP_SHIFT=16
				//因此判断校验标识是否相等,不相等则退出循环
				//sc == rs + 1,sc == rs + MAX_RESIZERS 这两个应该是用来判断扩容是否已经完成,但是计算方法存疑
				//感兴趣的可以看这个地址,应该是一个 bug ,
				// https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
				//nextTable=null 说明需要扩容的新数组还未创建完成
				//transferIndex这个参数小于等于0,说明已经不需要其它线程帮助扩容了,
				//但是并不说明已经扩容完成,因为有可能还有线程正在迁移元素。稍后扩容细讲就明白了。
				if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
					sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
					transferIndex <= 0)
					break;
				//到这里说明当前线程可以帮助扩容,因此sc值加一,代表扩容的线程数加1
				if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
					transfer(tab, nt);
			}
			//当sc大于0,说明sc代表扩容阈值,因此第一次扩容之前肯定走这个分支,用于初始化新表 nextTable
			//rs<<16
			//1000 0000 0001 1011 0000 0000 0000 0000
			//+2
			//1000 0000 0001 1011 0000 0000 0000 0010
			//这个值,转为十进制就是 -2145714174,用于标识,这是扩容时,初始化新表的状态,
			//扩容时,需要用到这个参数校验是否所有线程都全部帮助扩容完成。
			else if (U.compareAndSwapInt(this, SIZECTL, sc,
										 (rs << RESIZE_STAMP_SHIFT) + 2))
				//扩容,第二个参数代表新表,传入null,则说明是第一次初始化新表(nextTable)
				transfer(tab, null);
			s = sumCount();
		}
	}
}

//计算表中的元素总个数
final long sumCount() {
	CounterCell[] as = counterCells; CounterCell a;
	//baseCount,以这个值作为累加基准
	long sum = baseCount;
	if (as != null) {
		//遍历 counterCells 数组,得到每个对象中的value值
		for (int i = 0; i < as.length; ++i) {
			if ((a = as[i]) != null)
				//累加 value 值
				sum += a.value;
		}
	}
	//此时得到的就是元素总个数
	return sum;
}	

//扩容时的校验标识
static final int resizeStamp(int n) {
	return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

//Integer.numberOfLeadingZeros方法的作用是返回 n 的最高位为1的前面的0的个数
//n=16,
0000 0000 0000 0000 0000 0000 0001 0000
//前面有27个0,即27
0000 0000 0000 0000 0000 0000 0001 1011
//RESIZE_STAMP_BITS为16,然后 1<<(16-1),即 1<<15
0000 0000 0000 0000 1000 0000 0000 0000
//它们做或运算,得到 rs 的值
0000 0000 0000 0000 1000 0000 0001 1011

150、fullAddCount()方法流程?

//传过来的参数分别为 1 , false
private final void fullAddCount(long x, boolean wasUncontended) {
	int h;
	//如果当前线程的随机数为0,则强制初始化一个值
	if ((h = ThreadLocalRandom.getProbe()) == 0) {
		ThreadLocalRandom.localInit();      // force initialization
		h = ThreadLocalRandom.getProbe();
		//此时把 wasUncontended 设为true,认为无竞争
		wasUncontended = true;
	}
	//用来表示比 contend(竞争)更严重的碰撞,若为true,表示可能需要扩容,以减少碰撞冲突
	boolean collide = false;                // True if last slot nonempty
	//循环内,外层if判断分三种情况,内层判断又分为六种情况
	for (;;) {
		CounterCell[] as; CounterCell a; int n; long v;
		//1. 若counterCells数组不为空。  建议先看下边的2和3两种情况,再回头看这个。 
		if ((as = counterCells) != null && (n = as.length) > 0) {
			// (1) 若当前线程所在的格子(CounterCell对象)为空
			if ((a = as[(n - 1) & h]) == null) {
				if (cellsBusy == 0) {    
					//若无锁,则乐观的创建一个 CounterCell 对象。
					CounterCell r = new CounterCell(x); 
					//尝试加锁
					if (cellsBusy == 0 &&
						U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
						boolean created = false;
						//加锁成功后,再 recheck 一下数组是否不为空,且当前格子为空
						try {               
							CounterCell[] rs; int m, j;
							if ((rs = counterCells) != null &&
								(m = rs.length) > 0 &&
								rs[j = (m - 1) & h] == null) {
								//把新创建的对象赋值给当前格子
								rs[j] = r;
								created = true;
							}
						} finally {
							//手动释放锁
							cellsBusy = 0;
						}
						//若当前格子创建成功,且上边的赋值成功,则说明加1成功,退出循环
						if (created)
							break;
						//否则,继续下次循环
						continue;           // Slot is now non-empty
					}
				}
				//若cellsBusy=1,说明有其它线程抢锁成功。或者若抢锁的 CAS 操作失败,都会走到这里,
				//则当前线程需跳转到(9)重新生成随机数,进行下次循环判断。
				collide = false;
			}
			/**
			*后边这几种情况,都是数组和当前随机到的格子都不为空的情况。
			*且注意每种情况,若执行成功,且不break,continue,则都会执行(9),重新生成随机数,进入下次循环判断
			*/
			// (2) 到这,说明当前方法在被调用之前已经 CAS 失败过一次,若不明白可回头看下 addCount 方法,
			//为了减少竞争,则跳转到⑨处重新生成随机数,并把 wasUncontended 设置为true ,认为下一次不会产生竞争
			else if (!wasUncontended)       // CAS already known to fail
				wasUncontended = true;      // Continue after rehash
			// (3) 若 wasUncontended 为 true 无竞争,则尝试一次 CAS。若成功,则结束循环,若失败则判断后边的 (4)(5)(6)。
			else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
				break;
			// (4) 结合 (6) 一起看,(4)(5)(6)都是 wasUncontended=true,且CAS修改value失败的情况。
			//若数组有变化,或者数组长度大于等于当前CPU的核心数,则把 collide 改为 false
			//因为数组若有变化,说明是由扩容引起的;长度超限,则说明已经无法扩容,只能认为无碰撞。
			//这里很有意思,认真思考一下,当扩容超限后,则会达到一个平衡,即 (4)(5) 反复执行,直到 (3) 中CAS成功,跳出循环。
			else if (counterCells != as || n >= NCPU)
				collide = false;            // At max size or stale
			// (5) 若数组无变化,且数组长度小于CPU核心数时,且 collide 为 false,就把它改为 true,说明下次循环可能需要扩容
			else if (!collide)
				collide = true;
			// (6) 若数组无变化,且数组长度小于CPU核心数时,且 collide 为 true,说明冲突比较严重,需要扩容了。
			else if (cellsBusy == 0 &&
					 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
				try {
					//recheck
					if (counterCells == as) {// Expand table unless stale
						//创建一个容量为原来两倍的数组
						CounterCell[] rs = new CounterCell[n << 1];
						//转移旧数组的值
						for (int i = 0; i < n; ++i)
							rs[i] = as[i];
						//更新数组
						counterCells = rs;
					}
				} finally {
					cellsBusy = 0;
				}
				//认为扩容后,下次不会产生冲突了,和(4)处逻辑照应
				collide = false;
				//当次扩容后,就不需要重新生成随机数了
				continue;                   // Retry with expanded table
			}
			// (9),重新生成一个随机数,进行下一次循环判断
			h = ThreadLocalRandom.advanceProbe(h);
		}
		//2.这里的 cellsBusy 参数非常有意思,是一个volatile的 int值,用来表示自旋锁的标志,
		//可以类比 AQS 中的 state 参数,用来控制锁之间的竞争,并且是独占模式。简化版的AQS。
		//cellsBusy 若为0,说明无锁,线程都可以抢锁,若为1,表示已经有线程拿到了锁,则其它线程不能抢锁。
		else if (cellsBusy == 0 && counterCells == as &&
				 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
			boolean init = false;
			try {    
				//这里再重新检测下 counterCells 数组引用是否有变化
				if (counterCells == as) {
					//初始化一个长度为 2 的数组
					CounterCell[] rs = new CounterCell[2];
					//根据当前线程的随机数值,计算下标,只有两个结果 0 或 1,并初始化对象
					rs[h & 1] = new CounterCell(x);
					//更新数组引用
					counterCells = rs;
					//初始化成功的标志
					init = true;
				}
			} finally {
				//别忘了,需要手动解锁。
				cellsBusy = 0;
			}
			//若初始化成功,则说明当前加1的操作也已经完成了,则退出整个循环。
			if (init)
				break;
		}
		//3.到这,说明数组为空,且 2 抢锁失败,则尝试直接去修改 baseCount 的值,
		//若成功,也说明加1操作成功,则退出循环。
		else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
			break;                          // Fall back on using base
	}
}

150、transfer()方法

//迁移数据
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
	int n = tab.length, stride;
	//根据当前CPU核心数,确定每次推进的步长,最小值为16.(为了方便我们以2为例)
	if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
		stride = MIN_TRANSFER_STRIDE; // subdivide range
	//从 addCount 方法,只会有一个线程跳转到这里,初始化新数组
	if (nextTab == null) {            // initiating
		try {
			@SuppressWarnings("unchecked")
			//新数组长度为原数组的两倍
			Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
			nextTab = nt;
		} catch (Throwable ex) {      // try to cope with OOME
			sizeCtl = Integer.MAX_VALUE;
			return;
		}
		//用 nextTable 指代新数组
		nextTable = nextTab;
		//这里就把推进的下标值初始化为原数组长度(以16为例)
		transferIndex = n;
	}
	//新数组长度
	int nextn = nextTab.length;
	//创建一个标志类
	ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
	//是否向前推进的标志
	boolean advance = true;
	//是否所有线程都全部迁移完成的标志
	boolean finishing = false; // to ensure sweep before committing nextTab
	//i 代表当前线程正在迁移的桶的下标,bound代表它本次可以迁移的范围下限
	for (int i = 0, bound = 0;;) {
		Node<K,V> f; int fh;
		//需要向前推进
		while (advance) {
			int nextIndex, nextBound;
			// (1) 先看 (3) 。i每次自减 1,直到 bound。若超过bound范围,或者finishing标志为true,则不用向前推进。
			//若未全部完成迁移,且 i 并未走到 bound,则跳转到 (7),处理当前桶的元素迁移。
			if (--i >= bound || finishing)
				advance = false;
			// (2) 每次执行,都会把 transferIndex 最新的值同步给 nextIndex
			//若 transferIndex小于等于0,则说明原数组中的每个桶位置,都有线程在处理迁移了,
			//于是,需要跳出while循环,并把 i设为 -1,以跳转到④判断在处理的线程是否已经全部完成。
			else if ((nextIndex = transferIndex) <= 0) {
				i = -1;
				advance = false;
			}
			// (3) 第一个线程会先走到这里,确定它的数据迁移范围。(2)处会更新 nextIndex为 transferIndex 的最新值
			//因此第一次 nextIndex=n=16,nextBound代表当次迁移的数据范围下限,减去步长即可,
			//所以,第一次时,nextIndex=16,nextBound=16-2=14。后续,每次都会间隔一个步长。
			else if (U.compareAndSwapInt
					 (this, TRANSFERINDEX, nextIndex,
					  nextBound = (nextIndex > stride ?
								   nextIndex - stride : 0))) {
				//bound代表当次数据迁移下限
				bound = nextBound;
				//第一次的i为15,因为长度16的数组,最后一个元素的下标为15
				i = nextIndex - 1;
				//表明不需要向前推进,只有当把当前范围内的数据全部迁移完成后,才可以向前推进
				advance = false;
			}
		}
		// (4)
		if (i < 0 || i >= n || i + n >= nextn) {
			int sc;
			//若全部线程迁移完成
			if (finishing) {
				nextTable = null;
				//更新table为新表
				table = nextTab;
				//扩容阈值改为原来数组长度的 3/2 ,即新长度的 3/4,也就是新数组长度的0.75倍
				sizeCtl = (n << 1) - (n >>> 1);
				return;
			}
			//到这,说明当前线程已经完成了自己的所有迁移(无论参与了几次迁移),
			//则把 sc 减1,表明参与扩容的线程数减少 1。
			if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
				//在 addCount 方法最后,我们强调,迁移开始时,会设置 sc=(rs << RESIZE_STAMP_SHIFT) + 2
				//每当有一个线程参与迁移,sc 就会加 1,每当有一个线程完成迁移,sc 就会减 1。
				//因此,这里就是去校验当前 sc 是否和初始值是否相等。相等,则说明全部线程迁移完成。
				if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
					return;
				//只有此处,才会把finishing 设置为true。
				finishing = advance = true;
				//这里非常有意思,会把 i 从 -1 修改为16,
				//目的就是,让 i 再从后向前扫描一遍数组,检查是否所有的桶都已被迁移完成,参看 (6)
				i = n; // recheck before commit
			}
		}
		// (5) 若i的位置元素为空,则说明当前桶的元素已经被迁移完成,就把头结点设置为fwd标志。
		else if ((f = tabAt(tab, i)) == null)
			advance = casTabAt(tab, i, null, fwd);
		// (6) 若当前桶的头结点是 ForwardingNode ,说明迁移完成,则向前推进 
		else if ((fh = f.hash) == MOVED)
			advance = true; // already processed
		//(7) 处理当前桶的数据迁移。
		else {
			synchronized (f) {  //给头结点加锁
				if (tabAt(tab, i) == f) {
					Node<K,V> ln, hn;
					//若hash值大于等于0,则说明是普通链表节点
					if (fh >= 0) {
						int runBit = fh & n;
						//这里是 1.7 的 CHM 的 rehash 方法和 1.8 HashMap的 resize 方法的结合体。
						//会分成两条链表,一条链表和原来的下标相同,另一条链表是原来的下标加数组长度的位置
						//然后找到 lastRun 节点,从它到尾结点整体迁移。
						//lastRun前边的节点则单个迁移,但是需要注意的是,这里是头插法。
						//另外还有一点和1.7不同,1.7 lastRun前边的节点是复制过去的,而这里是直接迁移的,没有复制操作。
						//所以,最后会有两条链表,一条链表从 lastRun到尾结点是正序的,而lastRun之前的元素是倒序的,
						//另外一条链表,从头结点开始就是倒叙的。看下图。
						Node<K,V> lastRun = f;
						for (Node<K,V> p = f.next; p != null; p = p.next) {
							int b = p.hash & n;
							if (b != runBit) {
								runBit = b;
								lastRun = p;
							}
						}
						if (runBit == 0) {
							ln = lastRun;
							hn = null;
						}
						else {
							hn = lastRun;
							ln = null;
						}
						for (Node<K,V> p = f; p != lastRun; p = p.next) {
							int ph = p.hash; K pk = p.key; V pv = p.val;
							if ((ph & n) == 0)
								ln = new Node<K,V>(ph, pk, pv, ln);
							else
								hn = new Node<K,V>(ph, pk, pv, hn);
						}
						setTabAt(nextTab, i, ln);
						setTabAt(nextTab, i + n, hn);
						setTabAt(tab, i, fwd);
						advance = true;
					}
					//树节点
					else if (f instanceof TreeBin) {
						TreeBin<K,V> t = (TreeBin<K,V>)f;
						TreeNode<K,V> lo = null, loTail = null;
						TreeNode<K,V> hi = null, hiTail = null;
						int lc = 0, hc = 0;
						for (Node<K,V> e = t.first; e != null; e = e.next) {
							int h = e.hash;
							TreeNode<K,V> p = new TreeNode<K,V>
								(h, e.key, e.val, null, null);
							if ((h & n) == 0) {
								if ((p.prev = loTail) == null)
									lo = p;
								else
									loTail.next = p;
								loTail = p;
								++lc;
							}
							else {
								if ((p.prev = hiTail) == null)
									hi = p;
								else
									hiTail.next = p;
								hiTail = p;
								++hc;
							}
						}
						ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
							(hc != 0) ? new TreeBin<K,V>(lo) : t;
						hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
							(lc != 0) ? new TreeBin<K,V>(hi) : t;
						setTabAt(nextTab, i, ln);
						setTabAt(nextTab, i + n, hn);
						setTabAt(tab, i, fwd);
						advance = true;
					}
				}
			}
		}
	}
}

在这里插入图片描述
151、哪些方法会调用helpTransfer()方法?
merge()
compute()
computeIfPresent()
clear()
replaceNode()
putVal()
152、为什么JDK1.8要把lock+segment换为sychronized+CAS?
因为sychronized被大大优化了,利用它的锁升级来减轻并发,同时锁的粒度也变细了,锁的粒度变为了链表头结点。
可以减少内存开销,原本锁住的是segment,现在锁住的是链表头结点。
获得了JVM的支持,有更好的优化,例如锁粗化,锁消除,锁自旋等等。
至于CAS,它是一种乐观锁的方式,也能够带来性能上的提升。
153、ConcurrentLinkedQueue什么时候会把尾节点设为tail?
当 tail 指向的节点的下一个节点不为 null 的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过 casTail 进行 tail 更新;当 tail 指向的节点的下一个节点为 null 的时候,只插入节点不更新 tail。
154、ConcurrentLinkedQueue定位头节点过程?
如果当前节点的数据域为 null,很显然该节点不是待删除的节点,就用当前节点的下一个节点去试探。
155、ConcurrentLinkedQueue poll()流程
如果当前 head,h 和 p 指向的节点的 Item 不为 null 的话,说明该节点即为真正的队头节点(待删除节点),只需要通过 casItem 方法将 item 域设置为 null,然后将原来的 item 直接返回即可。
如果当前 head,h 和 p 指向的节点的 item 为 null 的话,则说明该节点不是真正的待删除节点,那么应该做的就是寻找 item 不为 null 的节点。通过让 q 指向 p 的下一个节点(q = p.next)进行试探,若找到则通过 updateHead 方法更新 head 指向的节点以及构造哨兵节点(通过updateHead方法的h.lazySetNext(h))
156、ConcurrentLinkedQueue是否可以通过poll()来判断队列是否为空?
不可以。
假设此时队列为空
队列
线程1执行poll(),线程2执行offer(),线程1执行到箭头处切换CPU,线程2加入一个值,线程1直接返回了。
157、ConcurrentLinkedQueue 更新head节点流程
当 head 指向的节点的 item 域为 null 的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过 updateHead 进行 head 更新;当 head 指向的节点的 item 域不为 null 的时候,只删除节点不更新 head。
158、什么是HOPS?为什么要使用他?
如果让 tail 永远作为队列的队尾节点,实现的代码量会更少,而且逻辑更易懂。但是,这样做有一个缺点,
如果大量的入队操作,每次都要执行 CAS 进行 tail 的更新,汇总起来对性能也会是大大的损耗。如果能减少 CAS 更新的操作,无疑可以大大提升入队的操作效率,所以 doug lea 大师每间隔 1 次(tail 和队尾节点的距离为 1)进行才利用 CAS 更新 tail。
对 head 的更新也是同样的道理,虽然,这样设计会多出在循环中定位队尾节点,但总体来说读的操作效率要远远高于写的性能,因此,多出来的在循环中定位尾节点的操作的性能损耗相对而言是很小的。
159、什么是阻塞队列?
阻塞队列是一个支持两个附加操作的队列。这两个附加操作支持阻塞的插入和移除方法。
(1)支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
(2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
160、Java中有哪些阻塞队列?
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
161、什么是ArrayBlockingQueue?
是一个用数组实现的有界阻塞队列
162、说一下ArrayBlockingQueue的take()和put()方法,以及怎么实现阻塞的,在说一下他是怎么标记队头和队尾的?
以take()方法为例:
take()方法
首先获得锁(这个锁是一个全局锁,因此take()和put()方法都是一个锁),如果获取到了这个锁,那么判断count这个变量的大小是否为0
count变量
count变量的作用在于保存这个队列的大小,因为会涉及到这个变量的方法都使用了lock,因此没有使用volatile来修饰。
如果count变量唯空,那么调用condition的await()方法,进入阻塞状态,等待被唤醒。
put()方法
put()方法类似。
163、LinkedBlockingQueue和ArrayBlockingQueue有什么区别
LinkedBlockingQueue是由链表实现的一个单项队列,而ArrayBlockingQueue是用数组实现的。
LinkedBlockingQueue有两个锁,takelock和putlock,而ArrayBlockingQueue只有一个全局锁。
LinkedBlockingQueue之所以要设置两个锁(这种算法叫双锁队列)是因为插入只会在尾节点,取出只会在头节点,因此不会存在死锁和并发问题。而ArrayBlockingQueue依靠putIndex和takeIndex来确定插入和取出位置,因此只有一个锁。
LinkedBlockingQueue的count变量是原子类,因此可能会一个线程插入,一个线程取出,所以要设置为原子类,但是ArrayBlockingQueue只有一把锁所以不需要原子类。
164、LinkedBlockingQueue的take和put流程?
在这里插入图片描述
首先获得take锁,然后判断容量,如果为0则进入notEmpty队列阻塞,否则插入队列,将容量减少1,最后释放锁,然后最后容量小于最大容量,则唤醒notFull阻塞队列中的线程(CAS返回之前的值)。
165、什么事PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序 升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化 PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证 同优先级元素的顺序。
166、PriorityBlockingQueue内部数据结构?
PriorityBlockingQueue本质上是一个最小堆,堆的顶部是根据排序后得到的最小值。
167、说说PriorityBlockingQueue,take()的流程
在这里插入图片描述
首先获得全局锁,然后调用dequeue()方法
在这里插入图片描述
dequeue()方法首先判断顶部元素(也就是最小元素是否为存在),如果存在弹出他,在弹出之前还需要对整个最小堆进行siftDown操作,把第二小的元素放到堆顶
在这里插入图片描述
siftDownComparable()方法做的是从顶部开始,找到左子节点和右子节点较小的值,然后依次下沉。
168、PriorityBlockdingQueue(),put()流程
put()方法唯一的作用就是调用offer()方法,因此分析ofeer()方法。
offer()方法
首先获得全局锁,然后判断当前是否以大于或等于队列长度,如果大于扩容;然后执行上浮操作,比如调用siftUpComparable()方法
在这里插入图片描述
判断与父亲节点的大小,如果小于父亲节点则执行上浮操作,上浮到相应位置后,跳回offer()方法,执行size+1以及唤醒等待在notEmpty队列的线程。最糊释放锁。
169、PriorityBlockingQueue的扩容流程
在这里插入图片描述
首先释放锁,之所以这里要释放锁,是为了增大吞吐量,使得其他线程可以出队列。
判断allocationSpinLock的值是否为0并且尝试使用CAS设置为1,allocationSpinLock的值的作用是用作锁,上面操作成功后新的容量等于原来容量的1.5倍或者2倍+2,这取决于原来容量是否大于64,如果超过了MAX_ARRAY_SIZE那么新容量为MAX_ARRAY_SIZE最后把allocationSpinLock设置为1,如果扩容失败,那么调用Thread.yield()方法让出cpu,然后再次获取全局锁,复制就数组到新数组。
170、什么是DelayQueue
DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
171、DelayQueue的offer()流程?
在这里插入图片描述
首先获取全局锁,获取到锁后把元素放入PriorityQueue中,然后判断队列头元素是不是当前元素,如果是说明离过期时间最小,然后把leader设为null(leader 指向的是第一个从队列获取元素阻塞等待的线程,其作用是减少其他线程不必要的等待时间),然后唤醒阻塞在available队列上的线程。最后释放锁。
172、说说DelayQueue的take()方法?
take()
首先获取全局锁,获取到锁后,使用自旋的方式从PriorityQueue调用peek()中获得元素。如果这个元素为null,说明队列暂时没有元素调用available.await(),是当前线程阻塞在available条件队列上;如果不为null,获得当前元素当前的剩余时间,如果小于0直接poll();然后判断是否有leader线程,如果有那么阻塞在available队列上,如果没有,则把自己设为leader线程,并等待剩余的时间,到期后判断自己是否为leader然后把leader设为null,最后唤醒follwer线程,释放锁。
173、什么是leader/follower模式?
在这里插入图片描述
所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。
174、什么是SynchronousQueue?
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作, 否则不能继续添加元素。 它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法 可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的 顺序访问队列。
175、SynchronousQueue实现公平模式时底层数据结构是什么?
是一个名叫TransferQueue的FIFO队列,实现非公平模式时底层数据结构是一个名叫TransferStack的堆。
176、说一下SynchronousQueue的put()流程
put()
首先对元素进行判空,如果元素不为空那么调用transfer对象的transfer()方法,对于这个对象来说,它是一个名为Transferer的抽象类
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200909155642382.png?x-oss-在这里插入图片描述
在运行期间JVM会确定他的一个实现类。他有两种实现类,一个是TransferQueue和TransferStack。
176、说说TransferQueue的transfer()方法的实现
根据注释来看
注释
该方法大概有两种情况,1、如果队列为空或者队列持有相同模式的节点,尝试把节点加入队列,等待完成或取消;2、如果队列显然包含等待项,并且此调用是互补模式,尝试通过CAS来匹配节点并将匹配节点其出队,然后返回匹配项来实现。
在此方法中根据传入的对象是否为空来判断当前模式是data还是request,也就是上文的两种情况。
对于这个方法来说,如果队列为空或者队尾元素与当前元素的模式相同,那么会自旋阻塞;如果队列不为空或者和队头元素模式不一致那么会尝试匹配队头元素
在这里插入图片描述
在这里插入图片描述
总结下来就是:队尾匹配队头出队,先进先出,体现公平原则。

E transfer(E e, boolean timed, long nanos) {
    /**
     *
     * 这个基本方法, 主要分为两种情况
     *
     * 1. 若队列为空 / 队列中的尾节点和自己的 类型相同, 则添加 node
     *      到队列中, 直到 timeout/interrupt/其他线程和这个线程匹配
     *      timeout/interrupt awaitFulfill方法返回的是 node 本身
     *      匹配成功的话, 要么返回 null (producer返回的), 或正真的传递值 (consumer 返回的)
     *
     * 2. 队列不为空, 且队列的 head.next 节点是当前节点匹配的节点,
     *      进行数据的传递匹配, 并且通过 advanceHead 方法帮助 先前 block 的节点 dequeue
     */

    QNode s = null; // 根据需要构造/重用
    // true:put  false:get
    boolean isData = (e != null);

    for (;;) {
        // 队列首尾的临时变量,队列空时,t=h
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null) // 看到未初始化的值
            continue;               // 自旋
        // 首尾节点相同,队列空
        // 或队尾节点的操作和当前节点操作相同
        if (h == t || t.isData == isData) {
            QNode tn = t.next;
            // tail 被修改,重试
            if (t != tail)
                continue;
            // 队尾后面的值还不为空,说明其他线程添加了 tail.next,t 还不是队尾,直接把 tn 赋值给 t
            if (tn != null) {
                advanceTail(t, tn);
                // 自旋
                continue;
            }
            // 超时直接返回 null
            if (timed && nanos <= 0)        // 等不及了
                return null;
            // 创建节点
            if (s == null)
                s = new QNode(e, isData);
            // 如果把 s 放到队尾失败,继续递归放进去
            if (!t.casNext(null, s))        // 链接失败
                continue;

            advanceTail(t, s);              // 推进 tail 节点并等待
            // 阻塞住自己,直到有其他线程与之匹配, 或它自己进行线程的中断
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                   // wait was cancelled
                clean(t, s); //  对接点 s 进行清除, 若 s 不是链表的最后一个节点, 则直接 CAS 进行 节点的删除, 若 s 是链表的最后一个节点, 则 要么清除以前的 cleamMe 节点(cleamMe != null), 然后将 s.prev 设置为 cleanMe 节点, 下次进行删除 或直接将 s.prev 设置为cleanMe
                return null;
            }

            if (!s.isOffList()) {           // 尚未取消链接
                advanceHead(t, s);          // unlink if head 推进head 节点, 下次就调用 s.next 节点进行匹配(这里调用的是 advanceHead, 因为代码能执行到这边说明s已经是 head.next 节点了)
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;
        // 队列不为空,并且当前操作和队尾不一致
        // 也就是说当前操作是队尾是对应的操作
        // 比如说队尾是因为 take 被阻塞的,那么当前操作必然是 put
        } else{
            // 如果是第一次执行,此处的 m 代表就是 tail
            // 也就是这行代码体现出队列的公平,每次操作时,从头开始按照顺序进行操作
            QNode m = h.next;               // node to fulfill
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   // m cancelled
                // m 代表栈头
                // 这里把当前的操作值赋值给阻塞住的 m 的 item 属性
                // 这样 m 被释放时,就可得到此次操作的值
                !m.casItem(x, e)) {         // lost CAS
                advanceHead(h, m);          // dequeue and retry
                continue;
            }
            // 当前操作放到队头
            advanceHead(h, m);              // successfully fulfilled
            // 释放队头阻塞节点
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

177、说说TransferStack的tansfer()方法实现

根据注释来看这个方法有三种情况:
1.如果显然是空的或已经包含相同模式的节点,尝试将节点压入堆栈并等待匹配,然后将其返回,如果取消则返回null。
2.如果显然包含互补模式的节点,尝试将一个满足条件的节点压入堆栈,与相应的等待节点匹配,从堆栈中弹出两者,然后返回匹配项。由于其他线程正在执行操作3,因此实际上可能不需要匹配或取消链接。
3.如果栈顶已经包含另一个实现节点,通过进行匹配和/或弹出操作来帮助解决问题,然后继续。帮助代码与实现代码基本相同,不同之处在于它不返回项目。
和TransferQueue的tansfer()的方法类似也有两种模式,一种是REQUEST一种是DATA

	@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
    SNode s = null; // constructed/reused as needed
    
    // e 为空: take 方法,非空: put 方法
    int mode = (e == null) ? REQUEST : DATA;
    
    // 自旋
    for (;;) {
        // 头节点情况分类
        // 1:为空,说明队列中还没有数据
        // 2:非空,并且是 take 类型的,说明头节点线程正等着拿数据
        // 3:非空,并且是 put 类型的,说明头节点线程正等着放数据
        SNode h = head;
        
        // 栈头为空,说明队列中还没有数据。
        // 栈头非空且栈头的类型和本次操作一致
        //	比如都是 put,那么就把本次 put 操作放到该栈头的前面即可,让本次 put 能够先执行
        if (h == null || h.mode == mode) {  // empty or same-mode
            // 设置了超时时间,并且 e 进栈或者出栈要超时了,
            // 就会丢弃本次操作,返回 null 值。
            // 如果栈头此时被取消了,丢弃栈头,取下一个节点继续消费
            if (timed && nanos <= 0) {      // 无法等待
                // 栈头操作被取消
                if (h != null && h.isCancelled())
                    // 丢弃栈头,把栈头的后一个元素作为栈头
                    casHead(h, h.next);     // 将取消的节点弹栈
                // 栈头为空,直接返回 null
                else
                    return null;
            // 没有超时,直接把 e 作为新的栈头
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                // e 等待出栈,一种是空队列 take,一种是 put
                SNode m = awaitFulfill(s, timed, nanos);
                if (m == s) {               // wait was cancelled
                    clean(s);
                    return null;
                }
                // 本来 s 是栈头的,现在 s 不是栈头了,s 后面又来了一个数,把新的数据作为栈头
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        // 栈头正在等待其他线程 put 或 take
        // 比如栈头正在阻塞,并且是 put 类型,而此次操作正好是 take 类型,走此处
        } else if (!isFulfilling(h.mode)) { // try to fulfill
            // 栈头已经被取消,把下一个元素作为栈头
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            // snode 方法第三个参数 h 代表栈头,赋值给 s 的 next 属性
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    // m 就是栈头,通过上面 snode 方法刚刚赋值
                    SNode m = s.next;       // m is s's match
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                     // tryMatch 非常重要的方法,两个作用:
                     // 1 唤醒被阻塞的栈头 m,2 把当前节点 s 赋值给 m 的 match 属性
                     // 这样栈头 m 被唤醒时,就能从 m.match 中得到本次操作 s
                     // 其中 s.item 记录着本次的操作节点,也就是记录本次操作的数据
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        s.casNext(m, mn);   // help unlink
                }
            }
        } else {                            // help a fulfiller
            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                casHead(h, null);           // pop fulfilling node
            else {
                SNode mn = m.next;
                if (m.tryMatch(h))          // help match
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

178、TransferQueue的clean()方法为什么不能直接清楚尾节点?
因为如何在清楚的过程中存在新的节点连接到了尾节点后面,直接清除尾节点会连带着新的节点一同被清除,所以不能,这也就是引入了cleanMe节点的意义

void clean(QNode pred, QNode s) {
    s.waiter = null; // forget thread
    // 如果pred.next!=s则说明s已经出队了
    while (pred.next == s) { // Return early if already unlinked
        QNode h = head;
        QNode hn = h.next;   // Absorb cancelled first node as head
        // 从队列头部开始遍历,遇到被取消的节点则将其出队 
        if (hn != null && hn.isCancelled()) {
            advanceHead(h, hn);
            continue;
        }
        QNode t = tail;      // Ensure consistent read for tail
        // t==h则队列为null
        if (t == h)
            return;
        QNode tn = t.next;
        if (t != tail)
            continue;
        // 帮助其他线程入队
        if (tn != null) {
            advanceTail(t, tn);
            continue;
        }
        // 只能出队非尾节点
        if (s != t) {        // If not tail, try to unsplice
            // 出队方式很简单,将pred.next指向s.next即可
            QNode sn = s.next;
            if (sn == s || pred.casNext(s, sn))
                return;
        }
        // 如果s是队尾元素,那么就需要cleanMe出场了,如果cleanMe==null,则只需将pred赋值给cleanMe即可,
        // 赋值cleanMe的意思是等到s不是队尾时再进行清除,毕竟队尾只有一个
        // 同时将上次的cleanMe清除掉,正常情况下此时的cleanMe已经不是队尾了,因为当前需要清除的节点是队尾
        // (上面说的cleanMe其实是需要清除的节点的前继节点)
        QNode dp = cleanMe;
        if (dp != null) {    // Try unlinking previous cancelled node
            QNode d = dp.next;
            QNode dn;
            // d==null说明需要清除的节点已经没了
            // d==dp说明dp已经被清除了,那么dp.next也一并被清除了
            // 如果d未被取消,说明哪里出错了,将cleanMe清除,不清除这个节点了
            // 后面括号将清除cleanMe的next出局,前提是cleanMe.next没有已经被出局
            if (d == null ||               // d is gone or
                d == dp ||                 // d is off list or
                !d.isCancelled() ||        // d not cancelled or
                (d != t &&                 // d not tail and
                 (dn = d.next) != null &&  //   has successor
                 dn != d &&                //   that is on list
                 dp.casNext(d, dn)))       // d unspliced
                casCleanMe(dp, null);
            // dp==pred说明cleanMe.next已经其他线程被更新了
            if (dp == pred)
                return;      // s is already saved node
        } else if (casCleanMe(null, pred))
            return;          // Postpone cleaning s
    }
}

179、说说TransferStack的clean()方法

void clean(SNode s) {
    s.item = null;   // forget item
    s.waiter = null; // forget thread
    // 清除
    SNode past = s.next;
    if (past != null && past.isCancelled())
        past = past.next;

    // Absorb cancelled nodes at head
    // 从栈顶节点开始清除,一直到遇到未被取消的节点,或者直到s.next
    SNode p;
    while ((p = head) != null && p != past && p.isCancelled())
        casHead(p, p.next);

    // Unsplice embedded nodes
    // 如果p本身未取消(上面的while碰到一个未取消的节点就会退出,但这个节点和past节点之间可能还有取消节点),
    // 再把p到past之间的取消节点都移除。
    while (p != null && p != past) {
        SNode n = p.next;
        if (n != null && n.isCancelled())
            p.casNext(n, n.next);
        else
            p = n;
    }
}

180、说说TransferStack是怎么等待的?
主要是调用了awaitFulfill()方法

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    long lastTime = (timed)? System.nanoTime() : 0;
    Thread w = Thread.currentThread();
    SNode h = head;
    // 计算自旋的次数,逻辑大致同TransferQueue
    int spins = (shouldSpin(s)?
                 (timed? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        if (w.isInterrupted())
            s.tryCancel();
        // 如果s的match不等于null,有三种情况:
        // a.等待被取消了,此时x==s
        // b.匹配上了,此时match==另一个节点,这种情况是trayMatch()方法改变的mathch值
        // c.线程被打断,会取消,此时x==s
        // 不管是哪种情况都不要再等待了,返回即可
        SNode m = s.match;
        if (m != null)
            return m;
        if (timed) {
            // 等待
            long now = System.nanoTime();
            nanos -= now - lastTime;
            lastTime = now;
            if (nanos <= 0) {
                s.tryCancel();
                continue;
            }
        }
        // 自旋
        if (spins > 0)
            spins = shouldSpin(s)? (spins-1) : 0;
        // 设置等待线程
        else if (s.waiter == null)
            s.waiter = w; // establish waiter so can park next iter
        // 等待
        else if (!timed)
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

181、说说TransferStack的匹配方法
主要是依靠tryMatch()这个方法
在这里插入图片描述
这个方法的作用是尝试匹配并且唤醒正在等待的节点
179、什么是LInkedTransferQueue
是一个由链表组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
180、transfer方法的作用
如果当前有消费者正在等待接受元素(消费者使用takle()方法或者带时间限制的poll())方法,transfer方法可以把生产者传入的元素立刻transfer给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
181、tryTransfer方法的作用
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接受元素,则返回false。和transfer方法不同的是tryTransfer方法无论消费者是否接受,方法立刻返回,而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的tryTransfer方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没有消费元素,则返回false,如果在超时时间消费了元素,则返回true。
182、LinkedBlockingDeque是什么?
是一个由链表结构组成的双向阻塞队列,支持从队列的两端插入和移除元素,在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。

同步工具

1、什么是原子类?
java 1.5引进原子类,具体在java.util.concurrent.atomic包下,atomic包里面一共提供了13个类,分为4种类型,分别是:原子更新基本类型,原子更新数组,原子更新引用,原子更新属性。原子类也是java实现同步的一套解决方案。
2、为什么需要原子类?

  1. 简单:操作简单,底层实现简单
  2. 高效:占用资源少,操作速度快
  3. 安全:在高并发和多线程环境下要保证数据的正确性

3、什么是LongAdder?
在存在高度竞争的条件下,LongAdder的性能会远远好于AtomicLong,不过会消耗更多空间。高度竞争当然是指在多线程条件下。

  • 该类可以理解为维护了若干个内部AtomicLong(cell),
    而最终值是所有cell相加值,那么并发修改时,会将压力分散到各个cell中。
  • 继承了Striped64类,主要代码由该类实现,该类内部维护了一个类似AtomicLong的简化的类Cell的数组
  • 对于分散并发到各元素的随机算法,该类用了Thread的probe属性(ThreadLocalRandom计算随机数也用到了该属性),将随机数 & cells.length -1 ,获取随机的下标
  • 该Cell数组属性初始为空,只维护另一个属性base(相当于单个Cell元素),当产生并发冲突(修改值时使用CAS方法失败),则放弃base,并将Cell数组扩充为2,
    然后依次进行2的幂等次的扩充,直到小于等于最接近 CPU核数的2的幂等(四核时为16)
  • 维护了一个自旋锁(通过UnSafe的CAS),用于操作base和Cell时的并发控制

4、LongAdder的add()步骤
在这里插入图片描述

  1. 如果cell数组不为空,或者使用CAS设置BASE值失败,表示正在竞争,将uncontended标志设为true
  2. 如果cell数组此时为空或者cell数组长度为1,或者求出的下标为空,或者再次尝试CAS失败,调用longAccumulate()方法
  3. longAccumulate()方法大概如下:在这里插入图片描述

5、什么是CountDownLatch?
CountDownlatch允许一个或多个线程等待其他线程完成操作。
6、CountDownLatch原理?
内部有一个Sync类,它继承于AQS,以及一个volatile变量,每当线程调用countDown()方法时就减一,当为0时就唤醒调用了await()方法的线程
7、CyclicBarrier是什么?
让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时(即线程调用await()方法的此时等于构造时传入的值时),屏障才会开门,所有被屏障兰姐的线程才会继续运行。
当然,也可以传入一个barrierAction,用于在线程到达屏障时,优先执行这个Action,方便复杂的业务场景
8、CyclicBarrier的应用场景?
可以用于多线程计算数据,最后合并计算结果的场景。
9、什么是Fork/Join框架?
Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结构的框架。
10、什么是工作窃取算法?
工作窃取算法是指某个线程从其他队列里窃取任务来执行。那么,为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如A线程负责处理A队列里的任务。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活。而在这时他们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通过使用双端队列。
11、工作窃取算法的优缺点?

  • 优点:充分利用线程进行并行计算,减少了线程间的竞争。
  • 缺点:在某些情况下还是有竞争,比如队列里只有一个任务,而且该算法会消耗更多的系统资源,比如创建多个线程和多个队列;

12、如何创建线程池?线程次有哪些参数?

  • corePoolSize:线程池基本大小。
  • maximumPoolSize:线程池最大数量,线程池允许创建的最大线程数,使用了无界任务队列就无效了。
  • keepAliveTime:线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。指的是非核心线程空闲时间达到的阈值会被回收。java核心线程池的回收由allowCoreThreadTimeOut参数控制,默认为false,若开启为true,则此时线程池中不论核心线程还是非核心线程,只要其空闲时间达到keepAliveTime都会被回收。
  • TimeUnit:线程活动保持时间,keepAliveTime的单位。
  • BlockingQueue:任务队列。
  • threadFactory:线程工厂。线程池在创建线程时通过调用线程工厂的Thread newThread(Runnable r)来创建线程。
  • handler:饱和策略。

13、ThreadFactory默认创建什么类型的线程?
默认线程工厂创建出的是一个非守护、优先级为Thread.NORM_PRIORITY 的线程。如果想要自己定制线程工厂满足需求,只需实现ThreadFactory接口的Thread newThread(Runnable r)方法。
14、线程池有哪些饱和策略?
JDK中的ThreadPoolExecutor类提供了4种不同的RejectedExecutionHandler实现:

  • AbortPolicy:默认的饱和策略,该策略抛出未检查(运行时异常)的RejectedExecutionException。
  • DiscardPolicy: 不执行任何操作,直接抛弃任务
  • CallerRunsPolicy: 在调用者线程中执行该任务
  • DiscardOldestPolicy: 丢弃阻塞队列中的第一个任务, 然后重新将该任务交给线程池执行

同样的,可以通过实现RejectedExecutionHandler接口自定义饱和策略。
15、Java线程池的生命周期?
Java线程池有5种不同的状态,分别为运行(RUNNING)、关闭(SHUTDOWN)、停止(STOP)、整理(TIDYING)、结束(TERMINATED)
AtomicInteger类型的变量ctl用高3位来表示当前线程池状态,低29位来表示当前的线程数。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

在ThreadPoolExecutor里由5个整型常量表示,每个整型常量的都由高3位表示状态:

  • RUNNING: 高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务
  • SHUTDOWN: 高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务。调用void shutdown()方法实现
  • STOP: 高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务。调用List shutdownNow()实现。
  • TIDYING: 高3位为010,当线程池关闭后阻塞队列的任务已完成或线程池停止,然后workerCount(当前线程数量)为0,线程池进入该状态后会调用terminated()方法进入TERMINATED状态。
  • TERMINATED: 高3位为011。

16、线程池执行任务的流程?
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值