简体   繁体   English

AtomicReference到可变对象和可见性

[英]AtomicReference to a mutable object and visibility

Say I have an AtomicReference to a list of objects: 假设我有一个对象列表的AtomicReference

AtomicReference<List<?>> batch = new AtomicReference<List<Object>>(new ArrayList<Object>());

Thread A adds elements to this list: batch.get().add(o); 线程A将元素添加到此列表: batch.get().add(o);

Later, thread B takes the list and, for example, stores it in a DB: insertBatch(batch.get()); 稍后, 线程B获取列表,例如,将其存储在DB: insertBatch(batch.get());

Do I have to do additional synchronization when writing (Thread A) and reading (Thread B) to ensure thread B sees the list the way A left it, or is this taken care of by the AtomicReference? 在写入(线程A)和读取(线程B)时,是否必须执行其他同步以确保线程B以A的方式查看列表,或者由AtomicReference处理?

In other words: if I have an AtomicReference to a mutable object, and one thread changes that object, do other threads see this change immediately? 换句话说:如果我有一个可变对象的AtomicReference,并且一个线程更改了该对象,其他线程是否会立即看到此更改?

Edit: 编辑:

Maybe some example code is in order: 也许一些示例代码是有序的:

public void process(Reader in) throws IOException {
    List<Future<AtomicReference<List<Object>>>> tasks = new ArrayList<Future<AtomicReference<List<Object>>>>();
    ExecutorService exec = Executors.newFixedThreadPool(4);

    for (int i = 0; i < 4; ++i) {
        tasks.add(exec.submit(new Callable<AtomicReference<List<Object>>>() {
            @Override public AtomicReference<List<Object>> call() throws IOException {

                final AtomicReference<List<Object>> batch = new AtomicReference<List<Object>>(new ArrayList<Object>(batchSize));

                Processor.this.parser.parse(in, new Parser.Handler() {
                    @Override public void onNewObject(Object event) {
                            batch.get().add(event);

                            if (batch.get().size() >= batchSize) {
                                dao.insertBatch(batch.getAndSet(new ArrayList<Object>(batchSize)));
                            }
                    }
                });

                return batch;
            }
        }));
    }

    List<Object> remainingBatches = new ArrayList<Object>();

    for (Future<AtomicReference<List<Object>>> task : tasks) {
        try {
            AtomicReference<List<Object>> remainingBatch = task.get();
            remainingBatches.addAll(remainingBatch.get());
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();

            if (cause instanceof IOException) {
                throw (IOException)cause;
            }

            throw (RuntimeException)cause;
        }
    }

    // these haven't been flushed yet by the worker threads
    if (!remainingBatches.isEmpty()) {
        dao.insertBatch(remainingBatches);
    }
}

What happens here is that I create four worker threads to parse some text (this is the Reader in parameter to the process() method). 这里发生的是我创建了四个工作线程来解析一些文本(这是process()方法中的Reader in参数)。 Each worker saves the lines it has parsed in a batch, and flushes the batch when it is full ( dao.insertBatch(batch.getAndSet(new ArrayList<Object>(batchSize))); ). 每个工作程序保存它在批处理中解析的行,并在批处理完成时刷新批处理( dao.insertBatch(batch.getAndSet(new ArrayList<Object>(batchSize))); )。

Since the number of lines in the text isn't a multiple of the batch size, the last objects end up in a batch that isn't flushed, since it's not full. 由于文本中的行数不是批处理大小的倍数,因此最后一个对象最终会在未刷新的批处理中结束,因为它未满。 These remaining batches are therefore inserted by the main thread. 因此,这些剩余的批次由主线程插入。

I use AtomicReference.getAndSet() to replace the full batch with an empty one. 我使用AtomicReference.getAndSet()将整个批处理替换为空批处理。 It this program correct with regards to threading? 这个程序在线程方面是否正确?

Um... it doesn't really work like this. 嗯......它真的不像这样。 AtomicReference guarantees that the reference itself is visible across threads ie if you assign it a different reference than the original one the update will be visible. AtomicReference保证引用本身在线程中是可见的,即如果为其指定的引用与原始引用不同,则更新将是可见的。 It makes no guarantees about the actual contents of the object that reference is pointing to. 它不保证引用所指向的对象的实际内容。

Therefore, read/write operations on the list contents require separate synchronization. 因此,对列表内容的读/写操作需要单独同步。

Edit : So, judging from your updated code and the comment you posted, setting the local reference to volatile is sufficient to ensure visibility. 编辑 :因此,根据您更新的代码和您发布的评论判断,将本地引用设置为volatile足以确保可见性。

I think that, forgetting all the code here, you exact question is this: 我想,忘了这里的所有代码,你确切的问题是:

Do I have to do additional synchronization when writing (Thread A) and reading (Thread B) to ensure thread B sees the list the way A left it, or is this taken care of by the AtomicReference? 在写入(线程A)和读取(线程B)时,是否必须执行其他同步以确保线程B以A的方式查看列表,或者由AtomicReference处理?

