避免fail-fast的方法:使用java.util.concurrent包下的类去取代java.util包下的类
import java.util.concurrent.CopyOnWriteArrayList;
List list = new CopyOnWriteArrayList<>();
使用Collections.synchronizedList(new ArrayList())来使ArrayList变成是线程安全的话,也是几乎都是每个方法都加上synchronized关键字的,只不过它不是加在方法的声明处,而是方法的内部。一般来说,我们都会使用JUC包下给我们提供的线程安全容器,而不是使用老一代的线程安全容器。
在遍历Vector的时候,有别的线程修改了Vector的长度,那还是会有问题!
在JDK5以后,Java推荐使用for-each
(迭代器)来遍历我们的集合,好处就是简洁、数组索引的边界值只计算一次。
CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
- CopyOnWriteArrayList是线程安全容器(相对于ArrayList),底层通过复制数组的方式来实现。
- CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁
- 元素可以为null
在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁。
- 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
- 写加锁,读不加锁
CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!
缺点
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行
add()、set()、remove()
的话,那是比较耗费内存的。- 因为我们知道每次
add()、set()、remove()
这些增删改操作都要复制一个数组出来。
- 因为我们知道每次
- 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
- 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用
setArray()
了)。但是线程A迭代出来的是原有的数据。
- 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用
Linux的COW
减少了Fork的开销。
- fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。
- fork出的子进程功能实现和父进程是一样的。如果有需要,我们会用
exec()
把当前进程映像替换成新的进程文件,完成自己想要实现的功能。
Redis的COW
- Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
- 总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
- 而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。
文件系统的COW
Copy-on-write在对数据进行修改的时候,不会直接在原来的数据位置上进行操作,而是重新找个位置修改,一旦系统突然断电,重启之后不需要做Fsck。好处就是能保证数据的完整性,掉电的话容易恢复。
要修改数据块A的内容,先把A读出来,写到B块里面去。如果这时候断电了,原来A的内容还在!