HashTable,HashMap和ConcurrentHashMap的区别?

目标:

理解ConcurrentHashMap的好处,掌握ConcurrentHashMap的使用,理解ConcurrentHashMap的底层原理

引入

1.为什么要使用ConcurrentHashMap呢,有什么好处?
2.使用ConcurrentHashMap有什么要注意的?

详解

  • 引入 ConcurrentHashMap 是为了在同步集合HashTable之间有更好的选择; HashTable 与 HashMap 、ConcurrentHashMap 主要的区别在于HashMap不是同步的线程不安全的不适合应用于多线程并发环境下,而 ConcurrentHashMap 是线程安全的集合容器
  • HashMap是Java Collection Framework的重要成员,也是Map体系中我们最为常用的一种双列集合。不过遗憾的是,HashMap不是线程安全的。也就是说,在多线程环境下,操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容在哈希时出现的死循环问题,脏读问题等。HashMap的这一缺点往往会造成诸
    多不便,虽然在并发场景下Hashtable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable,
java.io.Serializable {
 
public synchronized V put(K key, V value) {
    // 省略
 }
public synchronized V remove(Object key) {
   
 }
public synchronized V get(Object key) {
    // 省略
 }
public synchronized int size() {
    return count;
 }
}

从上面的源码可以看出,Hashtable操作数据的所有方法都是同步方法,都使用了锁,使用当前对象作为锁,如果同一时刻只能有一个线程操作Hashtable,其他线程会被阻塞等待。

  • 我们再来看看Collections.synchronizedMap(Map<K,V> m) 的源码
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
  return new SynchronizedMap<>(m);
}

SynchronizedMap源码

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private final Map<K,V> m;
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;  // 使用当前对象作为锁
}
public int size() {
synchronized (mutex) {return m.size();}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
}

通过SynchronizedMap的源码我们也发现SynchronizedMap使用当前对象作为锁,同一时刻只能有一个线程操作SynchronizedMap,其他线程还是会被阻塞。效果如下图
两个线程同时在跑Hashtable和SynchronizedMap使用synchronized来保证线程安全,使用当前对象作为锁。在线程竞争激烈的情况下效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法可能会进入阻塞或轮询状态。如线程A使用put进行添加元素,线程B不但不能使用put方法添加元素,并且也不能使用get方法
来获取元素,所以竞争越激烈效率越低。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 ConcurrentHashMap 。在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可支持16个线程执行并发写操作,及任意数量线程的读操作

  • ConcurrentHashMap继承的结构图:
    ConcurrentHashMap继承的结构图

  • 掌握使用ConcurrentHashMap的使用

  • 需求:使用2个线程,每个线程循环5次,每循环一次让map的value的值增加1

  • 使用HashMap实现:

// 需求使用2连个线程,每个线程循环5次,每循环一次让Map的value增加1
private static void testHashMap() {
  final Map<String, Integer> count = new HashMap<>();
  final CountDownLatch endLatch = new CountDownLatch(2);
  Runnable task = new Runnable() {
    @Override
    public void run() {
      for (int i = 0; i < 5; i++) {
        Integer value = count.get("a");
        if (null == value) {
          count.put("a", 1);
       } else {
          count.put("a", value + 1);
       }
     }
      System.out.println("xx");
      endLatch.countDown();
   }
 };
  new Thread(task).start();
  new Thread(task).start();
  try {
    endLatch.await();
    System.out.println(count);
 } catch (Exception e) {
    e.printStackTrace();
 }
}

多次运行,结果不一样,testHashMap方法是两个线程操作HashMap,意图将value变为10。但是,因为多个线程用相同的key调用时,很可能会覆盖相互的结果,造成记录的次数比实际出现的次数少。
在这里插入图片描述

  • 使用ConcurrentHashMap实现
private static void testConcurrentHashMap() {
  final Map<String, Integer> count = new ConcurrentHashMap<>();
  final CountDownLatch endLatch = new CountDownLatch(2);
  Runnable task = new Runnable() {
    @Override
    public void run() {
      Integer oldValue, newValue;
      for (int i = 0; i < 5; i++) {
while (true) {
          oldValue = count.get("a");
          if (null == oldValue) {
            newValue = 1;
            if (count.putIfAbsent("a", newValue) == null) { // 第一次的
时候次数设置成1
              break;
           }
         } else {
            newValue = oldValue + 1; // 后面每次数量+1
            if (count.replace("a", oldValue, newValue)) {
              break;
           }
         }
       }
     }
      System.out.println("xx");
      endLatch.countDown();
   }
 };
  new Thread(task).start();
  new Thread(task).start();
  try {
    endLatch.await();
    System.out.println(count);
 } catch (Exception e) {
    e.printStackTrace();
 }
}

多次执行的结果:
多次执行的结果都是10
{a=10}

  • ConcurrentHashMap底层原理是什么
