Java 동시 프로그래밍(5) 스레드 동기화 [CAS|원자 클래스|동기화 컨테이너 클래스|동기화 도구 클래스]

카스

개요

CAS의 정식 명칭은 Compare-And-Swap입니다. 이는 CPU의 원자적 명령이며 공유 데이터에 대한 동시 작업을 위한 하드웨어 지원입니다. 그 기능은 CPU가 특정 순간에 두 값이 같은지 비교하는 것입니다.

핵심 원리: CAS는 동작 중 메인 메모리에 있는 값과 스레드의 작업 메모리에 있는 값이 같은지 먼저 비교하고, 같으면 메인 메모리에 있는 값을 새로운 값으로 업데이트한다. 같지 않으면 교환되지 않습니다(같지 않은 경우 ). 항상 스핀을 통해 값을 업데이트하려고 시도합니다)

CAS 명령어에는 다음과 같은 문제가 있습니다.

  • ABA 문제 : 두 순간의 값을 비교하면 ABA 문제가 발생하게 되는데 원래는 A였는데 중간에 B로 바뀌었다가 다시 A로 바뀌었습니다. CAS 검출에서는 값이 변하지 않은 것으로 생각하는데, 사실은 변했다.
    • 솔루션: JDK1.5의 AtomicStampedReference를 사용하여 ABA 문제를 해결할 수 있습니다. 기본 아이디어는 버전 번호를 높이고 수정된 현재 값과 만료된 값이 일치하는지 확인하는 것입니다. AtomicStampedReference는 현재 참조와 만료된 참조가 동일한지 여부에 따라 CAS 작업을 수행합니다.
  • 긴 사이클 시간과 높은 오버헤드 : CAS가 실패하면 항상 회전하여 시도합니다. CAS가 오랫동안 실패하면 CPU에 많은 오버헤드가 발생할 수 있습니다. 따라서 충돌이 너무 빈번한 시나리오를 CAS처리하기 위해 기본 형식을 사용하는 것은 권장되지 않습니다( CAS낙관적 잠금 메커니즘입니다. 낙관적 잠금은 충돌이 빈번한 시나리오에는 적합하지 않습니다. 이 경우 비관적 잠금 메커니즘을 선택할 수 있습니다).

CAS 알고리즘 예

package com.bierce;

public class TestCAS {
    public static void main(String[] args) {
        final CAS cas = new CAS();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                int expectedValue = cas.getValue();//每次更新前必须获取内存中最新的值,因为可能与compareAndSwap中第一次获取的Vaule不一样
                boolean updateRes = cas.compareAndSet(expectedValue, (int) Math.random() * 101);
                System.out.print(updateRes + " "); // true true true true true true true true true true
            }).start();
        }
    }
}
class CAS{
    private int value;
    //获取内存值
    public synchronized  int getValue(){
        return value;
    }
    //比较内存值
    public synchronized  int compareAndSwap(int expectedValue, int newValue){
        int oldValue = value;//
        if (oldValue == expectedValue){
            this.value = newValue;
        }
        return oldValue;
    }
    //设置内存值
    public synchronized  boolean compareAndSet(int expectedValue, int newValue){
        return expectedValue == compareAndSwap(expectedValue, newValue);
    }
}

원자 클래스

개요

멀티 스레드 환경에서는 잠금( 동기화/잠금 )을 통해 데이터 작업의 일관성을 보장할 수 있지만 효율성이 너무 번거롭습니다. 따라서 Java는 java.util.concurrent.atomic사용하기 편리하고 성능이 뛰어나며 효율적인 원자 연산을 위한 캡슐화된 클래스인 패키지를 제공합니다. Atomic 클래스 메소드에는 잠금 기능이 없으며 기본 핵심은 Unsafe 클래스의 휘발성 및 CAS 알고리즘 연산을 통해 스레드 가시성을 보장하여 데이터 연산의 원자성을 보장하는 것입니다.

분류

java.util.concurrent.atomic 패키지 아래의 원자 클래스

AtomicInteger 연습

package com.bierce;
import java.util.concurrent.atomic.AtomicInteger;
public class TestAtomicDemo {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();

        //模拟多线程,通过原子类保证变量的原子性
        for(int i =0; i < 10; i++){
            new Thread(atomicDemo).start();
        }
    }
}
class AtomicDemo implements Runnable{
    //AtomicInteger原子类提供很多API,根据需求使用
    private AtomicInteger sn = new AtomicInteger(); 
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //getAndIncrement() :类似于 i++
        //incrementAndGet() :类似于 ++i
        //System.out.print(sn.getAndIncrement() + " "); // 输出:1 9 8 7 0 5 2 6 4 3
        System.out.print(sn.incrementAndGet() + " "); // 输出:1 10 9 5 2 8 3 6 7 4
    }
}

동기 컨테이너 클래스

  • ConcurrentHashMap : 동기화된 HashMap의 동시 구현
  • ConcurrentSkipListMap: 동기화된 TreeMap의 동시 구현
  • CopyOnWriteArrayList: 동시 읽기에 적합하거나 컬렉션 데이터 순회 요구 사항이 목록 업데이트 요구 사항보다 훨씬 큰 경우에 적합합니다.
  • ThreadLocal: 공간을 시간으로 교환한다는 아이디어를 활용하고, 스레드마다 복사본을 복사하고, 스레드를 격리하여 스레드 안전성을 확보합니다.
  • 차단 대기열
  • ConcurrentLinkedQueue: CAS를 통해 스레드 안전성을 보장하고 지연 업데이트 전략을 채택하며 처리량을 향상시킵니다.

