在Java中使用CopyOnWriteArrayList

在Java中使用CopyOnWriteArrayList,java,concurrency,Java,Concurrency,我现在正在学习java.util.concurrent。我正在努力理解CopyOnWriteArrayList 据我所知,这个类看起来像ArrayList,但却是线程安全的。如果你有大量的阅读和较少的写作,这门课是非常有用的 这是我的例子。我如何使用它(仅用于学习目的) 我可以那样用吗 package Concurrency; import java.util.concurrent.*; class Entry { private static int count; priv

我现在正在学习
java.util.concurrent
。我正在努力理解CopyOnWriteArrayList

据我所知,这个类看起来像
ArrayList
,但却是线程安全的。如果你有大量的阅读和较少的写作,这门课是非常有用的

这是我的例子。我如何使用它(仅用于学习目的)

我可以那样用吗

package Concurrency;

import java.util.concurrent.*;

class Entry {
    private static int count;
    private final int index = count++;
    public String toString() {
        return String.format(
                    "index:%-3d thread:%-3d", 
                    index, 
                    Thread.currentThread().getId()); 
    }

}

class Reader implements Runnable {
    private CopyOnWriteArrayList<Entry> list;
    Reader(CopyOnWriteArrayList<Entry> list) { this.list = list; }
    public void run() {
            try {
                while(true) {
                    if(!list.isEmpty())
                        System.out.println("-out " + list.remove(0));
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            } catch (InterruptedException e) {
                return;
            }
    }
}


class Writer implements Runnable {
    private CopyOnWriteArrayList<Entry> list;
    Writer(CopyOnWriteArrayList<Entry> list) { this.list = list; }
    public void run() {
        try {
            while(true) {
                Entry tmp = new Entry();
                System.out.println("+in  " + tmp);
                list.add(tmp);
                TimeUnit.MILLISECONDS.sleep(10);
            }
        } catch (InterruptedException e) {
            return;
        }
    }
}

public class FourtyOne {
    static final int nThreads = 7;
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<Entry> list = new CopyOnWriteArrayList<>();
        ExecutorService exec = Executors.newFixedThreadPool(nThreads);
        exec.submit(new Writer(list));
        for(int i = 0; i < nThreads; i++)
            exec.submit(new Reader(list));
        TimeUnit.SECONDS.sleep(1);
        exec.shutdownNow();
    }

}
包并发;
导入java.util.concurrent.*;
班级报名{
私有静态整数计数;
私有最终整数索引=计数++;
公共字符串toString(){
返回字符串格式(
“索引:%-3d线程:%-3d”,
指数
Thread.currentThread().getId());
}
}
类读取器实现可运行{
私有CopyOnWriteArrayList列表;
读取器(CopyOnWriteArrayList){this.list=list;}
公开募捐{
试一试{
while(true){
如果(!list.isEmpty())
System.out.println(“-out”+list.remove(0));
时间单位。毫秒。睡眠(100);
}
}捕捉(中断异常e){
返回;
}
}
}
类编写器实现可运行的{
私有CopyOnWriteArrayList列表;
编写器(CopyOnWriteArrayList){this.list=list;}
公开募捐{
试一试{
while(true){
条目tmp=新条目();
系统输出打印项次(“+in”+tmp);
添加列表(tmp);
时间单位。毫秒。睡眠(10);
}
}捕捉(中断异常e){
返回;
}
}
}
公共类41{
静态最终读数=7;
公共静态void main(字符串[]args)引发InterruptedException{
CopyOnWriteArrayList=新建CopyOnWriteArrayList();
ExecutorService exec=Executors.newFixedThreadPool(n线程);
执行官提交(新作者(名单));
对于(int i=0;i
请注意,在您的示例中,您的一位作者的写作速度是给定阅读器的10倍,导致大量副本被复制。还要注意,您的读卡器也正在对列表执行写入操作(
remove()

在这种情况下,您正在以惊人的速度写入列表,这会导致严重的性能问题,因为每次更新此列表时都会使用大量内存

CopyOnWriteArrayList仅在存在同步开销且读取与结构修改的比率较高时使用。当一个或多个读卡器尝试同时访问列表时,一个总阵列拷贝的成本由性能增益分摊。这与传统的同步列表不同,在传统的同步列表中,每个访问(读或写)都在某个互斥锁下控制,这样一次只有一个线程可以对列表执行某些操作

如果需要一个简单的线程安全列表,请考虑由<代码>集合提供的同步列表.SimuleDistList.()/代码> ./P> 另请注意:

if(!list.isEmpty()){
    System.out.println("-out " + list.remove(0));
}
编程无效,因为无法保证在if语句求值后列表不会为空。为了保证一致的效果,您需要直接检查
list.remove()
的返回值,或者将整个段包装在
synchronized
块中(不符合使用线程安全结构的目的)

作为结构修改调用的
remove()
调用也应替换为类似
get()
的方法,以确保在读取数据时不会进行结构修改


总之,我相信CopyOnWriteArrayList只需要以一种非常特定的方式使用,并且只需要在传统的同步变得令人无法接受的缓慢时使用。虽然您的示例可能在您自己的计算机上运行良好,但如果访问量的比例再大,您将导致gc在维护堆空间方面做太多工作。

大量读取而几乎没有写入。这很简单,真的。每当您的代码调用将修改列表的方法时,该方法都会创建数组的副本,修改其副本,然后通过单个原子操作将新副本替换为旧副本。如果任何其他线程同时尝试访问列表,则另一个线程将访问旧版本或新版本,但它保证永远不会访问处于某种无效中间状态的版本。您正在使用ExecutorService(即线程池)执行永不结束的任务(
while(true)…
)。不要那样做。我并不是说它不起作用,但这不是线程池的用途。我会使用
new Thread(…)
threadFactory.newThread(…)
来创建一个永不结束的线程。您的使用者(读者)盲目地相信,如果
list.isEmpty()
返回true,那么列表中将包含某些内容。这是一个天真的假设。当只有一个消费者时,这是真的,但在许多系统中,消费者不止一个。您应该养成编写使用者代码的习惯,如果唤醒它的事件已经由其他使用者线程提供服务,则不会抛出NullPointerException。
ConcurrentLinkedQueue
也是线程安全的,它可能更适合您尝试执行的操作。谢谢您的回答。坦率地说,我不喜欢这种方法
(虽然(正确))
。也许最好在(!Thread.interrupted)或类似的时候使用
。我还了解到,当列表包含某些内容时,
list.isEmpty()
将返回true。如果(!list.isEmpty?
,我可以评论
?或者我可以通过不同的延迟来处理它
sleep()
?谢谢您的回答。我明白了-我应该使用
sy