最近学习ConcurrentHashMap源码中发现其put时,如果当前table为空则会调用 initTable() 进行初始化。
跟踪initTable 方法发现:
initTable()
方法负责初始化内部哈希表的数组。这个方法是线程安全的,它使用了一种乐观锁机制来确保只有一个线程能够初始化哈希表。让我们逐步分析这个方法的工作流程:
-
首先,方法通过一个
while
循环检查当前的table
是否为null
或长度为0
,这表示哈希表尚未初始化。 -
如果
table
是未初始化的,它会检查sizeCtl
变量的值。这个变量用于控制表的初始化和扩容操作。如果sizeCtl
的值小于0
,这意味着另一个线程已经在初始化或者正在扩容哈希表。 -
当检测到
sizeCtl
小于0
时,当前线程调用Thread.yield()
。这是因为当前线程"输掉了初始化竞赛"(lost initialization race),即它发现有另一个线程已经在进行初始化操作。在这种情况下,当前线程通过yield()
让出CPU,希望给正在进行初始化的线程一个执行的机会,从而减少无谓的CPU竞争。 -
如果
sizeCtl
大于等于0
,当前线程尝试使用compareAndSwapInt
(CAS操作)来设置sizeCtl
的值为-1
,这是为了表示当前线程将负责初始化表。如果CAS操作成功,当前线程进入try
块进行初始化。 -
在
try
块中,再次检查table
是否未初始化,以防在CAS操作和这次检查之间有其他线程完成了初始化。如果table
仍然未初始化,它将创建一个新的数组,并将sizeCtl
设置为一个新的阈值,这个阈值是根据默认容量或者给定的初始容量计算出来的。 -
最后,
finally
块中sizeCtl
被设置为新的阈值,表示初始化完成,其他线程可以开始使用哈希表了。
为啥使用Thread.yield(); 因为在JDK 1.8中,ConcurrentHashMap
的initTable()
方法是用于初始化内部哈希表的。在这个方法中,如果检测到有其他线程也正在尝试初始化哈希表(通过检查sizeCtl
字段),当前线程会调用Thread.yield()
。这是一种启发式的方法,目的是为了减少资源的浪费和竞争,让出CPU时间片给其他可能正在运行的线程,以便它们能够完成初始化工作。
Thread.yield()
是一种提示调度器当前线程愿意放弃其当前的时间片,但是它并不保证会导致当前线程立即停止执行。调度器可能会忽略这个提示。在多数情况下,yield()
的作用是让相同优先级的其他线程有机会执行,但是它不会导致锁等资源的释放,也不会导致线程状态的改变。
与Thread.yield()
相比,Thread.sleep(0)
会导致当前线程暂停执行指定的时间(在这个例子中是0毫秒),并进入TIMED_WAITING
状态。即使是0毫秒,sleep(0)
也会请求调度器重新进行一次调度,但是它不会像yield()
一样仅仅是一个提示,而是确实会导致当前线程暂停。在sleep(0)
期间,其他线程可以利用CPU执行任务。
总的来说,Thread.yield()
是一种对调度器的非强制性提示,让出CPU以便其他线程可以运行,但是不保证会有任何效果。而Thread.sleep(0)
实际上会导致线程暂停执行,即使时间非常短,也会给其他线程执行的机会。
在ConcurrentHashMap
的initTable()
方法中,使用Thread.yield()
是为了在发现有其他线程正在进行初始化时,让当前线程暂时让出CPU,从而减少不必要的资源竞争,这是一种尝试使得初始化工作更高效的策略。
通过使用Thread.yield()
,ConcurrentHashMap
试图减少无谓的自旋等待,这在多线程环境中可能会导致较高的CPU消耗。如果不使用yield()
,那么在初始化哈希表时,其他线程将会在while
循环中忙等待,这将导致CPU资源的浪费,尤其是在高负载的服务器环境中。
总之,Thread.yield()
在这里是作为一种策略,用来提高多线程环境下的CPU使用效率,减少因自旋等待导致的资源浪费。