CopyOnWriteArrayList には、書き込み操作時にリストの基になる配列のコピーが作成されるため、パフォーマンス上の欠点があります。配列のコピーにより、書き込み操作が遅くなります。おそらく、Read レートが高く、Write レートが低い List の使用には、CopyOnWriteArrayList が有利です。
最終的に、java.util.concurrent.locks,ReadWriteLock を使用して独自の実装のコーディングを開始しました。オブジェクト レベルの ReadWriteLock インスタンスを維持し、読み取り操作で読み取りロックを取得し、書き込み操作で書き込みロックを取得するだけで実装を行いました。コードは次のようになります。
public class ConcurrentList< T > implements List< T >
{
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final List< T > list;
public ConcurrentList( List<T> list )
{
this.list = list;
}
public boolean remove( Object o )
{
readWriteLock.writeLock().lock();
boolean ret;
try
{
ret = list.remove( o );
}
finally
{
readWriteLock.writeLock().unlock();
}
return ret;
}
public boolean add( T t )
{
readWriteLock.writeLock().lock();
boolean ret;
try
{
ret = list.add( t );
}
finally
{
readWriteLock.writeLock().unlock();
}
return ret;
}
public void clear()
{
readWriteLock.writeLock().lock();
try
{
list.clear();
}
finally
{
readWriteLock.writeLock().unlock();
}
}
public int size()
{
readWriteLock.readLock().lock();
try
{
return list.size();
}
finally
{
readWriteLock.readLock().unlock();
}
}
public boolean contains( Object o )
{
readWriteLock.readLock().lock();
try
{
return list.contains( o );
}
finally
{
readWriteLock.readLock().unlock();
}
}
public T get( int index )
{
readWriteLock.readLock().lock();
try
{
return list.get( index );
}
finally
{
readWriteLock.readLock().unlock();
}
}
//etc
}
観察されたパフォーマンスの改善は顕著でした。
10 スレッドによる 5000 回の読み取り + 5000 回の書き込み (読み取りと書き込みの比率は 1:1) にかかった合計時間は、
- ArrayList - 16450 ns (スレッドセーフではない)
- ConcurrentList - 20999 ns
- ベクトル -35696 ns
- CopyOnWriteArrayList - 197032 ns
上記の結果を取得するために使用されるテスト ケースの詳細については、このリンクをたどってください。
ただし、Iterator を使用するときに ConcurrentModificationException を回避するために、現在の List のコピーを作成し、その反復子を返しました。これは、このリストが戻らないことを意味し、元のリストを変更できる Iterator です。まあ、私にとっては、これは今のところ大丈夫です。
public Iterator<T> iterator()
{
readWriteLock.readLock().lock();
try
{
return new ArrayList<T>( list ).iterator();
}
finally
{
readWriteLock.readLock().unlock();
}
}
グーグルで調べた後、元のリストを変更できるイテレータを返さないため、CopyOnWriteArrayList にも同様の実装があることがわかりました。Javadoc によると、
返された反復子は、反復子が構築されたときのリストの状態のスナップショットを提供します。イテレータをトラバースしている間、同期は必要ありません。イテレータは remove メソッドをサポートしていません。