锁分段技术 Hashtable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问Hashtable的线
程都必须竞争同一把锁。假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访
问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一
把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

在这里插入图片描述

  • ConcurrentHashMap的put方法源码
public V put(K key, V value) {
  if (value == null)
    throw new NullPointerException();
  int hash = hash(key.hashCode());
  return segmentFor(hash).put(key, hash, value, false);
}

ConcurrentHashMap的put操作被ConcurrentHashMap委托给特定的 Segment 段来实现。也就是说,当我们向ConcurrentHashMap中put一个Key/Value对时,首先会获得Key的哈希值并对其再次哈希,然后根据最终的hash值定位到这条记录所应该插入的段,定位段的segmentFor()方法源码如下:

final Segment<K,V> segmentFor(int hash) {
   return segments[(hash >>> segmentShift) & segmentMask];
}

根据key的hash值的高n位就可以确定元素到底在哪一个Segment中。紧接着,调用这个段的put()方法来将目标Key/Value对插入到Segment段中,Segment段的put()方法的源码如下所示:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
   lock(); // 上锁
   try {
     int c = count;
     if (c++ > threshold) // ensure capacity
       rehash();
     HashEntry<K,V>[] tab = table;  // table是Volatile的
     int index = hash & (tab.length - 1);  // 定位到段中特定的桶
     HashEntry<K,V> first = tab[index];  // first指向桶中链表的表头
     HashEntry<K,V> e = first;
     // 检查该桶中是否存在相同key的节点
     while (e != null && (e.hash != hash || !key.equals(e.key))) 
       e = e.next;
     V oldValue;
     if (e != null) { // 该桶中存在相同key的节点
       oldValue = e.value;
       if (!onlyIfAbsent)
         e.value = value; // 更新value值
    }else { // 该桶中不存在相同key的节点
       oldValue = null;
       ++modCount;
       tab[index] = new HashEntry<K,V>(key, hash, first, value); // 创建HashEntry
并将其链到表头
       count = c;
    }
     return oldValue; // 返回旧值(该桶中不存在相同key的结点,则返回null)
  } finally {
     unlock(); // 在finally子句中解锁
  }
}
  • ConcurrentHashMap的get方法源码 当我们从ConcurrentHashMap中查询一个指定Key的键值对时,首先会
    定位其应该存在的段,然后查询请求委托给这个段进行处理,源码如下:
public V get(Object key) {
  int hash = hash(key.hashCode());
  return segmentFor(hash).get(key, hash);
}
  • Segment中get操作的源码:
V get(Object key, int hash) {
  if (count != 0) {  // read-volatile,首先读count变量
    HashEntry<K,V> e = getFirst(hash);  // 获取桶中链表头结点
    while (e != null) {
      if (e.hash == hash && key.equals(e.key)) {  // 查找链中是否存在指定Key的键
值对
        V v = e.value;
        if (v != null)  // 如果读到value域不为 null,直接返回
          return v; 
        return readValueUnderLock(e); // recheck
     }
      e = e.next;
      }
 }
  return null;  // 如果不存在,直接返回null
}
  • 下面我们来看看ConcurrentHashMap并发读写的几种情形:

  • 不同Segment的写入是可以并发执行的。
    在这里插入图片描述

  • 同一Segment的写和读是可以并发执行的。
    在这里插入图片描述

  • Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。
    在这里插入图片描述ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

  • ConcurrentHashMap 存取小结:
    在ConcurrentHashMap进行存取时,首先会定位到具体的段,然后通过对具体段的存取来完成对整个ConcurrentHashMap的存取。特别地,无论是ConcurrentHashMap的读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问

  • ConcurrentHashMap注意事项
    ConcurrentHashMap是并发效率更高的Map,实际上,并发执行Concurrent-HashMap只能保证自身的数据不被破坏,但无法保证业务的行为是否正确。错误的理解这里的线程安全,不恰当的使用ConcurrentHashMap,往往会导致出现问题。如果只调用get(),或只调用put()时,ConcurrentHashMap是线程安全的。但是在你调用完get()后,调用put()之前,如果有另外一个线程调用了map.put(name, x),你再去执行map.put(name,x),就很可能把前面的操作结果覆盖掉了。所以,即使在线程安全的情况下,还是有可能违反原子(atom)操作的规则。

  • ConcurrentHashMap只能保证写是同步的,不能保证先读后写的原子性。

  • 总结
    Hashtable和SynchronizedMap使用synchronized来保证线程安全,使用当前对象作为锁,同一时刻只能有一个线程操作,其他线程会被阻塞,所以竞争越激烈效率越低。ConcurrentHashMap无论是读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响对其它段的访问。 ConcurrentHashMap只能保证单个方法是同步的,不能保证先读后写的原子性

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值