HashMap之快速失败避免读写同时进行

引言:

一般面试都有一些线程题目,大致是某某集合和某某集合什么区别,比如Hashmap .HashTable,ConcurrentHashMap有什么区别
很多网上答案都说线程安全,一个线程不安全。那么这里不是讨论什么叫线程安全,只是讨论线程不安全怎么办。
介绍:
以HashMap为例,它不安全,什么叫不安全。简单理解就是多线程读写可能是脏数据,比如你在A线程读,B线程写了一个新数据,而A线程却不知道。可怕。。
之前有一个奔溃是java.util.ConcurrentModificationException,就是线程不安全的操作,不过更专业的说法是快速失败。HashMap在读的过程中,如果写操作,jdk在处理这种情况是有一个判断的:
即迭代器迭代,检测当前集合大小,如果大小不符合预期则抛出异常达到快速失败的效果。
例如迭代器迭代开始时候是4个key,中间发现变成5个key,这时候迭代器直接失败抛出异常,因为它认为迭代中是读操作,至始至终都应该是不变的集合才能保障效果。这里有一些问题需要解释:迭代过程,修改key对应的键值对,那也是修改了,那迭代要不要抛异常。如果抛异常怎么判断?毕竟集合大小没有变化呢
代码:

开启阅读源码模式
—–会奔溃的代码。(我自己写的测试代码)

`public static void main(String args[]) {
        HashMap<String, String> map = new HashMap();
        map.put("0", "1");
        map.put("1", "x");
        map.put("2", "x");
        Iterator<String> i = map.keySet().iterator();
        while (i.hasNext()) {
            if (map.get(i.next()).equals("x")) {
                map.put("ok", "true");              
            }
        }
    }`

结束,那么问题应该是i.hasNext()
——jump it.read the fuck code
i是hashmap的键对应迭代器,这个类是HashMap$KeySet
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}

再看KeyIterator类。
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}

再看nextEntry()方法
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();


—–关键代码就是第一步的判断,也就是希望值和实际值不符合直接抛异常,这不就是生活中不符合预期直接解雇你吗。哈哈哈哈哈哈
if (modCount != expectedModCount)
throw new ConcurrentModificationException();

原理介绍:

小情绪梳理一下,HashMap迭代器中,每一次迭代都判断当前梳理和预期梳理是不是符合保持一致,如果不一致不再迭代直接抛异常。
那么再解释一下几个变量的原理
modCount != expectedModCount
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;

HashMap结构修改的次数,包括那些修改Hashmap映射的数量或者导致内部结构。变化的数量,这个变量是用于快速失败。就是这么理解呗,hashmap变化就将变量+1
常见的是不是有put操作,remove操作,
看看相关操作都会自增这个变量,也就是修改了HashMap都会让这个变量+1
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}

再看看 expectedModCount
1。赋值,是在初始化类也就是迭代器一开始就取当前modCount,预期是当前的数量。

2 那以后在迭代中,期望值是第一次迭代赋值,实际上你可能会添加一个值,那这样就不一样了。
直接异常处理。

一些引申:

1.HashMap如果在迭代时候又想添加元素是不可以的,至少没有一些外部方法可以这种效果又没有异常抛出。
但是迭代中去删除是可以的,并且也符合实际情况,有一个方法也对外调用就是不要直接使用hashmap的删除,而是使用迭代器的删除。
HashInterator调用这个方法可以删除相关key
`public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}`

再引申一下:一些奇葩面试题目可能面试官想吐槽想恶心别人导致心理阴暗,他们出一些快速失败的例子,但是又不会抛出异常导致奔溃,仔细想一下如果我们在迭代中集合只有一个元素,然后迭代中插入一个键值对会不会奔溃,答案是不奔溃。还有一些是直接break导致不会检测也就不会奔溃,非常隐蔽啊。因为根本没有机会让迭代器判断expect!=modCount啊根本原因也就是快速失败尽自己最大的努力去避免读写同时进行导致脏数据,而不是万无一失的保障。


终于写完快速失败的原理了,网上已经有很多例子啊(自己真的是妈的智障),但是自己看代码弄清楚原理就是不一样啊,希望大家都弄清楚原理,多读源码,少一些浮夸的东西

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值