/hosee/blog/

的原理以及如何实现,之前在JDK7与JDK8中的实现中已经说明了。

那么,为什么说是线程不安全的呢?它在多线程环境下,会发生什么情况呢?

1. 死循环

我们都知道初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫,这个成本相当的大。

void (int ) {

Entry[] = table;

int = .;

if ( == ) {

= .;

Entry[] = new Entry[];

(, ());

table = ;

= (int)Math.min( * , + 1);

void (Entry[] , ) {

int = .;

for (Entry e : table) {

while(null != e) {

Entry next = e.next;

if () {

e.hash = null == e.key ? 0 : hash(e.key);

int i = (e.hash, );

e.next = [i];

[i] = e;

e = next;

大概看下:

对索引数组中的元素遍历

对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点。

循环2,直到链表节点全部转移

循环1,直到所有索引数组全部转移

经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以, 的死锁问题就出在这个()函数上。

1.1 单线程 详细演示

单线程情况下, 不会出现任何问题:

如图所示:

线程死锁的原因_线程死锁_线程死锁的四个必要条件

1.2 多线程 详细演示

为了思路更清晰,我们只将关键代码展示出来

while(null != e) {

Entry next = e.next;

e.next = [i];

[i] = e;

e = next;

Entry next = e.next;——因为是单链表,如果要转移头指针,一定要保存下一个结点,不然转移后链表就丢了

e.next = [i];——e 要插入到链表的头部线程死锁,所以要先用 e.next 指向新的 Hash 表第一个元素(为什么不加到新链表最后?因为复杂度是 O(N))

[i] = e;——现在新 Hash 表的头指针仍然指向 e 没转移前的第一个元素,所以需要将新 Hash 表的头指针指向 e

e = next——转移 e 的下一个结点

假设这里有两个线程同时执行了put()操作,并进入了()环节

while(null != e) {

Entry next = e.next; //线程1执行到这里被调度挂起了

e.next = [i];

[i] = e;

e = next;

那么现在的状态为:

线程死锁的四个必要条件_线程死锁的原因_线程死锁

从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 后,就指向了线程2 后的链表。

然后线程1被唤醒了:

执行e.next = [i],于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null,

执行[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。

执行e = next,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

现在的 e 节点是 key(7),首先执行Entry next = e.next,那么 next 就是 key(3)了

执行e.next = [i],于是key(7) 的 next 就成了 key(3)

执行[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)

执行e = next,将 e 指向 next,所以新的 e 是 key(3)

这时候的状态图为:

线程死锁的四个必要条件_线程死锁的原因_线程死锁

然后又该执行 key(7)的 next 节点 key(3)了:

现在的 e 节点是 key(3),首先执行Entry next = e.next,那么 next 就是 null

执行e.next = [i],于是key(3) 的 next 就成了 key(7)

执行[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)

执行e = next,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:

线程死锁_线程死锁的原因_线程死锁的四个必要条件

很明显,环形链表出现了!!当然线程死锁,现在还没有事情,因为下一个节点是 null,所以()就完成了,等put()的其余过程搞定后, 的底层实现就是线程1的新 Hash 表了。

2. fail-fast

如果在使用迭代器的过程中有其他线程修改了map,那么将抛出,这就是所谓fail-fast策略。

这个异常意在提醒开发者及早意识到线程安全问题,具体原因请查看的原因以及解决措施

顺便再记录一个的问题:

为什么, 这样的类适合作为键? , 这样的类作为的键是再适合不过了,而且最为常用。因为是不可变的,也是final的,而且已经重写了()和()方法了。其他的类也有这个特点。不可变性是必要的,因为为了要计算(),就要防止键值改变,如果键值在放入时和获取时返回不同的的话,那么就不能从中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证是不变的,那么请这么做吧。因为获取对象的时候要用到()和()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的的话,那么碰撞的几率就会小些,这样就能提高的性能。


限时特惠:
本站持续每日更新海量各大内部创业课程,一年会员仅需要98元,全站资源免费下载
点击查看详情

站长微信:Jiucxh

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注