5분 안에 0에서 1까지 CopyOnWriteArrayList를 살펴보세요.

### 5분 안에 0에서 1까지 CopyOnWriteArrayList를 살펴보세요.

### 서문

최근 기사들은 모두 동시성 프로그래밍에 관해 작성되었으며, 이 기간 동안 동시성 패키지의 일부 동시 컨테이너에 대해 작성하고 하나씩 분석하여 동시성 패키지의 동시성 컨테이너에 대해 철저히 이해하겠습니다.

`CopyOnWriteArrayList`를 살펴보기 전에 먼저 `ArrayList`를 동시 시나리오에서 사용할 수 없는 이유에 대해 이야기해 보겠습니다.

### 동시 시나리오의 ArrayList

**ArrayList 배열은 동적 확장 및 임의 액세스를 지원합니다...**

일상 작업에서 가장 일반적으로 사용되는 컬렉션 클래스이므로 이미 익숙하다고 생각하지만 이러한 종류의 컬렉션은 동시 시나리오에서는 안전하지 않습니다.

**동시 읽기 및 쓰기가 발생하면 JDK는 `ConcurrentModificationException` 동시 수정 예외를 발생시키는 `fail-fast` 메커니즘을 제공합니다**

예를 들어 다음 코드를 살펴보세요. 10개의 스레드를 시작하여 컬렉션에 요소를 추가한 다음 이를 읽으면 동시 수정 예외가 발생합니다.

``java
    public void testCopyOnWriteArrayList()가 InterruptedException을 발생시킵니다. {
        List<String> list = new ArrayList();

        for (int i = 0; i < 10; i++) {
            new Thread(()-> {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println( 목록);
            },String.valueOf(i)).start();
        }

        TimeUnit.SECONDS.sleep(3);
    }
````

콘솔에 인쇄할 때 컬렉션의 `toString` 메서드가 실제로 호출됩니다.

상위 클래스 `AbstractCollection`은 `toString` 메서드를 재정의합니다. 반복자를 사용하여 배열을 순회하고 문자열과 연결합니다.

