2015-02-23 3 views
1

Сейчас я изучаю java.util.concurrent. Я пытаюсь понять CopyOnWriteArrayList.Использование CopyOnWriteArrayList в Java

Как я понял, этот класс выглядит как 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(); 
    } 

} 
+0

Много чтения и почти не написано. На самом деле все просто. Всякий раз, когда ваш код вызывает метод, который будет изменять список, этот метод создает копию массива, он изменяет его копию, а затем он заменяет новую копию для старого с помощью одной атомной операции. Если какой-либо другой поток пытается получить доступ к списку в одно и то же время, другой поток будет либо обращаться к старой версии, либо к новой версии, но гарантированно никогда не получит доступ к версии, которая находится в некотором недействительном промежуточном состоянии. –

+0

Вы используете ExecutorService (т. Е. Пул потоков) для выполнения задач, которые никогда не заканчиваются ('while (true) ...'). Не делай этого. Я не говорю, что это не сработает, но это не те пулы потоков. Я бы использовал 'new Thread (...)' или 'threadFactory.newThread (...)', чтобы создать поток, который никогда не заканчивается. –

+0

Ваш потребитель (читатель) слепо полагает, что если 'list.isEmpty()' возвращает true, тогда в списке будет что-то в нем. Это наивное предположение. Это будет верно, когда есть только один потребитель, но во многих системах существует более одного потребителя. Вы должны привыкнуть писать код потребителя, который не будет генерировать исключение NullPointerException, если событие, которое его разбудило, уже обслуживается каким-то другим потребительским потоком. –

ответ

1

Пожалуйста, обратите внимание, в вашем примере ваш один автор пишет на 10x скорость данного читателя, вызывая много копий, которые будут сделаны. Также обратите внимание, что ваш читатель (-и) выполняет запись (remove()) в списке.

В этой ситуации вы пишите в список с удивительно высокой скоростью, вызывая серьезные проблемы с производительностью, поскольку при обновлении этого списка используется большое количество памяти.

CopyOnWriteArrayList используется только в том случае, когда накладные расходы на синхронизацию являются проблемой, а отношение чтений к структурной модификации является высоким. Стоимость полной копии массива амортизируется приростом производительности, наблюдаемым при одновременном доступе к одному или нескольким читателям. Это контрастирует с традиционным синхронизированным списком, где каждый доступ (чтение или запись) контролируется под некоторым мьютексом, так что только один поток может выполнять некоторую операцию в списке сразу.

Если требуется простой поточно-безопасный список, ознакомьтесь с синхронизированным списком, предоставленным Collections.synchronizedList().

Обратите внимание:

if(!list.isEmpty()){ 
    System.out.println("-out " + list.remove(0)); 
} 

не является эффективным программирования, поскольку нет никакой гарантии, список будет не быть пустым после того, если оператор оценивает. Чтобы гарантировать согласованный эффект, вам нужно либо прямо проверить возвращаемое значение list.remove(), либо обернуть весь сегмент в блоке synchronized (побеждая цель использования поточно-безопасной структуры).

Звонок remove() также должен быть заменен на метод, подобный get(), чтобы гарантировать, что во время считывания данных не выполняются никакие структурные изменения.

В целом, я считаю, что CopyOnWriteArrayList нужно использовать только определенным образом и только тогда, когда традиционная синхронизация становится неприемлемо медленной. Пока ваш пример может работать нормально на вашем собственном компьютере, масштабируя масштаб доступа, который будет больше, и вы заставите gc делать слишком много работы, чтобы поддерживать кучу пространства.

+0

Спасибо за ответы. Я понял - я должен использовать 'synchronized (list) {...}' или 'if (list.remove (0)! = Null) {...}'. – Alex

+0

@Alex Обратите внимание, что если вы используете 'synchronized (list) {...}', вам больше не нужна потокобезопасная структура. Пока вы последовательно используете 'synchronized (list) {...}' для каждого доступа (чтение ** и ** запись), вы можете использовать более быстрые «небезопасные» объекты, такие как ArrayList, напрямую. – initramfs

+0

Хорошо. Считаете ли вы, что это будет быстрее, вместо использования 'CopyOnWriteArrayList'? – Alex

Смежные вопросы