今日学习总结如下:
CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
具体的实现类:
CopyOnWriteArrayList和CopyOnWriteArraySet(具体实现为CopyOnWriteArrayList)
CopyOnWriteArrayList的实现原理
可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList
CopyOnWriteArrayList每次添加元素都是在原始数组的长度基础上+1,删除元素则长度-1
CopyOnWrite的应用场景
CopyOnWrite并发容器用于读多写少的并发场景。
比如白名单,黑名单,类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索
商品注意两点:
- 减少扩容开销。根据实际需要,初始化一个Set,将数据全局添加到set中,然后再使用addAll之类的方法一行添加集合中的所有元素,以避免每次写时扩容的开销,合理的评估
- 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
CopyOnWrite的缺点
1、内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
针对内存占用问题,可通过压缩容器中的元素的方法来减少大对象的内存消耗,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
2、数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
ConcurrentSkipListMap
ConcurrentSkipListSet
二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存。这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了。
如果需要的是一个能够进行二分查找,又能快速添加和删除元素的数据结构,首先就是二叉查找树,二叉查找树在最坏情况下可能变成一个链表。
于是,就出现了平衡二叉树,根据平衡算法的不同有AVL树,B-Tree,B+Tree,红黑树等,但是AVL树实现起来比较复杂,平衡操作较难理解,这时候就可以用SkipList跳跃表结构。
monitor
Monitor可以翻译成监视器或者管程,是由JVM提供的,当线程执行到对象方法的临界区时,首先去看对象头是不是关联了Monitor,如果关联了加入Monitor的EntryList中,没有关联就改变对象的对象头的Mark Word或者指向一个Monitor,然后Monitor的拥有者就变成了该线程,执行完了之后,Monitor再把线程释放,去EntryList找一个新的拥有者(非公平竞争,不是先来的就先得)
一个对象关联一个Monitor;当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入,同时在Object类中还提供了notify和wait方法来对线程进行控制
重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对(也称为管程或监视器锁)的起始地址
不加synchronized的对象不会关联监视器;
自旋优化:在竞争锁的时候,自旋重试几次,要是此时锁被释放就进入,否则进入阻塞状态
java中每个对象都有唯一的一个monitor,想拥有一个对象的monitor的话有以下三种方式:
1、执行该对象的同步方法`public synchronize a () {}`
2、执行该对象的同步块synchronize(obj) {}`
3、执行某个类的静态同步方法`public static synchronize b(){}`。
Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。
当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时,它会首先在entry-set入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的Monitor,那么它会和entry-set队列和wait-set队列中的被唤醒的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的Monitor,执行受保护的代码段,执行完毕后释放Monitor,如果已经有线程持有对象的Monitor,那么需要等待其释放Monitor后再进行竞争。
容错机制
failfast快速失败
如果出现故障,则立即报错。
通常用于非幂等性操作,如:下单操作,如果写入故障,则立即报错,不必重试
failsafe故障安全
如果出现故障,则可以忽略,因为这种故障不会造成损失或损失在可接受范围内。
通常用于辅助性操作,如:写入监控日志,如果写入故障,则忽略。
failover故障切换
如果出现故障,则重试备份操作模式。
通常用于幂等性操作,如:MySql的双Master模式,如果主Master故障,则切换至从Master。
重试通常会带来更多延时。
failback故障恢复
故障切换之后,如果主要操作模式恢复,则自动从备份操作模式恢复主要操作模式。
如:MySql的双Master模式,如果主Master故障,则failover至从Master;当主Master恢复之后,则自动切换至主Master。