HashMap,ConcurrentHashMap,volatitle
table:entry->entry->entry
entry:<key,value>,next
数组与链表的结合体,链式数组
拥有两个参数loadFactor和initialCapacity,分别称作加载因子0.75和初始化容量16。
HashMap在没有数据进入,即put时,是不会创建数组的。
在这里,为了确保table容量满足要求,却又必须为2的幂。
所以阈值就是table的容量与负载因子的乘积。
put方法
在添加数据时put,为了确保所对应的key的hash值不超出table的长度,所以需要与table.length-1与操作,即成功取得hash的length长度,即低多少位。
在往table中添加数据时,当数组为空时,可以直接插入。
当向table中添加entry时,首先要判断entry数量是否超过阈值,其次要判断当前位置是否为空,若为空,可以直接添加。
get方法
在从table中进行取值操作时,如果传入的key值为null时,则需要从table[0]中进行取值操作。
在通过key获取entry时,首先需要判断的key的hash与遍历所得到的hash是否相同,再判断两个key是否相等,才能够返回entry。
扩容
在添加entry时,若table的长度超过阈值,且table当前位置不为空。
- 容量直接进行拓展两倍
- {将旧的数据与长度保存起来}
- {判断旧的table容量是否已经处于为设置的阈值,若处于阈值,则将阈值变为int类型的最大值,若没有则创建新的table数组}->将旧的table数据都传输到新创建的table中。
- 将阈值进行新的设置,即newCapacity*loadFactor 最大容量为所设置的阈值的<<1
高并发问题
问题出现在,table已经满了,需要进行拓展,即resize。
当某一个线程完成插入操作,而另外一个线程也要进行插入至同一位置时,当A线程更新完成后,但B线程正在进行transfer,若继续运行将造成,同一位置的链表由单向链表变为双向链表。那么在进行get操作时,将会陷入死循环。
如何解决高并发问题ConcurrentHashMap
按照以往的常规操作将使用synchronized方法,但是将导致所有线程在加锁的过程中阻塞,为了解决这样的问题,推荐使用concurrentHashMap。
在concurrentHashMap中,我们将以segment作为一个基本的组成单元。
一个segement就可以当作是一个table,而table的基本单元为entry。而concurrentHashMap就是一些segment的集合,可以称作segments。
如此我们可以有清晰地了解。
concurrentHashMap:segments:segment->segment->segment
相信我们可以有了一个了解,concurrentHashMap就可以看作为一个二级的哈希表。
Segment
通过观看segment的内部类,我们可以看到segemnt类继承ReentrantLock,进行锁的设置。
在观看segment的内部参数时,我们会发现其与HashMap中的table作用是一样的,但是它将使用volatitle进行修饰:
transient volatile HashEntry<K,V>[] table;
我们具体观看HashEntry的内容:
static final HashEntry<K,V>{
final int hssh;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
由此可见,对于value与链表都是由volatile进行修饰。
volatile
相较于synchronized重量级锁来说,volatile属于轻量级锁,不会引起线程上下的切换与调度,但是同步性较差,使用易出错。
可见性
当多个线程同时访问同一个变量时,一个线程修改这个变量的值,其他线程能够立刻看到变量的值。
相较于多线程情况下,synchronized能够保证一个线程获取到锁后然后执行同步代码,并在释放锁之前将变量刷新到内存中。但变量被volatile修饰就将意味着线程对于本地变量的存储无效,一旦修改,将立刻刷新到内存中,其他线程想要读取共享变量就会直接从内存中读取。
有序性
程序执行的顺序按照代码的先后顺序执行。
在线程内,所有操作是有序的,但在线程中观察另外一个线程,操作是无序的。
因为JAVA为了效率是允许编译器进行重排序的,这将对于多线程造成比较大的影响,而通过volatile可以保证有序性。当然,synchronized与lock也可以使得某一时刻只有一个线程执行某一变量。
可见性原理
通过上文,我们可以明确感知到了,每一个线程都是有着对于变量存储利用的自主性。
但是因为多线程的存在,且多线程对于共享变量如何使用又产生了新的问题。
为了确保在不同线程使用共享变量的过程中,不出现数据更新过慢,导致的数据引用出错的问题。JAVA提出,每个线程都将使用属于自己的本地内存,而不再使用主内存,本地内存是对于线程中所需数据的复制拷贝。需要注意的是这里的内存与实际的物理内存没有关系。
volatitle原理
保证可见性,提供有序性,但是无法保证原子性。
- 确保使用volatile进行修饰的指令不会在重排序中,排到前面指令的后面,也不会排在后面指令的前面。
- 若为写操作,则对于共享变量的修改直接写入主存中,且对于其他线程的缓存设为无效。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
单例模式的双重加锁,注意这里synchronized锁的是类,如果锁的是对象的话,多个线程调用同一对象方法会阻塞,但是不同对象则不会阻塞。在这里使用类锁是为了使得只有单一线程才可成功进入临界区。之所以使用volatile是因为,这里volatile是对instance对象进行上锁,两者之间不会相互影响,且在创建instance后,将会立刻写入主内存中,保证可见性。
ConcurrentHashMap:put方法
首先通过key获取哈希值,通过哈希值的低位来获取segment。
获取锁,发现获取失败,使用scanAndLockForPut()自旋获取锁,
- 尝试获取锁失败;
- 若尝试次数小于0,观看segment是否为空,若空添加新的entry;
- 若尝试次数大于所设定的最大尝试次数,则直接加锁。
在成功获取锁后:
- 遍历旧的table,进行key对比,若相等则将旧的value进行替换。
- 若table为空,添加新的entry,并判断是否需要扩展。
- 释放锁
ConcurrentHashMap:get方法
因为concurrentHashMap为二级哈希表,所以需要通过key来hash来定位segment,再通过hash来获取value。
问题解决,因为使用volatile使得共享变量都是可见的,且操作都是针对于segemnt的,因此可以解决线程的安全问题。
参考推荐: