目录
今日良言:投资自己才是最好的投资
一.HashMap.
二.HashTable
三.ConcurrentHashMap
四.三者的区别
这篇博客主要介绍的是 HashTable HashMap ConcurrentHashMap 之间的区别,重点要知道ConcurrentHashMap的优点(好处/与HashTable相比的优点)
1.概念和场景
在介绍HashMap之前,先来介绍一下什么是Map:
Map是一种专门用来搜索的容器或者数据结构,其具体的搜索效率与其具体的实例化子类有关.
在我们之前的学习过的内容中,常见的搜索方式有:
1).直接遍历 时间复杂度为O(N),元素较多时,效率会非常慢
2).二分查找 时间复杂度为O(logN),但搜索前必须要求序列是有序的
上述这两种方式都适合静态类型的查找,即:一般不会对区间进行插入和删除操作.而现实中的很多查找,如:
1).根据姓名查找成绩
2).通讯录,即根据姓名查询联系方式
3).不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,也就是动态查找,此时上述的两种查找方式就不适合了,而Map是一种适合动态查找的集合容器
2.模型
一般把搜索的数据叫做关键字(Key),和关键字对应的称为值(Value),将其称为 Key-Value键值对,所以,模型一般有两种:
1).纯Key模型
比如:
有一个英文字典,快速查找一个单词是否在词典中
快速查找某个名字在不在通讯录中
2).Key-Value模型
比如:
梁山好汉的江湖绰号(每个好汉都对应一个绰号)
Map中存储的就是Key-Value的键值对,Set中只存储了Key
3.关于Map的说明
Map是一个接口类,没有继承自Collection,该类中存储的是
关于Map.Entry
的说明 Map.Entry
是map内部实现的用来存放 键值对映射关系的内部类,该内部类中主要提供提供了 的获取,value的设置以及Key的比较方式 方法 解释K getKey () 返回 entry 中的 keyV getValue () 返回 entry 中的 valueV setValue(V value) 将键值对中的 value 替换为指定 value通过下面的代码以及运行结果增加理解public class Exercise { public static void main(String[] args) { Map
map = new HashMap<>(); map.put("宋江","及时雨"); map.put("鲁智深","花和尚"); Set > set = map.entrySet(); for (Map.Entry entry:set) { System.out.println("K:"+entry.getKey()+" V:"+entry.getValue()); } } } public class Exercise { public static void main(String[] args) { Map
map = new HashMap<>(); map.put("宋江","及时雨"); map.put("鲁智深","花和尚"); Set > set = map.entrySet(); for (Map.Entry entry:set) { entry.setValue("好人"); } for (Map.Entry entry:set) { System.out.println("K:"+entry.getKey()+" V:"+entry.getValue()); } } } 注:Map.Entry
并没有提供设置Key的方法
4.Map的常用方法说明
注:
1).Map是一个接口,不能直接实例化对象,如果要实例化对象,只能实例化其实现类TreeMap或者HashMap
2).Map中存放的键值对的Key是位移的,Value可以重复
3).Map中的Key可以全部分离出来存储到Set中(Key值不重复)
4).Map中的Value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)
5).Map中键值对的Key不能直接修改,Value可以修改,如果要修改Key,只能先将Key删除,然后再重新进行插入
5.HashMap的使用案例
public class Exercise {
public static void main(String[] args) {
Map m = new HashMap<>();
// put(key, value):插入key-value的键值对
// 如果key不存在,会将key-value的键值对插入到map中,返回null
m.put("林冲", "豹子头");
m.put("鲁智深", "花和尚");
m.put("武松", "行者");
m.put("宋江", "及时雨");
String str = m.put("李逵", "黑旋风");
System.out.println(m.size());
System.out.println(m);
// put(key,value): 注意key不能为空,但是value可以为空
// key如果为空,会抛出空指针异常
//m.put(null, "花名");
str = m.put("无名", null);
System.out.println(m.size());
// put(key, value):
// 如果key存在,会使用value替换原来key所对应的value,返回旧value
str = m.put("李逵", "铁牛");
// get(key): 返回key所对应的value
// 如果key存在,返回key所对应的value
// 如果key不存在,返回null
System.out.println(m.get("鲁智深"));
System.out.println(m.get("史进"));
//GetOrDefault(): 如果key存在,返回与key所对应的value,如果key不存在,返回一个默认值
System.out.println(m.getOrDefault("李逵", "铁牛"));
System.out.println(m.getOrDefault("史进", "九纹龙"));
System.out.println(m.size());
//containKey(key):检测key是否包含在Map中,时间复杂度:O(logN)
// 按照红黑树的性质来进行查找
// 找到返回true,否则返回false
System.out.println(m.containsKey("林冲"));
System.out.println(m.containsKey("史进"));
// containValue(value): 检测value是否包含在Map中,时间复杂度: O(N)
// 找到返回true,否则返回false
System.out.println(m.containsValue("豹子头"));
System.out.println(m.containsValue("九纹龙"));
// 打印所有的key
// keySet是将map中的key防止在Set中返回的
for(String s : m.keySet()){
System.out.print(s + " ");
}
System.out.println();
// 打印所有的value
// values()是将map中的value放在collect的一个集合中返回的
for(String s : m.values()){
System.out.print(s + " ");
}
System.out.println();
// 打印所有的键值对
// entrySet(): 将Map中的键值对放在Set中返回了
for(Map.Entry entry : m.entrySet()){
System.out.println(entry.getKey() + "--->" + entry.getValue());
}
System.out.println();
}
}
运行结果:
HashTable 继承了Dictionary抽象类,是一个Dictionary抽象类的具体实现,Dictionary是声明了操作"键值对"的函数接口的抽象类
哈希表的代码实现博主之前写过博客:
(4条消息) 哈希表(限定版)_程序猿小马的博客-CSDN博客
HashTable 定义了四种构造方法
1).默认构造方法
Hashtable
2).传入一个参数,指定哈希表的大小
Hashtable(int size)
3).传入两个参数,第一个参数指定哈希表的大小,第二个参数指定负载因子(即:达到负载因子,哈希表中就不能再添加元素,需要扩容)
Hashtable(int size,float fillRatio)
4).创建了一个以M中元素为初始化元素的哈希表。
哈希表的容量被设置为M的两倍。
Hashtable(Map m)
HashMap除了从Map接口中定义的方法外,还包含以下方法:
ConcurrentHashMap是更优化的线程安全哈希表,主要使用于多线程中
对于ConcurrentHashMap的详细介绍放到下面与HashTable区别中
1.HashTable 和 ConcurrentHashMap的区别
1).ConcurrentHashMap相较于HashTable 大大缩小了锁冲突的概率,具体来讲就是将一把大锁转换成了多把小锁.
HashTable的做法是在方法上直接加synchronized,等于是给this加锁.只要操作哈希表上的任意元素,都会产生加锁,也就都可能发生锁冲突.
ConcurrentHashMap的做法:让每条链表有各自的锁(而不是公用一把大锁),具体来说就是:使用每个链表的头结点作为锁对象.(在jdk1.8之前ConcurrentHashMap使用的是"分段锁",也就是是给几条链表加锁,从而实现缩小锁冲突的概率,但是这种做法不够彻底,一方面粒度切分的还不够细,另一方面代码实现也更繁琐)
2).ConcurrentHashMap做了一个激进的操作,针对读操作不加锁,针对写操作加锁.
具体来说就是:
a.两个线程如果都进行读取变量的操作,不发生冲突(读和读操作之间没有冲突)
b.两个线程如果都进行修改变量的操作,发生冲突(写和写之间发生冲突)
c.如果一个线程进行读取变量的操作,一个线程进行修改变量的操作,不发生冲突.
第三种情况中,必须要求写操作是原子的,并且变量是被voltile修饰的,否则就会发生类似"脏读"的情况
脏读属于数据库事务的知识点,博主之前的博客有过详细介绍:
(4条消息) 如何理解数据库事务?_程序猿小马的博客-CSDN博客
volatile关键字,博主在之前的博客也有过详细介绍,主要是解决内存可见性问题和禁止指令重排序
(4条消息) 线程安全问题_程序猿小马的博客-CSDN博客
3).ConcurrentHashMap内部充分的使用了CAS,通过这个进一步的削减加锁操作的数目
(CAS博主在后面的文章会有具体介绍,后续会附上博客链接)
4).二者对于扩容的方式不同
HashTable/HashMap 扩容:
创建一个更大的数组空间,将旧数组上的每条链表上的每个元素都搬运到新的数组上,主要进行的操作就是插入和删除,这个扩容操作会在某次put操作的时候发生,也就是达到负载因子时,此时,如果旧数组中的元素特别多的话,就导致这次的put操作比平常的put操作要慢很多倍
ConcurrentHashMap 扩容:
采用的是"化整为零"的方式,也就是每次搬运一小部分
创建新的数组,旧数组也保留.
每次put操作,都往新数组上添加,同时进行一部分的搬运(将一部分旧数组上的元素搬运到新的数组上)
每次get的时候,新数组和旧数组都查询.
每次remove的时候,直接删除即可
经过一定时间之后,所有的元素都搬运好了,此时就释放旧数组
2.HashTable和HashMap的区别
1).父类不同
HashTable的父类是Dictionary,HashMap的父类是AbstractMap
2).线程安全
HashTable是线程安全的,HashMap是线程不安全的
这就意味着,在单线程下HashMap的性能要比HashTable要好
如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
3.HashMap和ConcurrentHashMap的区别
1).线程安全
ConcurrentHashMap是线程安全的,而HashMap是线程不安全的
2).并发操作
ConcurrentHashMap支持并发操作,HashMap不支持并发操作