``java
    public String toString() {
        Iterator<E> it = iterator();
        if (!it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(이 컬렉션)" : e);
            if (!it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }
```

그렇다면 빠른 실패는 어떻게 달성됩니까?

추가, 삭제, 수정 등 컬렉션에 대한 쓰기 작업을 수행할 때 내부 필드 'modCount'가 증가하여 수정 횟수를 나타냅니다.

**반복자를 가져올 때 `modCount`는 반복자 내부의 `expectedModCount` 필드에 할당됩니다**

**반복자를 순회할 때 두 필드가 동일한지 확인하고, 다르다면 다른 스레드가 쓰기 작업을 수행하고 있다는 의미이므로 동시 수정 예외**가 발생하여 빠른 실패를 달성합니다.

``java
        final void checkForComodification() {
            if (modCount != ExpectModCount)
                throw new ConcurrentModificationException();
        }
````

### 동시 시나리오의 솔루션

그런데 ArrayList는 동시 시나리오에서 사용할 수 없습니다. 그렇다면 해결책은 무엇입니까?

고대 버전에서는 동시 시나리오에서 `Vector` 또는 `Collections.synchronizedList(new ArrayList<>())`를 사용합니다.

읽기 및 쓰기 작업은 모두 '동기화'로 잠겨 원자성을 보장합니다.

어떤 학생들은 '동기화'의 성능이 너무 나빠서 사용하지 않는다고 말합니다.

그런데 잠금 업그레이드 및 최적화 후에도 `동기화` 성능이 여전히 좋지 않습니까? 관심 있으신 분들은 [Synchronized에 대한 이해를 돕기 위한 15,000단어, 6개의 코드 케이스, 5개의 회로도](https://juejin.cn/post/7272015112819556412)를 확인하실 수 있습니다.

사실 잠금의 세분성이 너무 크다고 생각합니다. 동시 시나리오에서 읽기 작업은 잠금을 통해 액세스해야 합니까?

휘발성에 익숙한 학생들은 그것이 필요하지 않다는 것을 알아야 합니다.

동시 읽기 및 쓰기에서 해결해야 할 문제는 스레드가 컬렉션을 수정할 때 다른 스레드가 이를 읽을 때 어떻게 표시할 수 있는지, 즉 가시성을 어떻게 확보할 수 있는가 하는 것입니다.

가시성을 보장하려면 휘발성을 사용하고 읽기 시나리오에서는 잠금이 필요하지 않습니다.

이해가 안 되시면 [0~1까지의 휘발성 키워드를 이해하는데 도움이 되는 5가지 사례와 흐름도](https://juejin.cn/post/7270869041631002636)를 참고하시면 됩니다.

오랜 준비 끝에 이번 글의 주인공 `CopyOnWriteArrayList`를 살펴보겠습니다.

### CopyOnWrite

`CopyOnWriteArrayList`는 이름에서 `CopyOnWrite`처럼 보입니다. 이는 쓰기 중 복사를 의미합니다. 

글을 쓰면서 COW를 베끼는 아이디어는 무엇인가요?

**COW는 쓰기 시 원본 데이터를 복사하고, 쓰기 후에 다시 설정하는 것입니다**

이 아이디어는 동시성 시나리오에서 매우 일반적입니다. 예를 들어 Redis 영구 RDB 및 AOF 파일은 COW를 사용합니다. MySQL에 의해 구현된 MVCC 버전 체인

### CopyOnWriteArrayList

다음으로 소스코드에서 어떻게 구현되는지 살펴보겠습니다.

생성하는 동안 길이가 0인 배열이 초기화됩니다.

이상하긴 하지만 COW를 생각해보면 글을 쓸 때 데이터의 복사본을 복사한 뒤 다시 설정하면 바로 정상이 됩니다.

``java
    final void setArray(Object[] a) {
        array = a;
    }

    공개 CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
````

읽기 작업을 살펴보겠습니다.

``java
    public E get(int index) {
        return get(getArray(), index);
    }

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

읽기 작업은 특정 인덱스의 데이터를 읽는 데 매우 일반적입니다.

잠금을 사용하지 않으므로 데이터를 저장하는 필드는 휘발성을 사용해야 합니다.

``java
private 임시 휘발성 Object[] 배열;
````

이런 종류의 휘발성은 가시성을 보장하며, 동시 시나리오에서 잠금 없이 읽기 작업을 허용한다는 아이디어도 매우 일반적입니다.예를 들어 AQS의 휘발성은 동기화 상태 가시성을 보장합니다.

다음으로 쓰기 작업을 살펴보겠습니다 - 추가

``java
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //Lock
    lock.lock();
    try {
        //원본 배열 가져오기
        Object[] elements = getArray();
        int len = elements .length;
        //원래 배열의 데이터를 새 배열에 복사합니다.
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //새 데이터 추가
        newElements[len] = e;
        //재설정 새로운 배열 back
        setArray(newElements);
        return true;
    } finally {
        //Unlock
        lock.unlock();
    }
}
```

동시 쓰기 중에 하나의 스레드만 쓰기 작업을 수행할 수 있도록 쓰기 작업 중에 잠금이 수행되므로 스레드 안전성이 보장됩니다.

복사에는 성능이 더 좋은 `System.arrayCopy`가 사용되지만 쓰기 작업이 너무 많으면 성능 오버헤드도 너무 커집니다.

``java
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<?extends T[]> newType) { @SuppressWarnings("unchecked ") //새 배열
        생성
        T
        [ ] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        / / 이전 배열의 이전 데이터를 새 배열로 복사합니다
        . System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
```

반복자를 얻을 때 원본 데이터를 사용하여 새 객체가 생성된 다음 이 객체에서 반복됩니다.

따라서 스레드 안전하지 않은 동시 수정 문제가 발생하지 않습니다. 이는 동시성 패키지에 제공되는 오류 방지 메커니즘입니다.

``java
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
````

### 사용 장면

분석 후에는 'CopyOnWriteArrayList'의 특성을 알 수 있으므로 이에 적용 가능한 사용 시나리오를 분석할 수 있습니다.

**휘발성을 사용하여 메모리 가시성을 보장하고 읽기 작업에는 잠금이 필요하지 않으므로 성능이 매우 좋습니다. 배열에 대한 무작위 액세스와 결합하면 읽기 시나리오에 매우 적합합니다**

**쓰기 작업을 수행할 때 JUC에서 'ReentrantLock'을 사용하여 원자성을 보장하기 위해 잠금 및 잠금 해제를 해야 하며, 쓰기 중에 데이터를 복사해야 하므로 데이터 복사에 오버헤드가 발생하므로 쓰기 작업이 빈번한 시나리오에는 적합하지 않습니다**

**데이터 볼륨이 점점 커지면 쓰기 작업을 위해 더 많은 데이터가 복사되므로 쓰기 작업 전에 많은 양의 데이터를 저장하는 데 적합하지 않습니다**

**안전한 실패를 제공합니다.iterator를 사용할 때 반드시 최신 실시간 데이터가 반복되는 것은 아니지만 동시 수정 예외는 발생하지 않습니다**

### 요약하다

**`ArrayList`는 `fail-fast` 빠른 실패 메커니즘을 제공합니다. 동시 읽기 및 쓰기 시나리오에서 이 메커니즘은 빠른 실패를 달성하기 위해 동시 수정 예외를 발생시키는 데 사용됩니다. 동시 시나리오에서는 안전하지 않습니다**

**동시 시나리오에는 많은 솔루션이 있지만 `CopyOnWriteArrayList`는 `Vector` 또는 `Collections.synchronizedList(new ArrayList<>())`보다 잠금 세분성이 낮고 성능이 더 좋습니다**

**`CopyOnWriteArrayList`는 읽기 시나리오를 잠그지 않고 가시성을 보장하기 위해 휘발성을 사용하여 배열을 수정합니다**

**쓰기 작업의 원자성을 보장하기 위해 쓰기 시나리오에서 'ReentrantLock'을 사용합니다. 쓰기 시 원본 데이터의 복사본이 먼저 복사된 다음 수정 후 새 데이터로 설정됩니다**

**iterator를 얻을 때, iteration을 위해 원본 데이터를 통해 새로운 객체도 생성됩니다. 이를 통해 데이터가 최신이 아닐지라도 예외가 발생하지 않습니다**

**`CopyOnWriteArrayList`는 데이터 양이 크지 않고, 읽기는 많고 쓰기는 적으며, 반복 중에 약한 일관성이 허용될 수 있는 시나리오에 적합합니다**

**데이터 양이 특히 많고 쓰기 작업이 많은 시나리오에서는 쓰기 중 복사의 오버헤드가 매우 높을 수 있으므로 사용하지 마세요**

### 마지막으로 (성매매를 괜히 이용하지 마시고 3번 연속으로 눌러 도움을 요청하세요~)

본 글은 [점에서 선으로, 선에서 표면으로, 간단한 용어로 자바 동시 프로그래밍 지식 시스템 구축](https://juejin.cn/column/7270046881542766604) 칼럼에 포함되어 있습니다. .

이 글의 노트와 사례는 [gitee-StudyJava](https://gitee.com/tcl192243051/StudyJava), [github-StudyJava](https://github.com/Tc-liang/StudyJava)에 포함되어 있습니다. 관심있는 분들은 계속해서 팔로우 하시면 됩니다~

케이스 주소:

[Gitee-JavaConcurrentProgramming/src/main/java/F_Collections](https://gitee.com/tcl192243051/StudyJava/tree/master/Java 동시 프로그래밍/JavaConcurrentProgramming/src/main/java/F_Collections)

[Github-JavaConcurrentProgramming/src/main/java/F_Collections](https://github.com/Tc-liang/StudyJava/tree/main/%E6%B7%B1%E5%85%A5%E6%B5%85 %E5%87%BAJava%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/JavaConcurrentProgramming/src/main/java/F_Collections)

궁금한 사항은 댓글로 토론 가능하며, 까이까이의 글이 좋다고 생각하시면 좋아요, 팔로우, 모아서 응원해주시면 됩니다~

까이까이를 팔로우하고 더 유용한 정보를 공유해보세요, 공개 계정: 까이까이의 백엔드 프라이빗 주방

레이쥔: 샤오미의 새로운 운영 체제인 ThePaper OS의 공식 버전이 패키징되었습니다. Gome App 복권 페이지의 팝업 창이 창업자를 모욕합니다. 미국 정부는 NVIDIA H800 GPU의 중국 수출을 제한합니다. Xiaomi ThePaper OS 인터페이스 마스터가 스크래치 를 사용하여 RISC-V 시뮬레이터를 작동시켰고 성공적으로 실행되었습니다. Linux 커널 RustDesk 원격 데스크톱 1.2.3 출시, 향상된 Wayland 지원 Logitech USB 수신기를 분리한 후 Linux 커널이 충돌했습니다. DHH "패키징 도구에 대한 날카로운 검토 ": 프런트 엔드를 전혀 구축할 필요가 없습니다(빌드 없음) JetBrains는 기술 문서를 작성하기 위해 Writerside를 출시합니다. Node.js 21용 도구 공식 출시
{{o.이름}}
{{이름}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10112324
Recomendado
Clasificación