エントリから高度な(4)へのマルチスレッド---マルチスレッド環境でのコレクション安全クラス-リスト

1つは、ArrayListの不安定さです。

public class ArrayListDemo {
    
    

    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
    
    
            new Thread(() -> {
    
    
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

上記のメソッドを実行し、書き込みと読み取りを同時に行う30のスレッドを作成すると、例外ConcurrentModificationExceptionがスローされます。例外の理由は非常に単純で、ArrayListコレクションクラスはスレッドセーフではないため、複数のスレッドが読み取りと同時に書き込むと例外がスローされ、問題が発生しますか?Listコレクションの下でスレッドセーフの問題を解決するにはどうすればよいですか?ベクター?これは良い方法ですか?

2、ベクトル

リストコレクションのセキュリティ問題を解決することになると、ほとんどの学生はベクターに答えることができます。ベクターはリストコレクションのスレッドセーフ問題を確実に解決できます。ベクターがリストコレクションのスレッドセーフ問題をどのように解決するかを簡単に分析しましょう。

Vectorの最下層もArrayListのような配列を使用しており、以下のソースコードを投稿して見てください。

メソッドの追加

public synchronized boolean add(E e) {
    
    
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

getメソッド

public synchronized E get(int index) {
    
    
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

VectorでSynchronizedキーワードが使用されていることがわかります。その結果、効率が非常に低くなります。

三、同期リスト

スレッドセーフなリストと言えば、Collectionsパッケージの下のSynchronizedListについて言及する必要があります。このクラスの名前を見ると、これがSynchronizedがListコレクションに追加されたことがわかります。

このクラスのいくつかのメソッドを見てください

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
    
    
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

        SynchronizedList(List<E> list) {
    
    
            super(list);
            this.list = list;
        }
        SynchronizedList(List<E> list, Object mutex) {
    
    
            super(list, mutex);
            this.list = list;
        }

        public boolean equals(Object o) {
    
    
            if (this == o)
                return true;
            synchronized (mutex) {
    
    return list.equals(o);}
        }
        public int hashCode() {
    
    
            synchronized (mutex) {
    
    return list.hashCode();}
        }

        public E get(int index) {
    
    
            synchronized (mutex) {
    
    return list.get(index);}
        }
        public E set(int index, E element) {
    
    
            synchronized (mutex) {
    
    return list.set(index, element);}
        }
        public void add(int index, E element) {
    
    
            synchronized (mutex) {
    
    list.add(index, element);}
        }
        public E remove(int index) {
    
    
            synchronized (mutex) {
    
    return list.remove(index);}
        }

        public int indexOf(Object o) {
    
    
            synchronized (mutex) {
    
    return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
    
    
            synchronized (mutex) {
    
    return list.lastIndexOf(o);}
        }

        public boolean addAll(int index, Collection<? extends E> c) {
    
    
            synchronized (mutex) {
    
    return list.addAll(index, c);}
        }

        public ListIterator<E> listIterator() {
    
    
            return list.listIterator(); // Must be manually synched by user
        }

        public ListIterator<E> listIterator(int index) {
    
    
            return list.listIterator(index); // Must be manually synched by user
        }

        public List<E> subList(int fromIndex, int toIndex) {
    
    
            synchronized (mutex) {
    
    
                return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }

    }

SynchronizedListはListインターフェースを実装していることがわかります。つまり、拡張可能です。Listオブジェクトを渡すことができます。ただし、上記のメソッドから、同期ロックが各addおよびgetメソッドに追加されていることがわかります。 SynchronizedListの効率がそれほど高くないため、スレッドセーフを実現する方法

公式文書に記載

List list = Collections.synchronizedList(new ArrayList());
...
    synchronized (list) {
    
    
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext()){
    
    }
        foo(i.next());
}

奇妙なことに、Synchronizedはスレッドセーフなので、反復するときにロックのレイヤーを追加するのはなぜですか?SynchronizedListの実装コードでは、iterator()は再度カプセル化されません。Synchronizedは、読み取り操作にイテレーターを使用すると、同時実行性が高い場合にスレッドセーフが達成されないため、Synchronizedを使用する場合は手動で追加する必要があります。 。

したがって、SynchronizedListは、高性能を必要とせず、Iteratorを使用しない状況に適しています。

SynchronizedListとVectorの主な違いは次のとおりです。

  1. Vectorは元の長さの2倍に拡張され、ArrayListは元の長さの1.5倍に拡張されます
  2. SynchronizedListには、非常に優れた拡張機能と互換性機能があります。彼はすべてのリストサブクラスをスレッドセーフクラスに変換できます。
  3. SynchronizedListを使用する場合、トラバース時に手動で同期を実行する必要があります。
  4. SynchronizedListは、ロックされたオブジェクトを指定できます。

4、CopyOnWriteArrayList

VectorとSynchronizedListのパフォーマンスはそれほど高くないので、高いパフォーマンスはありますか?CopyOnWriteArrayListのパフォーマンスは、書き込みを減らして読み取りを増やすという特定のケースでは、上記の2つのコレクションよりもはるかに高くなります。

CopyOnWriterArrayListはjava.util.concurrentパッケージの下にあり、同時実行性の高い環境でJDKによって提供される高性能リストです。CopyOnWriterArrayListは、実際には読み取りと書き込みの分離を考慮したものです。CopyOnWriterArrayListを追加すると、最初にコンテナーがコピーされ、コンテナーに要素を追加した後、元のコンテナーの参照が新しいコンテナーに戻されます。このアプローチの利点読み取り時にロックする必要がなく、高い同時実行性で読み取ることができます

ソースコードの追加を見てみましょう

/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array
    
    
public boolean add(E e) {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
    
    
        lock.unlock();
    }
}

private E get(Object[] a, int index) {
    
    
    return (E) a[index];
}

ソースコードは非常に明確に記述されており、最初に現在の配列を取得してから、ArraysのcopyOfメソッドを使用して現在の配列をコピーします。これはローカル変数であるため、現在のスレッドのワークスペースにのみ存在するため、コピーされたコンテナはスレッドセーフである必要があり、要素を追加してから、新しいコンテナのアドレスを元のコンテナに追加します

getメソッドはインデックスを使用してデータを返すため、書き込みがロックされている間、CopyOnWriteArrsyListの読み取りを同時に実行できます。したがって、この種のコンテナは、読み取りが多く、書き込みが少ない場合に非常に適しています。

注意が必要な詳細がもう1つあります。CopyOnWriteArrayListの配列はVolatileによって変更されます。これは、時間内の可視性を確保することを目的としています。そうしないと、新しい配列にコピーした後、他のスレッドがそれを見ることができず、面倒です。

一般に、CopyOnWriteArrayListは、読み取りと書き込みを分離するという考え方です。つまり、読み取りと書き込みは同じコンテナー内にありません。これは、データのリアルタイムの一貫性を保証するものではありませんが、の最終的な一貫性を保証することはできます。データであり、書き込みの読み取りにのみ適しています。書き込みが多すぎると、新しい配列をコピーすると大量のメモリが消費されます。

おすすめ

転載: blog.csdn.net/weixin_44706647/article/details/114829889