동시해시맵

ConcurrentHashMap은 내부적으로 HashTable의 배타적 잠금을 대체하기 위해 "잠금 분할" 개념을 채택하고 동시성을 제어하기 위한 동기화 작업을 추가하며 성능을 향상시키지만 JDK1.7&&JDK1.8 버전에서는 기본 구현이 다릅니다.

  • JDK1.7 버전: 하단 레이어는 ReentrantLock+Segment+HashEntry, JDK1.8 버전: 하단 레이어는 동기화+CAS+HashEntry+red-black 트리입니다.
  • JDK1.8에서는 잠금 세분성이 감소되었습니다. JDK1.7 버전의 잠금 세분성은 Segment를 기반으로 하며 여러 HashEntry를 포함하는 반면, JDK1.8의 잠금 세분성은 HashEntry(첫 번째 노드)입니다.
  • JDK1.8의 데이터 구조는 더 간단합니다. JDK1.8은 동기화를 위해 동기화를 사용하므로 Segment와 같은 데이터 구조가 필요하지 않습니다.
  • JDK1.8은 연결된 목록을 최적화하기 위해 레드-블랙 트리를 사용합니다. 긴 연결 목록을 기반으로 한 순회는 매우 긴 프로세스이지만 레드-블랙 트리의 순회 효율성은 매우 빠릅니다.
  • 확장: reentrantLock 대신 동기화를 사용하는 이유
    • 세분성이 줄어들기 때문에 동기화는 ReentrantLock보다 나쁘지 않으며, 성긴 잠금에서는 ReentrantLock이 Condition을 사용하여 각 낮은 세분성의 경계를 제어할 수 있고 더 유연하지만, 낮은 세분성에서는 Condition의 장점이 사라집니다.
    • JVM 개발팀은 한번도 동기화를 포기한 적이 없으며, JVM 기반의 동기화 최적화 공간이 더 넓기 때문에 API를 사용하는 것보다 임베디드 키워드를 사용하는 것이 더 자연스럽습니다.
    • 대량의 데이터 작업에서 API 기반 ReentrantLock은 JVM 메모리 부족으로 인해 더 많은 메모리를 소비합니다.

CopyOnWriteArrayList 연습

package com.bierce;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * CopyOnWriteArrayList : 支持并发读取时写入
 * 注意:添加数据操作多时,效率比较低,因为每次添加都会进行复制一个新的列表,开销大;
 *      而并发迭代操作多时效率高
 */
public class TestCopyOnWriteArrayList {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayListDemo copyOnWriteArrayListDemo = new CopyOnWriteArrayListDemo();

        for (int i = 0; i < 10; i++) {
            new Thread(copyOnWriteArrayListDemo).start();
        }
    }
}
class CopyOnWriteArrayListDemo implements  Runnable{
    // 不支持并发修改场景,Exception in thread "Thread-1" java.util.ConcurrentModificationException
    //private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
    public static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    static{
        list.add("贾宝玉");
        list.add("林黛玉");
        list.add("薛宝钗");
    }
    @Override
    public void run() {
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            list.add("test");
        }
    }
}

동기화 도구

  • 카운트다운래치
  • 순환 장벽
  • 리소스 액세스 제어세마포어

카운트다운래치(래치)

개요

  • CountDownLatch는 여러 스레드 간의 동기화를 조정하고 스레드 간의 통신을 구현하는 데 사용되는 동기화 도구 클래스입니다.
  • CountDownLatch는 한 스레드가 실행을 계속하기 전에 다른 스레드가 작업을 완료할 때까지 기다리는 시나리오에 적합합니다.

원칙

CountDownLatch의 하위 레이어는 카운터를 통해 구현되며, 카운터의 초기화 값은 스레드 수입니다. 스레드가 작업을 완료할 때마다 그에 따라 카운터 값이 1씩 감소합니다. 카운터가 0에 도달하면 모든 스레드가 작업을 완료했으며 잠금을 대기 중인 스레드가 계속 작업을 실행할 수 있음을 의미합니다.

관행

package com.bierce;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;

public class TestCountDownLatchDemo {
    public static void main(String[] args) {
        final CountDownLatch countDownLatch = new CountDownLatch(5);//声明5个线程
        CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo(countDownLatch);

        //long start = System.currentTimeMillis();
        Instant start = Instant.now();//JDK8新特性API
        for (int i = 0; i < 5; i++) { //此处循环次数必须和countDownLatch创建的线程个数一致
            new Thread(countDownLatchDemo).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
        }
        //long end = System.currentTimeMillis();
        Instant end = Instant.now();
        //System.out.println("执行完成耗费时长: " + (end - start));
        System.out.println("执行完成耗费时长: " + Duration.between(end, start).toMillis()); // 110ms
    }
}
class CountDownLatchDemo implements Runnable{
    private CountDownLatch countDownLatch;
    public CountDownLatchDemo(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        synchronized (this){ //加锁保证并发安全
            try {
                for (int i = 0; i < 10000; i++) {
                    if (i % 2 == 0) {
                        System.out.println(i);
                    }
                }
            }finally {
                countDownLatch.countDown(); //线程完成后执行减一
            }
        }
    }
}

추천

출처blog.csdn.net/qq_34020761/article/details/132245992