数据一致性概念
数据一致性,通常指数据在某个时间点多个副本中的一致性。比如同一份数据如果既存在关系型数据库汇中,又存在于Redis中,那么如何来保证它的双写一致性呢?
除了这种时间点的一致性问题,在数据库中也会要求具备一致性,即ACID中的C(Consistency)。
在分布式系统中,通常为了保证AP而采用最终一致性,即BASE理论中的E(Eventual Consitency)。
一致性级别
强一致性,在任意时刻,所有副本中的数据是一样的。
弱一致性,相对于强一致性,即可能存在某些副本中数据是不一致的。但这种不一致的状态应该是暂时的,最终还是要一致的。
其它的还有顺序一致性,比如zookeeper实现的就是顺序一致性。
JAVA内存模型与数据一致性
当多个处理器的运算任务都涉及同一块主内存区域时,各处理器将主内存数据拷贝到本处理器高速缓存中的时机如果不一致将可能导致各自的缓存数据不一致。而为了解决一致性问题,需要各个处理器访问缓存时都遵循一些协议,如MESI等。
另外除了高速缓存可能导致不一致以外,指令重排序也可能产生这一问题。
而java语言中的volatile、synchronized关键字都可以保证可见性、有序性。
除了增加高速缓存以外,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器也有指令重排序(Instruction Reorder)优化。
重排序
happens-before
volatile
集合类
在并发编程中,我们经常会用到集合类。这些集合类的数据一致性不尽相同,这里简单总结一下。
ArrayList
ArrayList不是线程安全的,也无法保证数据一致性。
Vector
Vector 基于 Synchronized 同步锁实现线程安全,Synchronized 关键字几乎修饰了所有对外暴露的方法,因此可以实现强一致性。但在读远大于写的操作场景中,Vector 将会发生大量锁竞争,虽然jdk对synchronized作了一系列的优化(锁升级等),但还是要考虑性能开销。
CopyOnWrite容器
CopyOnWriteArrayList、CopyOnWriteArraySet
CopyOnWrite容器即写时复制的容器。CopyOnWrite容器的弱一致性体现在当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
Hashtable
Hashtable 是基于 HashMap 实现的,与Vector相似,使用 Synchronized 同步锁修饰了 put、get、remove 等方法,具有强一致性。
Hashmap
Hashmap不是线程安全的,也无法保证数据一致性。
ConcurrentHashmap
ConcurrentHashMap也是弱一致的
jdk1.6版本中, HashEntry如下,由于指向下一个结点的 next 是不可变的,可能导致数据被修改后不能立即可见。
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
@SuppressWarnings("unchecked")
static final <K,V> HashEntry<K,V>[] newArray(int i) {
return new HashEntry[i];
}
}
1.7,1.8版本中,hashEntry、Node中的next则为volatile变量。
另外如果新增一个Entry加入底层数据结构时,由于get方法没有加锁,且put方法中的tab没有被volatile元素修饰,happens-before不生效,因此也可能存在数据不一致的情况。
HashEntry<K,V>[] tab = table;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
另外,在1.8的实现中,一些试图、迭代器也存在读写分离的思想,也是弱一致的。
参考文档
https://juejin.im/post/6844903782329876494
https://es.cs.uni-kl.de/publications/datarsg/Senf13.pdf
http://ifeve.com/concurrenthashmap-weakly-consistent/