首页 >> 大全

除了加锁,Hashtable和HashMap还有不同吗?

2023-09-19 大全 30 作者:考证青年

如何保证线程安全

一般有三种方式来代替原生的线程不安全的 :

1)使用 java.util. 类的 方法包装一下 ,得到线程安全的 ,其原理就是对所有的修改操作都加上 。方法如下:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 

2)使用线程安全的 类代替,该类在对数据操作的时候都会上锁,也就是加上

3)使用线程安全的 类代替,该类在 JDK 1.7 和 JDK 1.8 的底层原理有所不同,JDK 1.7 采用数组 + 链表存储数据,使用分段锁 保证线程安全;JDK 1.8 采用数组 + 链表/红黑树存储数据,使用 CAS + 保证线程安全。

不过**前两者的线程并发度并不高,容易发生大规模阻塞,**所以一般使用的都是 ,他的性能和效率明显高于前两者。

具体是如何保证线程安全?

一般我们会使用来创建一个线程安全的Map

Map m = Collections.synchronizedMap(new HashMap(...));

中的这个静态方法 其实是创建了一个内部类的对象,这个内部类就是 。在其**内部维护了一个普通的 Map 对象以及互斥锁 mutex,**如下图所示:

可以看到 有两个构造函数,如果你传入了互斥锁 mutex 参数,就使用我们自己传入的互斥锁。如果没有传入,则将互斥锁赋值为 this,也就是将调用了该构造函数的对象作为互斥锁,即我们上面所说的 Map。

创建出 对象之后,通过源码可以看到对于这个对象的所有操作全部都是上了悲观锁 的:

由于多个线程都共享同一把互斥锁,导致同一时刻只能有一个线程进行读写操作,而其他线程只能等待,所以虽然它支持高并发,但是并发度太低,多线程情况下性能比较低下。

而且,大多数情况下,业务场景都是**读多写少,**多个线程之间的读操作本身其实并不冲突,所以 极大的限制了读的性能。

所以多线程并发场景我们很少使用 。

为什么不使用

和 一样, 也是非常粗暴的给每个方法都加上了悲观锁 ,我们随便找几个方法看看:

除了加锁,和还有不同吗?

Hash table的key和value不支持null,但是是支持key和value为null的

1)如果我们 put 了一个 value 为 null 进入 Map, 会直接抛空指针异常:

2)如果我们put一个key为null进入Map,在调用下面这个方法时就会报错,因为我们使用null值去调用方法了

那么为什么支持key和value为null呢?

1) 相比 做了一个特殊的处理,如果我们put进来的key是null,那么在计算这个key的hash值时会直接返回0

也就是说 中 key为 null 的键值对的 hash 为 0。因此一个 对象中只会存储一个 key 为 null 的键值对,因为它们的 hash 值都相同。

2)因为不会对put进来的value作检验,所以如果我们put进来的value值为null也没关系,因此一个对象可以存储多个value为null的键值对

但是有一个点要注意,并不是说调用get方法返回null就代表这个值在里,我们来看一下源码

如果 Map 中没有查询到这个 key 的键值对,那么 get 方法就会返回 null 对象。但是我们上面刚刚说了, 里面可以存在多个 value 为 null 的键值对,也就是说,通过 get(key) 方法返回的结果为 null 有两种可能:

1)没有查询到对应的键值对,返回null

2) 中这个 key 对应的 value 为 null

因此我们不能使用get方法来判断是否存在某个key,而是应该使用

方法。

为什么不支持 key 和 value 为 null 呢?

不仅仅不支持key和value为null,也不支持,作为支持并发的容器,如果它们像 一样,允许 null key 和 null value 的话,在多线程环境下会出现问题。

假设它们允许 null key 和 null value,我们来看看会出现什么问题:当你通过 get(key) 获取到对应的 value 时,如果返回的结果是 null 时,你无法判断这个 key 是否真的存在。为此,我们需要调用 方法来判断这个 key 到底是 value = null 还是它根本就不存在,如果 方法返回的结果是 true,OK,那我们就可以调用 map.get(key) 获取 value。

但是注意,这仅仅是在单线程的情况下!!

由于 和 是支持多线程的容器,在调用 map.get(key) 的这个时候 map 对象可能已经不同了。

比如说某个线程 A 调用了 map.get(key) 方法,它返回为 value = null 的真实情况就是因为这个 key 不能存在。当然,线程 A 还是会按部就班的继续用 map.(key),我们期望的结果是返回 false。

但是如果在线程 A 调用 map.get(key) 方法之后,map. 方法之前,另一个线程 B 执行了 map.put(key,null) 的操作。那么线程 A 调用的 map. 方法返回的就是 true 了。这就与我们的假设的真实情况不符合了。

所以为了保证并发情况的安全性, 和 不允许 key 和 value 为 null

和的区别

除了 不允许 null key 和 null value 而 允许以外,它俩还有以下几点不同:

1)初始化容量不同: 的初始容量为 16, 初始容量为 11。两者的负载因子默认都是 0.75;

2)扩容机制不同:当现有容量大于总容量 * 负载因子时, 扩容规则为当前容量翻倍, 扩容规则为当前容量翻倍 + 1;

3)迭代器不同:首先, 的迭代器 是fail-fast(快速失败)的,而的迭代器是fail-safe(失败安全)的

fail-fast原理

当迭代器在遍历容器中的元素时,会维护一个变量,在遍历集合过程中,如果某个元素发生变化,的值也会发生改变。这就是为什么是线程不安全的。

因为在使用()/next()方法时,首先会拿元素的和一个值比较,如果两者相等就返回这个元素的值,否则返回一个错误。

(**这里异常的抛出条件是检测到 != 这个条件。**如果集合发生变化时修改值刚好又设置为了值,则异常不会抛出。这就是为什么线程不安全)

fail-safe原理

和fail-fast不同,fail-safe机制在遍历集合元素时,如果此时对集合结构修改,**fail-safe机制会先重新复制一份原集合的元素出来,然后迭代器遍历的是这个原集合的副本!**这也是为什么支持多线程了。但是fail-safe也有缺点:

1.复制原集合需要额外的空间和时间开销

2.无法保证遍历的是最新的内容。

.7

存储结构

Java 7 中 的存储结构如上图, 由很多个 组合,而每一个 是一个类似于 的结构,所以每一个 的内部可以进行扩容。但是 的个数一旦初始化就不能改变,默认 的个数是 16 个,你也可以认为 默认支持最多 16 个线程并发。

初始化

通过 的无参构造探寻 的初始化流程。

    /*** Creates a new, empty map with a default initial capacity (16),* load factor (0.75) and concurrencyLevel (16).*/public ConcurrentHashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);}

无参函数中调用了有参构造,总共有三个参数

    /*** 默认初始化容量*/static final int DEFAULT_INITIAL_CAPACITY = 16;/*** 默认负载因子*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 默认并发级别*/static final int DEFAULT_CONCURRENCY_LEVEL = 16;

总结一下在 Java 7 中 的初始化逻辑。

1.必要参数校验。

2.校验并发级别 大小,如果大于最大值,重置为最大值。无参构造默认值是 16.

3.寻找并发级别 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16。

4.记录 偏移量,这个值为【容量 = 2 的N次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - = 28.

5.记录 ,默认是 ssize - 1 = 16 -1 = 15.

6.初始化 [0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了