面试必问:读写一致性,你需要思考的问题

前锋JAVA发展学院我想昨天分享

首先,本文将讨论的多线程读写是指线程写入,一个或多个要读取的线程,不包括多线程同时写入。

想象一下线程将数据写入散列映射并且线程将数据读入散列映射的场景。这有问题吗?如果是这样,问题是什么?

我相信每个人都知道存在问题,但问题是什么,可能并不那么明显。

有两个问题。

首先,内存可见性的问题,hashmap存储数据的表没有用voriate修改,这意味着读线程可能并不总是读取数据的最新值。

第二个是重新排序指令的问题。获得时,您可能会获得中间数据状态。我们来看看put方法的一些代码。

Final V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){

. if((p=tab [i=(n - 1)hash])==null)

Tab [i]=新节点(散列,键,值,下一个);

.

}

如您所见,在put操作中,如果表数组的指定位置为null,则将创建Node对象并将其放置在表数组上。但是我们知道jvm tab [i]=new Node(hash,key,value,next);这样的操作不是原子的,可能是由于指令重新排序,导致另一个线程调用get to get tab [i]你得到的是一个尚未调用构造函数的对象,导致无法预料的问题。

上述两个问题可以说是因为HashMap中的内部属性没有被voriate修改。如果HashMap中的对象全部由voriate修改,则一个线程写入,一个线程没有读取任何问题(这里我的猜测,确认这个猜测的正确性的基础是ConcurrentHashMap的get没有被锁定,这意味着在地图结构中阅读和写作不会发生冲突。请参阅以下区域中sora-zero同学的评论

用对象创建原子问题

有些学生可能对Object obj=new Object有疑问;在多线程的情况下,这样的操作将获得未初始化的对象。这是一个简单的解释。上面的java语句分为4个步骤:

为堆栈上的obj引用分配空格

在jvm堆中创建一个Object对象,注意这只是分配空间,没有对构造函数的调用

初始化在步骤2中创建的对象,即调用其构造函数

堆栈中的obj指向堆中的对象

上述步骤似乎也没有问题,毕竟,在调用构造函数之前,不会引用创建的对象。

但问题是jvm重新排序指令。重新排序后,可能是在步骤3之前执行步骤4.此时,另一个线程读取没有仍然执行构造函数的对象,从而导致未知问题。 Jvm重新排列仅保证重排前后单线程结果的一致性。

请注意,java中引用的赋值操作必须是原子的。例如,如果a和b都是对象,无论是32位还是64位jvm,a=b操作都是原子的。但是如果a和b是长或双原子数据,那么32位jvm上的a=b不一定是原子的(参见jvm实现),它可以分为两个32位操作。但对于voriate的long,double变量,赋值是原子的。

你可以在这里看到它

在数据库中读写一致性

跳出hashmap,在数据库中都是使用mvcc机制来避免添加读写锁。也就是说,如果你不使用mvcc,数据库是添加读写锁,那为什么数据库应该是读写锁?原因是写操作不是原子的。如果不添加读写锁或mvcc,则可以读取中间状态。对于数据,以HBase为例,Hbase编写过程分为以下几个步骤:

1.获取行锁

2.打开mvcc

3.写入内存缓冲区

4.写入附加日志

5.释放线锁

6.flush log

7.mvcc结束(这只对读者可见)

想象一下,如果你没有留下2,7个数据实际上是写入失败。也就是说,其他线程已经读取了不存在的数据。

同样,在mysql中,如果不使用mvcc而不使用读写锁,则事务尚未提交,数据可以读取,如果使用读写锁,则事务将在更改时添加写锁数据,此其他读取操作将阻塞,直到事务提交,这对性能有很大影响,因此在大多数情况下,数据库使用MVCC机制来实现非锁定读取。

收集报告投诉