So, the exact response to that is: YES , atomic take care of visibility. 因此,对此的确切响应是: 是的 ,原子能够照顾可见性。 And it is not my opinion but the JDK documentation one : 这不是我的意见,而是JDK文档之一

The memory effects for accesses and updates of atomics generally follow the rules for volatiles, as stated in The Java Language Specification, Third Edition (17.4 Memory Model). 访问和更新原子的记忆效应通常遵循挥发性规则,如Java语言规范,第三版(17.4内存模型)中所述。

I hope this helps. 我希望这有帮助。

Adding to Tudor 's answer: You will have to make the ArrayList itself threadsafe or - depending on your requirements - even larger code blocks. 添加到都铎的回答是:你不得不作出的ArrayList本身线程或-根据您的要求-即使是较大的代码块。

If you can get away with a threadsafe ArrayList you can "decorate" it like this: 如果你可以使用线程安全的ArrayList你可以像这样“装饰”它:

batch = java.util.Collections.synchronizedList(new ArrayList<Object>());

But keep in mind: Even "simple" constructs like this are not threadsafe with this: 但请记住:即使像这样的“简单”构造也不是线程安全的:

Object o = batch.get(batch.size()-1);

The AtomicReference will only help you with the reference to the list, it will not do anything to the list itself. AtomicReference只会帮助您对列表的引用,它不会对列表本身做任何事情。 More particularly, in your scenario, you will almost certainly run into problems when the system is under load where the consumer has taken the list while the producer is adding an item to it. 更具体地说,在您的方案中,当系统处于负载状态时,您几乎肯定会遇到问题,而消费者在生产者向其添加项目时已采用该列表。

This sound to me like you should be using a BlockingQueue . 这听起来像你应该使用BlockingQueue You can then Limit the memory footprint if you producer is faster than your consumer and let the queue handle all contention. 如果生产者比消费者更快并且让队列处理所有争用,则可以限制内存占用。

Something like: 就像是:

ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<Object> (50);

// ... Producer
queue.put(o);

// ... Consumer
List<Object> queueContents = new ArrayList<Object> ();
// Grab everything waiting in the queue in one chunk. Should never be more than 50 items.
queue.drainTo(queueContents);

Added 添加

Thanks to @Tudor for pointing out the architecture you are using. 感谢@Tudor指出您正在使用的架构。 ... I have to admit it is rather strange. ......我不得不承认这很奇怪。 You don't really need AtomicReference at all as far as I can see. 就我所见,你根本不需要AtomicReference Each thread owns its own ArrayList until it is passed on to dao at which point it is replaced so there is no contention at all anywhere. 每个线程拥有自己的ArrayList直到它被传递给dao ,此时它被替换,因此在任何地方都没有争用。

I am a little concerned about you creating four parser on a single Reader . 我有点担心你在一个Reader上创建四个解析器。 I hope you have some way of ensuring each parser does not affect the others. 我希望你有办法确保每个解析器不会影响其他解析器。

I personally would use some form of producer-consumer pattern as I have described in the code above. 我个人会使用某种形式的生产者 - 消费者模式,正如我在上面的代码中描述的那样。 Something like this perhaps. 或许这样的事情。

static final int PROCESSES = 4;
static final int batchSize = 10;

public void process(Reader in) throws IOException, InterruptedException {

  final List<Future<Void>> tasks = new ArrayList<Future<Void>>();
  ExecutorService exec = Executors.newFixedThreadPool(PROCESSES);
  // Queue of objects.
  final ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<Object> (batchSize * 2);
  // The final object to post.
  final Object FINISHED = new Object();

  // Start the producers.
  for (int i = 0; i < PROCESSES; i++) {
    tasks.add(exec.submit(new Callable<Void>() {
      @Override
      public Void call() throws IOException {

        Processor.this.parser.parse(in, new Parser.Handler() {
          @Override
          public void onNewObject(Object event) {
            queue.add(event);
          }
        });
        // Post a finished down the queue.
        queue.add(FINISHED);
        return null;
      }
    }));
  }

  // Start the consumer.
  tasks.add(exec.submit(new Callable<Void>() {
    @Override
    public Void call() throws IOException {
      List<Object> batch = new ArrayList<Object>(batchSize);
      int finishedCount = 0;
      // Until all threads finished.
      while ( finishedCount < PROCESSES ) {
        Object o = queue.take();
        if ( o != FINISHED ) {
          // Batch them up.
          batch.add(o);
          if ( batch.size() >= batchSize ) {
            dao.insertBatch(batch);
            // If insertBatch takes a copy we could merely clear it.
            batch = new ArrayList<Object>(batchSize);
          }
        } else {
          // Count the finishes.
          finishedCount += 1;
        }
      }
      // Finished! Post any incopmplete batch.
      if ( batch.size() > 0 ) {
        dao.insertBatch(batch);
      }
      return null;
    }
  }));

  // Wait for everything to finish.
  exec.shutdown();
  // Wait until all is done.
  boolean finished = false;
  do {
    try {
      // Wait up to 1 second for termination.
      finished = exec.awaitTermination(1, TimeUnit.SECONDS);
    } catch (InterruptedException ex) {
    }
  } while (!finished);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM