1.CopyOnWriteArrayList和ArrayList相比有哪些相同点和不同点?
答:相同点是底层的数据结构都是数组,提供出来的API都是对数组进行的操作。不同点是后者是线程安全的,在多线程环境下使用,无需加锁。
2.CopyOnWriteArrayList通过哪些手段实现了线程安全?
答:主要通过以下三点保证了线程安全。第一,数组容器被volatile关键字修饰,保证了数组内存地址被任意线程修改后,都会通知到其他线程。第二,对数组的所有修改,都进行了加锁操作,保证了同一时刻,只能有一个线程对数组进行修改。第三,修改操作对原数组进行了复制,是在新数组上进行修改的,修改过程中,不会对原数组产生任何影响。
3.CopyOnWriteArrayList在add方法中,对数组进行加锁后,已经是线程安全了,为什么还需要对老数组进行拷贝?
答:的确,对数组进行加锁后,能够保证同一时刻,只有一个线程能对数组进行add,单核CPU的多线程环境下肯定没有问题,但现在的机器都是多核CPU,如果不通过复制拷贝新数组,直接修改原数组容器的内存地址,是无法触发volatile可见性效果的,那么其他CPU下的线程就无法感知原数组已经被修改了,就会引发多核CPU下的线程安全问题。
假设不复制拷贝,而是在原来数组上直接修改值的话,数组的内存地址就不会变,而数组被volatile修饰时,当数组的内存地址变更时,就能及时通知到其他线程。内存地址不变,仅仅是数组元素值发生变化时,是无法把数组元素值发生变动的事实,通知到其它线程的。
4.对老数组进行拷贝,会有性能损耗,平时使用时需要注意什么?
答:在批量操作时,尽量使用addAll、removeAll方法,而不要在循环里面使用add、remove方法,主要是因为for循环里面使用add 、remove时,每次操作都会进行一次数组的拷贝(甚至多次),非常耗性能,而addAll、removeAll方法底层做了优化,整个操作只会进行一次数组拷贝,由此可见,当批量操作的数据越多,批量方法的高性能体现的越明显。
5.为什么CopyOnWriteArrayList迭代过程中,数组结构变动,不会抛出ConcurrentModificationException?
答:主要是因为CopyOnWriteArrayList每次操作时,都会产生新的数组,而迭代时,持有的仍然是老数组的引用。数组结构变动,是用新数组替换了老数组,老数组的结构并没有发生变化,所以不会抛出异常。
6.插入的数据正好在List的中间,请问两种List分别拷贝数组几次?为什么?
答:ArrayList只需拷贝一次,假设插入的位置是2,只需要把位置 2(包含2)后面的数据都往后移动一位即可,所以拷贝一次。
CopyOnWriteArrayList拷贝两次,因为CopyOnWriteArrayList多了一次将老数组数据拷贝到新数组这一步。可能有人会想到这种方式,先把老数组拷贝到新数组,再把2后面的数据往后移动一位,这的确是一种拷贝的方式,但CopyOnWriteArrayList底层实现更加灵活,是把老数组0到2的数据拷贝到新数组上,预留出新数组2的位置,再把老数组3到最后的数据拷贝到新数组上,这种拷贝方式可以减少拷贝的数据,虽然是两次拷贝,但拷贝的数据却仍然是老数组的大小,设计的非常巧妙。
7.ConcurrentHashMap和HashMap相比有哪些相同点和不同点?
答:相同点是数组 + 链表 +红黑树的数据结构,以及都实现了Map接口,继承了AbstractMap抽象类,所以两者的方法大多都是相似的,可以互相切换。
不同点是ConcurrentHashMap是线程安全的,在多线程环境下,无需加锁,可直接使用。数据结构上,ConcurrentHashMap多了转移节点,主要用于保证扩容时的线程安全。
8.ConcurrentHashMap通过哪些手段保证了线程安全?
答:主要通过以下五点保证了线程安全。第一,储存Map数据的数组被volatile关键字修饰,一旦被修改,立马就能通知其他线程,因为是数组,所以需要改变其内存值,才能真正的发挥出volatile的可见性。第二,put时,如果计算出来的数组下标索引没有值的话,会采用无限for循环 + CAS算法,这样既可以新增成功,又不会覆盖其他线程put进去的值。第三,
如果put的节点正好在扩容,会等待扩容完成之后,再进行put ,保证了在扩容时,老数组的值不会发生变化。第四,对数组的槽点进行操作时,会先锁住槽点,保证只有当前线程才能对槽点上的链表或红黑树进行操作。第五,红黑树旋转时,会锁住根节点,保证旋转时的线程安全。
9.描述一下CAS算法在ConcurrentHashMap中的应用?
答:CAS其实是一种乐观锁,一般有三个值,分别为赋值对象、原值和新值,在执行的时候,会先判断内存中的值是否和原值相等,相等的话把新值赋值给对象,否则赋值失败,整个过程都是原子性操作,没有线程安全问题。ConcurrentHashMap的put方法中,有使用到CAS,是结合无限for循环一起使用的,步骤如下,第一步先计算出数组索引下标,拿出下标对应的原值。第二步,CAS覆盖当前下标值,赋值时,如果发现内存值和上一步拿出来的原值相等,执行赋值,退出循环,否则不赋值,转到第三步。第三步,进行下一次for循环,重复执行第一和第二步,直到成功为止。可以看到这样做的好处,第一是不会盲目的覆盖原值,第二是一定可以赋值成功。
10.ConcurrentHashMap是如何发现当前槽点正在扩容的?
答:ConcurrentHashMap新增了一个节点类型,叫做转移节点,当发现当前槽点是转移节点时(转移节点的hash值是-1),即表示Map正在进行扩容。
11.发现槽点正在扩容时,put操作会怎么办?
答:无限for循环,或者走到扩容方法中去,帮助扩容,一直等待扩容完成之后,再执行put操作。
12.两种Map扩容时,有啥区别?
答:区别很大,HashMap是直接在老数据上面进行扩容的,多线程环境下,会有线程安全的问题,而ConcurrentHashMap就不太一样,扩容过程是这样的。
第一步,从数组的队尾开始拷贝。第二步,拷贝数组的槽点时,先把原数组槽点锁住,拷贝成功到新数组时,把原数组槽点赋值为转移节点。第三步,从数组的尾部拷贝到头部,每拷贝成功一次,就把原数组的槽点设置成转移节点。第四步,直到所有数组数据都拷贝到新数组时,直接把新数组整个赋值给数组容器,拷贝完成。
简单来说,通过扩容时给槽点加锁和发现槽点正在扩容就等待的策略,让ConcurrentHashMap可以将槽点一个一个地转移,保证了扩容时的线程安全。