JDK1.8之前HashMap由数组+链表组成 ,JDK1.8以在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转 化为红黑树,以减少搜索时间
链表长度不超过8
链表长度超出8 且HashMap数组长度超出64(默认是64)
HashMap put的流程 map.put(key,value);
int hashCode = hash(key);
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
(n - 1) & hash
n为数组长度 所以 当hashMap数组扩容后 key的数组下标值会变动
6
步。6
步。6
步。重新组装
存放到新的数组内。与大多数集合类一样,此类不同步。可以使用 Collections.synchronizedMap 方法构造同步的HashMap。Map m = Collections.synchronizedMap(new HashMap(...));
,为什么说HashMap是线程不安全的呢?
线程不安全的情况,多个线程给map put数据
图文表示:
线程A 找到下标2的位置,看没有值 创建一个nodeA
线程A被挂起,线程B 查询 下标2 没有数据,创建节点 NodeB
线程A获取资源,直接把nodeA放入桶内
线程B ,一直认为索引2的桶没有数据 ,忽略已有的链表数据 直接把nodeB节点覆盖放入桶内
案例测试代码
public class HashMapTest {
static HashMap map = new HashMap();
public static void main(String[] args) {
new Thread(() -> {
System.out.println("线程1 执行");
for (int i = 0; i < 8000; i++) {
map.put(i,i);
}
System.out.println("线程1---------"+map.get(500));
}).start();
new Thread(() -> {
System.out.println("线程2 执行");
for (int i = 8000; i < 16000; i++) {
map.put(i,i);
}
System.out.println("线程2---------"+map.get(1500));
}).start();
}
}
多次执行结果
线程1 执行
线程2 执行
线程2---------null
线程1---------null
线程1 执行
线程2 执行
线程1---------500
线程2---------1500
线程1 执行
线程2 执行
线程1---------null
线程2---------1500
线程1 执行
线程2 执行
线程2---------1500
线程1---------null
线程1 执行
线程2 执行
线程1---------500
线程2---------null
public class HashMapTest {
public static void main(String[] args) {
Map map = Collections.synchronizedMap(new HashMap());
new Thread(() -> {
System.out.println("线程1 执行");
for (int i = 0; i < 80000; i++) {
map.put(i,i);
}
System.out.println("线程1---------"+ map.get(500));
}).start();
new Thread(() -> {
System.out.println("线程2 执行");
for (int i = 80000; i < 160000; i++) {
map.put(i,i);
}
System.out.println("线程2---------"+ map.get(80020));
}).start();
}
}
和hashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
和hashMap 差不多相同 但是没有红黑树 ,具体的看代码。
HashTable 内的方法是同步的 synchronized ,当多个线程同时put 或remove 时
多线程案例,把创建的HashTable对象 加锁,缺点:没加锁之前多个线程 put key的数组下标索引不一样时,同时往数组添加数据没有影响 ,整个对象加锁后 要等上个线程释放锁后才能操作put
hashTable锁加的只加
public class HashTableTest {
static Hashtable<Integer,Integer> map = new Hashtable();
public static void main(String[] args) {
new Thread(() -> {
System.out.println("线程1 执行");
for (int i = 0; i < 8000; i++) {
map.put(i,i);
}
System.out.println("线程1---------"+map.get(500));
}).start();
new Thread(() -> {
System.out.println("线程2 执行");
for (int i = 8000; i < 16000; i++) {
map.put(i,i);
}
System.out.println("线程2---------"+map.get(1500));
}).start();
}
}
执行结果
线程相对安全的
线程1 执行
线程2 执行
线程2---------1500
线程1---------500
1.默认情况下会有16个区段 Segment数组 Segment[16]
2.每次每个区段Segment中会保存若干个散列桶,每次散列桶长度扩容成2^n次方的长度。 多个散列桶相连就构成了散列表。
3.存入元素: key带入到hashcode方法众获得hashcode值,然后把hashcode值带入到散列算法中获取segment的下标(区段编号),再根据key带入到定义好的函数中获取Segment对象中散列桶的下标。
如果此位置有元素就构成链表(JDK1.8及以后会形成红黑树),如果没有元素就存入
3.存取的线程安全问题: 如果多个线程操作同一个Segment,则某个线程给此Segment加锁,另一个线程只能阻塞。
同时解决了HashTable的问题,HashTable只能由一个线程操作。 ConcurrentHashMap可以让一个线程操作第一个Segment,另一个线程操作另一个Segment。
4.小矩形块表示散列桶
绿色的Segment表示ConcurrentHashMap集合众 Segment[16]数组里的一个对象。
5.并发问题:
两个线程给不同的区段Segment中添加元素,这种情况可以并发。 所以ConcurrentHashMap可以保证线程安全(多个线程操作同一个Segment,则某个线程给此Segment加锁,另一个线程只能阻塞)并且在一定程度上提交线程并发执行效率。
两个线程给同一个区段Segment中添加元素,这种情况不可以并发, 这样JDK1.8进行了改进:
没有区段了,和HashMap一致了,数组+链表+红黑树 +乐观锁 + synchronized
省略 下次补上
与大多数集合类一样,此类不同步。可以使用 Collections.synchronizedMap 方法构造同步的 WeakHashMap。
WeakHashMap.Entry 和 HashMap.Node 的不同点在于,WeakHashMap.Entry 继承了WeakReference。
弱引用的生存期特别短。https://blog.csdn.net/ChineseSoftware/article/details/119212399的时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。
想象一下如下场景:
WeakHashMap使用了软引用结构,它的对象在垃圾回收时会被删除
注:垃圾回收是优先级非常低的线程,不能被显示调用,当内存不足的时候会启用
下面是 WeakHashMap 的实现原理拆分:
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
... ...
// 用于存储需要清理的引用对象
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
... ...
// 内部Entry继承自WeakReference,从而有弱引用特性
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
... ...
}
... ...
// 用于移除内部不用的Entry来释放内存
private void expungeStaleEntries() { ... ... }
... ...
}
WeakHashMap 原理说明
1.每次GC清理对象后,引用对象被放置到 ReferenceQueue 之中
2.每次访问 WeakHashMap 都会调用 expungeStaleEntries 遍历删除 ReferenceQueue 中引用对象
1、缓存中使用
由于 WeakHashMap 是弱引用,因此适合在缓存中使用,当内存不足GC的时候,会清理不用的引用达到释放内存的目的
2、不要使用基础类型作为WeakHashMap的key
我大概理解的是,基础类型的一定范围不会被回收
原文:objectMap.put方法执行的时候i会被封装为Integer类型的,Integer保留了-128到127的缓存。但是对于int来说范围大很多,因此那些Key <= 127的Entry将不会进行自动回收,但是那些大于127的将会被回收,因此最后的尺寸总是会稳定在128左右
key 设置从0 开始 ,一直循环发现 0至128 的key value 没有被清理
public static void main(String[] args) {
WeakHashMap map = new WeakHashMap();
map.put(0,0);
for (int i = 0; map.size() == 0; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i< 2000){
map.put(i,i);
}
System.out.println("i 值为: "+i);
System.out.println("map size 为: "+map.size());
if(map.size() == 0){
System.out.println("map size 为 " + 0);
break;
}
}
}
int 类型改为大于 128 gc自动清除
public class WeakHashMapTest {
public static void main(String[] args) {
WeakHashMap map = new WeakHashMap();
map.put(3000,3000);
for (int i = 129; map.size() != 0; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i< 2000){
map.put(i,i);
}
System.out.println("i 值为: "+i);
System.out.println("map size 为: "+map.size());
if(map.size() == 0){
System.out.println("map size 为 " + 0);
break;
}
}
}
}
在检测到适当的可达性更改后,垃圾收集器会将注册的引用对象附加到该队列中。
byte[] key = new byte[1024*10];
WeakReference
//当垃圾回收之后实际上会将reference对象放进引用队列中。
//而key就是也就是byte数组对象回收掉。通常引用队列就是作为一个通知的信号,表明这个对象被回收掉了。
验证代码 这里设置堆的大小为-Xmx20m (20m)
@Test
public void test() throws InterruptedException {
ReferenceQueue queue = new ReferenceQueue<>();
new Thread(()->{
HashMap
}
WeakReference的地址java.lang.ref.WeakReference@6930026c
byte对象地址null
WeakReference的地址java.lang.ref.WeakReference@561e343
byte对象地址null
map.size()=100
实际上只有map数组中key对象中的byte数组被回收掉了。