0
点赞
收藏
分享

微信扫一扫

关于Java中ArrayList线程不安全的思考

清冷的蓝天天 2022-04-04 阅读 117
java

在平时日常开发中,经常会根据不同的应用场景,使用到许多集合类比如List、Set、Map这些。对于个人而言,应用最为频繁的要数ArrayList,然而在偶然的一次机会接触到了一个这样的问题:ArrayList是否线程安全,如果不安全要怎么让它线程安全?

这个问题让我一整晚都百思不得其解,遂翻阅源码,啃之。

ArrayList线程是否安全?

通过源码注释的第一段就可以得知,这个ArrayList大致上都跟Vector差不多,唯一不同就是不是同步的。这个时候又引申出了一个叫做Vector的集合。

Vector的同步方式

简单地去看一下Vector的add方法和get方法,通过synchronized关键字使多线程对集合的操作变成串行的方式去执行,达成了同步的效果进而保证了线程安全。


那么如果我只想使用ArrayList而不是Vector呢?要怎么实现线程安全的问题?

这个时候,再回去注释上面翻看一下,即可知道答案

因为ArrayList并不支持同步,所以当有多个线程对其进行访问,同时有一个线程对它进行修改的时候,需要在外部进行同步。如果不想通过编码的方式实现的话,也可以调用Collections类的synchronizedList方法获取一个同步的对象。

SysnchronizedList的同步方式

SynchronizedList继承了SynchronizedCollection同时实现了List接口。

同时SynchronizedCollection类中有一个很关键的成员变量——mutex,这个变量用于线程的同步访问。

除此之外,SynchronizedList也有一个用final修饰的成员变量list,这个意味着一旦赋值以后就不能再修改了。尽管list引用不能被修改,但是list的内部结构仍然可以进行变动的。

然后看到SynchronizedList的构造方法,可以知道除了使用SynchronizedCollection中的mutex,也可以自定义一个对象作为线程同步的锁。

研究完SynchronizedList的结构外,再看到它对于list的操作,也是通过获取mutex的锁来进行线程的同步,实现的原理跟Vector差不多。


在这个通过加锁来保证线程安全的方案下,虽然说能够保证数据不会丢失,但是也可以轻易的预见这类集合的性能瓶颈问题,一个时刻中只能有一个线程对其进行访问,如果是多写少读的场景,还可以接受,但是如果是多读少写,甚至是读的比例占比很大的情况下,这样的方案会显得不那么灵活。

这个时候可能就可以通过引入读写锁(ReentrantReadWriteLock)来解决这样的问题,采用读写锁的方案相较于上面的Vector在读性能上有很大的提升,能够支持多个线程同时进行读取,但是当出现一个写的操作的时候,依然会阻塞大量读的线程,假如写操作是一个长时间的操作的话,那么大量的读请求将会被阻塞掉。

如果去结合一下数据库的知识,以上情况是不是跟数据一致性的思想有点相似,阻塞读操作无非是为了不让它读到旧的还没有被修改的数据(脏数据),假如说现在的场景如果对数据的准确性并不要求那么高的话,只需要保证数据的最终一致性,不要出现先两个线程同时写而导致数据丢失的情况即可,这个要求下那么就可以抽象的理解为对于一个集合只能同时有一个线程进行写操作,然后支持多个线程进行读操作,尽管读的数据不是最新的也没有问题。针对这个场景可以了解到一个叫做CopyOnWrite原理(写时复制)。

CopyOnWrite原理及CopyOnWriteArrayList实现类

写入时复制(Copy-on-write)是一个被使用在程序设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。 ——摘自百度百科

写入时复制,按照个人理解其实就是多个线程去读同一个数据的时候,都会读到内存中的同一块区域,而当中有一个线程想要对这个数据进行修改的时候,修改操作不会直接作用到原本的内存空间,而是将原来的数据做一个拷贝,在副本中进行修改,最后再讲数据覆盖到原来的位置中。粗浅的可以理解为数据库中一个事务修改完数据库后进行的事务提交操作,事务提交之后,如果事务没有提交,那么对原来的数据也没有其他影响,因为数据修改只发生在数据的副本中。


CopyOnWriteArrayList基于CopyOnWrite原理实现了线程安全,相较于Vector的线程串行执行和基于读写锁的并发访问机制,CopyOnWriteArrayList支持了更高的并发能力,当有线程进行修改操作的时候,不会对其他读线程进行阻塞。对于数据一致性的问题,CopyOnWriteArrayList实现了弱一致性——最终一致性,在业务中对数据的一致性敏感度不那么高,同时允许客户端读取到相对旧的数据的时候,CopyOnWriteArrayList是一个相对而言比较好的选择。

CopyOnWriteArrayList源码阅读

array:数据存放的位置,只能通过getter/setter方法进行访问

锁对象,用于对修改集合的线程进行串行化执行

对集合修改的方法之一:通过得到锁来获得对集合的修改权限,然后通过getArray()方法来获得数组的引用,之后对原数组进行拷贝,修改操作只存在于拷贝的数据中,随后将修改后的数据通过setArray()方法使得array这个成员变量重新指向这个新的拷贝。

在执行了setArray()方法之后,其他读线程读到的数据都会是新的数据,而在那之前线程读取到的数据是相对旧的数据,只要是正常的读取操作,两者都不会出现太大的问题,并不会有脏读的情况出现。

总结

通过ArrayList线程安全的问题,了解到了针对集合并发访问同时保证数据一致性的解决方案。

  1. Synchronized加锁,使得线程通过串行化的方式来访问集合,对多读少写的场景并不太友好;
  2. 读写锁,适用于多读少写的场景,但是多写少读的场景下也会退化成方案1,如果写操作时间过长,也会对读线程进行阻塞,影响读线程的执行效率,业务对于数据实时性高的情况下适用于这套解决方法;
  3. CopyOnWrite原理,通过牺牲数据强一致性,转变为弱一致性,提高读写效率,写操作不会对读操作阻塞,同时写操作会单独进行串行化执行来保证数据的安全,在并发能力和数据一致性之间取得一个相对较好的平衡,只要业务对数据的实时性要求不是很高的情况下,可以考虑这种方案。
举报

相关推荐

0 条评论