一. JUC包下的并发容器
Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map,大家熟知的这些集合类ArrayList、LinkedList、HashMap这些容器都是非线程安全的。 所以,Java先提供了同步容器供用户使用。 同步容器可以简单地理解为通过synchronized来实现同步的容器 ,比如Vector、Hashtable以及SynchronizedList等容器。这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低。因此为了解决同步容器的性能问题,所以才有了并发容器。 java.util.concurrent 包中提供了多种并发类容器:1.1 CopyOnWriteArrayList
1.1.1 引入
当我们遍历数组时加入,这时候有另一个线程也在往里添加数据的话,会报错如图:
所以当我们使用 CopyOnWriteArrayList 时就可以解决package com.laoyang.Thread.JUC包下的并发容器; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; /** * @author:Kevin * @create: 2023-10-19 16:47 * @Description: 对应ArrayList */ public class CopyOnWriteArrayListDemo { public static void main(String[] args) { CopyOnWriteArrayList arrayList = new CopyOnWriteArrayList(); // ArrayList arrayList = new ArrayList(); arrayList.add("小杨"); arrayList.add("小凯"); arrayList.add("小文"); new Thread(() -> { arrayList.add("test"); }).start(); for (Object o : arrayList) { System.out.println(o.toString()); } } }
概念
CopyOnWriteArrayList 是 Java 中的一种线程安全的 List,它是一个可变的数组,支持并发读和写。与普通的 ArrayList 不同,它的读取操作不需要加锁,因此具有很高的并发性能。
1.1.2 场景
CopyOnWriteArrayList 的应用场景主要有两个方面:
1. 读多写少的场景由于 CopyOnWriteArrayList 的读操作不需要加锁,因此它非常 适合在读多写少的场景中使用 。例如,一个读取频率比写入频率高得多的缓存,使用 CopyOnWriteArrayList 可以提高读取性能,并减少锁竞争的开销。2. 不需要实时更新的数据由于 CopyOnWriteArrayList 读取的数据可能不是最新的,因此它适合于不需要实时更新的数据。例如,在日志应用中,为了保证应用的性能,日志记录的操作可能被缓冲,并不是实时写入文件系统,而是在某个时刻批量写入。这种情况下,使用 CopyOnWriteArrayList 可以避免多个线程之间的竞争,提高应用的性能。1.1.3 黑名单实战
package com.laoyang.Thread.JUC包下的并发容器; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; /** * @author:Kevin * @create: 2023-10-19 19:35 * @Description: 黑名单实战 */ public class IpDemo { private static final CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); //添加默认黑名单 static { list.add("Ip0"); list.add("Ip1"); list.add("Ip2"); } public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { //模拟接入 try { Thread.sleep(new Random().nextInt(5000)); }catch (Exception e){} String currentIP = "Ip" + new Random().nextInt(3); if (list.contains(currentIP)){ System.out.println(Thread.currentThread().getName() + " IP " + currentIP + "命中黑名单,拒绝接入处理"); return; } System.out.println(Thread.currentThread().getName() + " IP " + currentIP + "接入处理..."); } }; new Thread(runnable, "thread1").start(); new Thread(runnable, "thread2").start(); new Thread(runnable, "thread3").start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(new Random().nextInt(2000)); }catch (Exception e){} String ip = "Ip3"; list.add(ip); System.out.println(Thread.currentThread().getName() + " 添加了新的非法" + ip+ " + newBlackIP"); } }).start(); } }
1.1.4 CopyOnWriteArrayList原理
CopyOnWriteArrayList 内部使用了一种称为“写时复制”的机制。当需要进行写操作时,它会创建一个新的数组,并将原始数组的内容复制到新数组中,然后进行写操作。因此,读操作不会被写操作阻塞,读操作返回的结果可能不是最新的,但是对于许多应用场景来说,这是可以接受的。此外,由于读操作不需要加锁,因此它可以支持更高的并发度。1.1.5 CopyOnWriteArrayList 的缺陷
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;CopyOnWriteArrayList 合适读多写少的场景 ,不过这类慎用。因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。