做英文网站 赚美元,ppt制作软件免费模板,甘肃建筑人才网,网站开发流程中客户的任务是什么文章目录 背景常见集合线程安全性HashMap为什么线程不安全#xff1f;怎么保证HashMap线程安全 ConcurrentHashMap为什么线程安全代码中分析 小结 背景 面试的时候先会喊你说说集合#xff0c;那些集合线程不安全#xff1f;当你说了HashMap线程不安全#xff0c;面试官可能… 文章目录 背景常见集合线程安全性HashMap为什么线程不安全怎么保证HashMap线程安全 ConcurrentHashMap为什么线程安全代码中分析 小结 背景 面试的时候先会喊你说说集合那些集合线程不安全当你说了HashMap线程不安全面试官可能会进一步询问你是否了解 ConcurrentHashMap以及它是如何实现线程安全的。 常见集合线程安全性
ArrayList、LinkedList、TreeSet、HashSet、HashMap、TreeMap等都是线程不安全的。
HashTable是线程安全的。
HashMap为什么线程不安全
来看个例子
public static void main(String[] args) {HashMapString, Integer map new HashMap();// 创建两个线程同时向HashMap中添加1000个元素Thread thread1 new Thread(new Runnable() {Overridepublic void run() {for (int i 0; i 1000; i) {map.put(String.valueOf(i), i);}}});Thread thread2 new Thread(new Runnable() {Overridepublic void run() {for (int i 1000; i 2000; i) {map.put(String.valueOf(i), i);}}});// 启动线程thread1.start();thread2.start();try {// 等待两个线程执行完成thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}// 输出 HashMap 的大小System.out.println(map集合大小: map.size());}//输出结果map集合大小:1920这个数字在变动在多线程环境下如果多个线程同时对 HashMap 进行修改操作例如添加、删除元素可能会导致数据结构破坏进而引发各种问题比如丢失数据等。
怎么保证HashMap线程安全
第一 如何保证HashMap的线程安全呢可能你们想到了synchronized ,确实你可以通过在添加元素时使用 synchronized 来确保 HashMap 的线程安全性。这样做可以在多线程环境下保证对 HashMap 的操作是互斥的从而避免了多个线程同时修改 HashMap 导致的线程不安全问题。 Thread thread1 new Thread(new Runnable() {Overridepublic void run() {//synchronized (map) 中的 map 是一个对象锁//它指定了在执行同步代码块时使用的锁对象synchronized (map) {for (int i 0; i 1000; i) {map.put(String.valueOf(i), i);}}}});当一个线程进入同步代码块即 synchronized (map) 所包围的部分时它会尝试获取 map 对象的锁。如果这个锁当前没有被其他线程占用那么该线程将获得锁并可以执行同步代码块中的操作如果该锁已经被其他线程占用那么该线程将被阻塞直到锁被释放。被锁住的对象将会在同步代码块执行完毕后自动释放。
第二 使用Collections.synchronizedMap() 包装 MapInteger, String synchronizedMap Collections.synchronizedMap(new HashMap()); 这样可以获得一个线程安全的 HashMap但性能可能不如 ConcurrentHashMap。
第三 还可以用其他锁ReentrantReadWriteLock来控制多线程下对集合安全读写。可能有人又疑惑好像自己见过ReentrantLock 。ReentrantLock 是一个可重入的互斥锁而 ReentrantReadWriteLock 是一个可重入的读写锁。区别
互斥性 ReentrantLock 是一种独占锁同一时刻只允许一个线程获得锁其他线程必须等待该线程释放锁才能继续执行。 ReentrantReadWriteLock 包含了两种锁读锁和写锁。多个线程可以同时持有读锁但是只有一个线程可以持有写锁并且在持有写锁时不允许其他线程持有读锁或写锁。
读写分离 ReentrantReadWriteLock 的设计目的是为了提高读操作的并发性能。在读多写少的情况下读锁允许多个线程同时访问共享资源从而提高了系统的并发能力。 ReentrantLock 没有读写分离的特性它只是简单的互斥锁适用于那些需要严格互斥的场景。
第四 使用 ConcurrentHashMap ConcurrentHashMap 是专门为高并发环境设计的JDK 1.8它使用了 CAS synchronized 来保证线程安全性而且性能表现优秀。 MapInteger, String concurrentHashMap new ConcurrentHashMap();
ConcurrentHashMap为什么线程安全
ConcurrentHashMap 之所以是线程安全的主要是因为它在内部实现时采用了特殊的机制来确保多个线程同时访问和修改数据时不会发生冲突。
JDK 1.7 版本中的实现 ConcurrentHashMap 在 JDK 1.7 中使用了分段锁Segmentation的结构将整个哈希表分成了多个段Segment每个段有自己的锁。不同段之间的修改操作可以并发进行提高了并发性能只有在同一段内的操作才需要竞争锁。
JDK 1.8 版本中的优化 JDK 1.8 对 ConcurrentHashMap 进行了重大优化废弃了分段锁的设计而是采用了更细粒度的锁分离技术。 在 JDK 1.8 中ConcurrentHashMap 内部使用了基于 CASCompare and Swap 是一种原子操作用于在多线程环境下实现对共享数据的安全更新。CAS 是一种乐观锁机制可以避免使用传统的互斥锁提高了并发性能。 操作的 synchronized 关键字 同时ConcurrentHashMap 在 JDK 1.8 中引入了红黑树作为链表的替代结构当链表长度达到一定阈值时会将链表转换为红黑树以提高查找效率。这种优化又提高了 ConcurrentHashMap 的并发性能和吞吐量。
代码中分析
来看看put方法 public static void main(String[] args) {ConcurrentHashMapString, Integer map new ConcurrentHashMap();map.put(a,111);}先了解一下CAS 在 ConcurrentHashMap 中CAS 主要用于对节点的插入、更新和删除操作。这些操作涉及到对节点的创建、替换和删除需要确保线程安全不能出现多线程操作同一节点的情况。使用 CAS 可以保证节点的创建、替换和删除的原子性从而保证了线程安全。 在 JDK8 中ConcurrentHashMap 的实现方式已经改变不再采用分段锁的方式而是采用了 CASSynchronized 的方式来保证线程安全
我们需要先了解tabAt ,casTabAt(本质是CAS 算法看下面源码可知)的利用来保障线程安全的操作 tabAt 方法用于从哈希表数组 tab 中获取指定索引 i 处的节点数组。 casTabAt 方法用于原子性地将哈希表数组 tab 中指定索引 i 处的值从 c 更新为 v。 static final K,V boolean casTabAt(NodeK,V[] tab, int i,NodeK,V c, NodeK,V v) {return U.compareAndSwapObject(tab, ((long)i ASHIFT) ABASE, c, v);}put源码如下
public V put(K key, V value) {return putVal(key, value, false);}//下面会用到transient volatile NodeK,V[] table;/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) {if (key null || value null) throw new NullPointerException();int hash spread(key.hashCode());int binCount 0;for (NodeK,V[] tab table;;) {NodeK,V f; int n, i, fh;if (tab null || (n tab.length) 0)tab initTable();else if ((f tabAt(tab, i (n - 1) hash)) null) {//如果tab为空则尝试使用 casTabAt 方法原子地将新节点插入到tab中如果插入成功则退出循环。if (casTabAt(tab, i, null,new NodeK,V(hash, key, value, null)))break; // no lock when adding to empty bin}else if ((fh f.hash) MOVED)tab helpTransfer(tab, f);else {V oldVal null;synchronized (f) {if (tabAt(tab, i) f) {if (fh 0) {binCount 1;for (NodeK,V e f;; binCount) {K ek;if (e.hash hash ((ek e.key) key ||(ek ! null key.equals(ek)))) {oldVal e.val;if (!onlyIfAbsent)e.val value;break;}NodeK,V pred e;if ((e e.next) null) {pred.next new NodeK,V(hash, key,value, null);break;}}}else if (f instanceof TreeBin) {NodeK,V p;binCount 2;if ((p ((TreeBinK,V)f).putTreeVal(hash, key,value)) ! null) {oldVal p.val;if (!onlyIfAbsent)p.val value;}}}}if (binCount ! 0) {// 如果链表长度达到阈值默认8则将链表转化为红黑树以提高查找性能if (binCount TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal ! null)return oldVal;break;}}}addCount(1L, binCount);return null;}注意其中table,synchronized , treeifyBin(tab, i);
table 源码中赋值表达式NodeK,V[] tab table;其中table是什么呢定义table的源码 transient volatile NodeK,V[] table;其中使用了 transient 和 volatile 关键字进行修饰。 transient 关键字表示该变量不会被序列化。在对象被序列化时table 数组不会被保存这意味着在反序列化时需要重新构建 table 数组。
volatile 关键字表示该变量是易变的并且在多线程环境下可能会被多个线程同时修改。当一个线程修改了 table 数组时volatile 关键字可以确保对其他线程可见即其他线程能够立即看到 table 数组的更新而不会使用过期的或者缓存的值。
synchronized put方法里面其实调用了putVal。putVal中有 synchronized (f) {...}在链表中查找并插入新节点时首先对桶进行加锁然后遍历链表查找对应的键值对。如果找到对应的键值对则更新其值如果未找到则在链表末尾插入新节点。
treeifyBin(tab, i) 如果 binCount 大于等于预设的阈值 TREEIFY_THRESHOLD则将链表转换为红黑树
小结
而在 JDK 1.8 中ConcurrentHashMap 放弃了分段锁而是采用了更为精细的桶结构。每个桶可以独立加锁使得并发修改操作可以更细粒度地进行。此外当桶中的元素数量达到一定阈值时链表结构会转变为红黑树以减少搜索时间。这种锁分离技术提高了并发性能使得 ConcurrentHashMap 在并发情况下表现更加出色。它是通过 CAS synchronized 来实现线程安全的并且它的锁粒度更小查询性能也更高。 JDK 1.7 中ConcurrentHashMap 使用了分段锁的结构即将整个哈希表分成多个段Segment每个段有自己的锁。这样多个修改操作可以并发进行只要它们发生在不同的段上相应的段才会加锁。这种设计减少了不同线程之间的竞争提高了并发性能。 昨天面试被问到了当时回答支支吾吾今天总结了一下可能有不到位的。欢迎大家的指点
觉得有用点个赞