多线程之ConcurrentHashMap原理

多线程之ConcurrentHashMap原理

1 关于JDK7HashMap死锁

要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了.

测试类

public static void main(String[] args) {
     // 测试 java 7 中哪些数字的 hash 结果相等
     System.out.println("长度为16时,桶下标为1的key");
     for (int i = 0; i < 64; i++) {
         if (hash(i) % 16 == 1) {
             System.out.println(i);
         }
     }
     System.out.println("长度为32时,桶下标为1的key");
     for (int i = 0; i < 64; i++) {
         if (hash(i) % 32 == 1) {
             System.out.println(i);
         }
     }
     // 1, 35, 16, 50 当大小为16时,它们在一个桶内
     final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
     // 放 12 个元素
     map.put(2, null);
     map.put(3, null);
     map.put(4, null);
     map.put(5, null);
     map.put(6, null);
     map.put(7, null);
     map.put(8, null);
     map.put(9, null);
     map.put(10, null);
     map.put(16, null);
     map.put(35, null);
     map.put(1, null);
     System.out.println("扩容前大小[main]:"+map.size());
     new Thread() {
         @Override
         public void run() {
         // 放第 13 个元素, 发生扩容
         map.put(50, null);
         System.out.println("扩容后大小[Thread-0]:"+map.size());
         }
     }.start();
     new Thread() {
         @Override
         public void run() {
         // 放第 13 个元素, 发生扩容
         map.put(50, null);
         System.out.println("扩容后大小[Thread-1]:"+map.size());
         }
     }.start();
}
final static int hash(Object k) {
     int h = 0;
     if (0 != h && k instanceof String) {
         return sun.misc.Hashing.stringHash32((String) k);
     }
     h ^= k.hashCode();
     h ^= (h >>> 20) ^ (h >>> 12);
     return h ^ (h >>> 7) ^ (h >>> 4);
}

死锁复现步骤

在idea中断点都指定位Thread模式.

1 在 HashMap 源码 590 行(如下)加断点

int newCapacity = newTable.length;

断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来

newTable.length==32 &&
 (
 Thread.currentThread().getName().equals("Thread-0")||
 Thread.currentThread().getName().equals("Thread-1")
 )

运行结果:

长度为16时,桶下标为1的key 
1 
16 
35 
50 
长度为32时,桶下标为1的key 
1 
35 
扩容前大小[main]:12 

扩容流程

1 在 HashMap 源码 594 行加断点

Entry<K,V> next = e.next; // 593
if (rehash) // 594

了观察 e 节点和 next 节点的状态,Thread-0 单步执行到 594 行,再 594 处再添加一个断点.

Thread.currentThread().getName().equals("Thread-0")

可以在 Variables 面板观察到 e 和 next 变量,使用 view as -> Object 查看节点状态.

e (1)->(35)->(16)->null 
next (35)->(16)->null 

在 Threads 面板选中 Thread-1 恢复运行,可以看到控制台输出新的内容如下,Thread-1 扩容已完成

newTable[1] (35)->(1)->null 
newTable[1] (35)->(1)->null 

Thread-0 还停在 594 处, Variables 面板变量的状态已经变化为

e (1)->null 
next (35)->(1)->null 

因为 Thread-1 扩容时链表也是后加入的元素放入链表头,因此链表就倒过来了,但 Thread-1 虽然结 果正确,但它结束后 Thread-0 还要继续运行

下一轮循环到 594,将 e 搬迁到 newTable 链表头.

newTable[1] (1)->null 
e (35)->(1)->null 
next (1)->null 

下一轮循环到 594,将 e 搬迁到 newTable 链表头

newTable[1] (35)->(1)->null 
e (1)->null 
next null 

源码

e.next = newTable[1];
// 这时 e (1,35)
// 而 newTable[1] (35,1)->(1,35) 因为是同一个对象
newTable[1] = e; 
// 再尝试将 e 作为链表头, 死链已成
e = next;
// 虽然 next 是 null, 会进入下一个链表的复制, 但死链已经形成了

源码说明

HashMap 的并发死链发生在扩容时.

// 将 table 迁移至 newTable
void transfer(Entry[] newTable, boolean rehash) { 
     int newCapacity = newTable.length;
     for (Entry<K,V> e : table) {
         while(null != e) {
         Entry<K,V> next = e.next;
         // 1 处
         if (rehash) {
             e.hash = null == e.key ? 0 : hash(e.key);
         }
         int i = indexFor(e.hash, newCapacity);
         // 2 处
         // 将新元素加入 newTable[i], 原 newTable[i] 作为新元素的 next
         e.next = newTable[i];
         newTable[i] = e;
         e = next;
         }
     }
}

说明:

原始链表,格式:[下标] (key,next)
[1] (1,35)->(35,16)->(16,null)
    
线程 a 执行到 1 处 ,此时局部变量 e 为 (1,35),而局部变量 next 为 (35,16) 线程 a 挂起
    
线程 b 开始执行
第一次循环
[1] (1,null)
    
第二次循环
[1] (35,1)->(1,null)
    
第三次循环
[1] (35,1)->(1,null)
[17] (16,null)
    
切换回线程 a,此时局部变量 e 和 next 被恢复,引用没变但内容变了:e 的内容被改为 (1,null),而 next 的内
容被改为 (35,1) 并链向 (1,null)
第一次循环
[1] (1,null)
    
第二次循环,注意这时 e 是 (35,1) 并链向 (1,null) 所以 next 又是 (1,null)
[1] (35,1)->(1,null)
    
第三次循环,e 是 (1,null),而 next 是 null,但 e 被放入链表头,这样 e.next 变成了 352 处)
[1] (1,35)->(35,1)->(1,35)
    
已经是死链

死锁原因: 在多线程环境下使用了非线程安全的 map 集合

此外, JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能 够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: maps.newconcurrentmap() 是一个 Go 语言中的函数,用于创建一个并发安全的 map(映射表)。在多个 goroutine(协程)同时访问该 map 时,可以避免出现数据竞争(race condition)等并发问题。 ### 回答2: maps.newconcurrentmap()是一个在Google Guava库中提供的方法。这个方法用于创建一个基于哈希表实现的线程安全的ConcurrentMap对象。ConcurrentMapJava中的一个接口,用于表示一组键值对,其中的操作都是线程安全的。线程安全的ConcurrentMap多线程环境下可以很好地控制访问共享资源的顺序和方式。 在ConcurrentMap中,和HashMap一样,每个键都需要是唯一的。通过使用哈希表,ConcurrentMap可以很快地定位到对应的值,而且由于它是线程安全的,它可以在并发场景下高效地进行插入、删除和读取操作。在Java中,ConcurrentMap多线程编程场景中是非常常用的一种容器。 Google Guava库的maps.newconcurrentmap()方法可以用来创建一个线程安全的ConcurrentMap。这个方法返回的对象是一个ConcurrentMapBuilder实例,它可以用来配置ConcurrentMap的一些参数,比如初始容量和负载因子等。ConcurrentMapBuilder还提供了一些其他的方法,比如withConcurrencyLevel()来指定并发级别,或者weakKeys()和weakValues()来使用弱引用来管理键或值对象。 maps.newconcurrentmap()提供了一种快速创建线程安全的ConcurrentMap的方法,而且它还可以用来定制化ConcurrentMap的一些参数。如果需要在多线程编程场景中使用ConcurrentMap,那么使用maps.newconcurrentmap()方法来创建对象是一个很好的选择。 ### 回答3: maps.newConcurrentMap()是Golang中的一种并发安全的Map结构,它可以在多个goroutine同时读写时保证线程安全。当我们需要在高并发场景下使用Map时,我们可以使用maps.newConcurrentMap()以避免出现竞争条件和其它并发问题。 在Golang中,Map是一种非常常用的数据结构,可以用来将key-value按照key排序并存储在一个容器中。然而,Map是非线程安全的数据结构,而当多个goroutine并发访问同一个Map时,可能会出现多种并发问题,例如竞争条件、死锁和数据丢失等问题。为了解决这些问题,我们可以使用maps.newConcurrentMap()。 当我们使用maps.newConcurrentMap()时,它会返回一个与Map类似的并发安全的结构。它支持并发读写,而不会导致死锁、竞争条件和数据丢失等问题。即使在高并发环境下,它也可以提供更好的性能和稳定性。 当我们需要在Golang中使用Map,并且我们的程序中需要进行并发操作时,就可以使用maps.newConcurrentMap()。而由于该结构是并发安全的,不会出现竞争条件和其它并发问题,所以我们可以在任何需要使用Map的场景中使用它,例如Web服务、后端任务队列、数据库连接池等。 总之,maps.newConcurrentMap()是Golang中的一个非常有用的数据结构,它可以帮助我们解决多线程Map的并发问题,并为高并发场景下的程序提供更好的性能和稳定性。因此,我们在需要使用Map时,应该优先考虑使用该结构来保证程序的稳定性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值