>  기사  >  Java  >  Java 스레드 안전 전략 분석 예시

Java 스레드 안전 전략 분석 예시

PHPz
PHPz앞으로
2023-04-20 19:13:061069검색

    1. 불변 객체

    불변 객체가 충족해야 하는 조건

    (1) 객체가 생성된 후에는 상태를 수정할 수 없습니다

    (2) 객체의 모든 필드가 최종 유형입니다

    ( 3) 객체가 올바르게 생성되었습니다(객체 생성 중에 이 참조가 오버플로되지 않았습니다)

    불변 객체의 경우 JDK에서 String 클래스를 볼 수 있습니다.

    최종 키워드: 클래스, 메소드, 변수

    (1) 수정된 클래스: 이 클래스는 상속될 수 없습니다. 기본 유형(예: Integer, Long 등)의 String 클래스 및 래퍼 클래스는 모두 최종 유형입니다. final 클래스의 멤버 변수는 필요에 따라 final 유형으로 설정할 수 있지만 final 클래스의 모든 멤버 메서드는 암시적으로 final 메서드로 지정됩니다.

    (2) 수정 방법: 잠금 방법은 상속된 클래스 효율성에 따라 수정되지 않습니다. 참고: 클래스의 프라이빗 메소드는 암묵적으로 최종 메소드로 지정됩니다

    (3) 수정된 변수: 기본 데이터 유형 변수(초기화 후에는 값을 수정할 수 없음), 참조 유형 변수(초기화 후에는 가리킬 수 없음) 다른 값으로) 객체)

    는 JDK에서 다음과 같이 수정 불가능으로 시작하는 다양한 메소드를 제공하는 Collections 클래스를 제공합니다.

    Collections.unmodifyingXXX: Collection, List, Set, Map...

    그중 컬렉션은 .unmodifyingXXX 메소드 XXX는 Collection, List, Set, Map이 될 수 있습니다. 이때 우리가 직접 생성한 Collection, List, Set, Map을 Collections.unmodifyingXXX 메소드에 전달하면 불변이 됩니다. 이때 Collection, List, Set, Map의 요소가 수정되면 java.lang.UnsupportedOperationException 예외가 발생한다.

    Google Guava에는 다음과 같이 Immutable로 시작하는 많은 클래스가 있습니다.

    ImmutableXXX, XXX는 Collection, List, Set, Map...이 될 수 있습니다...

    참고: Google Guava를 사용하려면 다음 종속성을 추가해야 합니다. Maven 패키지:

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>

    2. 스레드 폐쇄

    (1) 임시 스레드 폐쇄: 프로그램 제어 구현, 최악, 무시

    (2) 스택 폐쇄: 지역 변수, 동시성 문제 없음

    (3) ThreadLocal 스레드 클로저: 특히 좋은 닫기 방법

    3. 스레드에 안전하지 않은 클래스 및 작성 방법

    1. StringBuilder -> StringBuffer

    StringBuilder: thread-unsafe; 멀티 스레드 작업에서는 StringBuffer를 사용하여 구현합니다.

    특정 메서드에서 문자열 접합 개체를 정의하고 StringBuilder를 사용하여 구현할 수 있습니다. 지역 변수가 메서드 내에서 정의되고 사용되면 하나의 스레드만 변수를 사용하므로 여러 스레드의 변수 작업은 포함되지 않습니다.

    2.SimpleDateFormat -> JodaTime

    SimpleDateFormat: Thread-safe 안전한 코드 예시는 다음과 같습니다.

    package io.binghe.concurrency.example.commonunsafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    @Slf4j
    public class DateFormatExample {
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for(int i = 0; i < clientTotal; i++){
                executorService.execute(() -> {
                    try{
                        semaphore.acquire();
                        update();
                        semaphore.release();
                    }catch (Exception e){
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
        }
        public static void update(){
            try {
                simpleDateFormat.parse("20191024");
            } catch (ParseException e) {
                log.error("parse exception", e);
            }
        }
    }

    다음 코드로 수정하세요.

    package io.binghe.concurrency.example.commonunsafe;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    @Slf4j
    public class DateFormatExample2 {
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for(int i = 0; i < clientTotal; i++){
                executorService.execute(() -> {
                    try{
                        semaphore.acquire();
                        update();
                        semaphore.release();
                    }catch (Exception e){
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
        }
    
        public static void update(){
            try {
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
                simpleDateFormat.parse("20191024");
            } catch (ParseException e) {
                log.error("parse exception", e);
            }
        }
    }
    JodaTime의 경우 Maven에 다음 종속성 패키지를 추가해야 합니다.

    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.9</version>
    </dependency>

    샘플 코드는 다음과 같습니다.

    package io.binghe.concurrency.example.commonunsafe;
    import lombok.extern.slf4j.Slf4j;
    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    import org.joda.time.format.DateTimeFormatter;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class DateFormatExample3 {
        //请求总数
        public static int clientTotal = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
    
        private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for(int i = 0; i < clientTotal; i++){
                final int count = i;
                executorService.execute(() -> {
                    try{
                        semaphore.acquire();
                        update(count);
                        semaphore.release();
                    }catch (Exception e){
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
        }
    
        public static void update(int i){
            log.info("{} - {}", i, DateTime.parse("20191024", dateTimeFormatter));
        }
    }

    3 ArrayList, HashSet, HashMap 및 기타 컬렉션 컬렉션 클래스는 스레드에 안전하지 않습니다.

    4. 먼저 확인한 후 실행: if(condition(a)){handle(a);}

    참고: 이 작성 방법은 스레드에 안전하지 않습니다! ! ! ! !

    두 스레드가 동시에 이 작업을 수행하고 if 조건을 동시에 판단하며, 두 스레드가 모두 if 조건을 충족하면 두 스레드가 핸들(a) 문을 실행합니다. 동시에, handler(a) 문은 스레드로부터 안전하지 않을 수 있습니다.

    안전하지 않은 점은 두 작업에서 이전 실행 프로세스가 스레드로부터 안전하더라도 후속 프로세스도 스레드로부터 안전하지만 실행 프로세스 전후의 간격이 원자적이지 않다는 것입니다. 또한 스레드 불안전 질문이 발생합니다.

    실제 프로세스에서 if(condition(a)){handle(a);} 클래스를 만날 때 a가 스레드에서 공유되는지 여부를 고려하십시오. 스레드에서 공유하는 경우 전체 실행 방법을 잠가야 합니다. 또는 if(condition(a)){handle(a);} 전후의 두 작업(판단 및 코드 실행인 경우)이 원자성인지 확인하세요.

    4. 스레드 안전성 - 동기화된 컨테이너

    1. ArrayList -> 벡터, 스택

    ArrayList: 스레드 안전하지 않음

    Vector: 동기 작업이지만 스레드 안전하지 않은 상황 및 스레드 안전하지 않은 코드가 발생할 수 있음

    public class VectorExample {
    
        private static Vector<Integer> vector = new Vector<>();
    
        public static void main(String[] args) throws InterruptedException {
            while (true){
                for(int i = 0; i < 10; i++){
                    vector.add(i);
                }
                Thread thread1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for(int i = 0; i < vector.size(); i++){
                            vector.remove(i);
                        }
                    }
                });
                Thread thread2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for(int i = 0; i < vector.size(); i++){
                            vector.get(i);
                        }
                    }
                });
                thread1.start();
                thread2.start();
            }
        }
    }

    Stack: 벡터에서 상속되며 선입, 후출됩니다.

    2. HashMap -> HashTable(키와 값은 null일 수 없음)

    HashMap: 스레드 안전하지 않음

    HashTable: HashTable을 사용할 때 키와 값은 모두 null이 될 수 없습니다. .synchronizedXXX(List, Set, Map)

    참고: 컬렉션을 탐색할 때 컬렉션을 업데이트하지 마세요. 컬렉션에서 요소를 삭제해야 하는 경우 컬렉션을 순회하여 삭제해야 할 요소를 먼저 표시한 다음 컬렉션 순회가 완료된 후 삭제 작업을 수행하면 됩니다. 예를 들어 다음 샘플 코드는 다음과 같습니다.

    public class VectorExample3 {
    
        //此方法抛出:java.util.ConcurrentModificationException
        private static void test1(Vector<Integer> v1){
            for(Integer i : v1){
                if(i == 3){
                    v1.remove(i);
                }
            }
        }
        //此方法抛出:java.util.ConcurrentModificationException
        private static void test2(Vector<Integer> v1){
            Iterator<Integer> iterator = v1.iterator();
            while (iterator.hasNext()){
                Integer i = iterator.next();
                if(i == 3){
                    v1.remove(i);
                }
            }
        }
        //正常
        private static void test3(Vector<Integer> v1){
            for(int i = 0; i < v1.size(); i++){
                if(i == 3){
                    v1.remove(i);
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Vector<Integer> vector = new Vector<>();
            vector.add(1);
            vector.add(2);
            vector.add(3);
    
            //test1(vector);
            //test2(vector);
            test3(vector);
        }
    }

    五、线程安全-并发容器J.U.C

    J.U.C表示的是java.util.concurrent报名的缩写。

    1. ArrayList -> CopyOnWriteArrayList

    ArrayList:线程不安全;

    CopyOnWriteArrayList:线程安全;

    写操作时复制,当有新元素添加到CopyOnWriteArrayList数组时,先从原有的数组中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。

    CopyOnWriteArrayList缺点:

    (1)每次写操作都需要复制一份,消耗内存,如果元素特别多,可能导致GC;

    (2)不能用于实时读的场景,适合读多写少的场景;

    CopyOnWriteArrayList设计思想:

    (1)读写分离

    (2)最终一致性

    (3)使用时另外开辟空间,解决并发冲突

    注意:CopyOnWriteArrayList读操作时,都是在原数组上进行的,不需要加锁,写操作时复制,当有新元素添加到CopyOnWriteArrayList数组时,先从原有的集合中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。

    2.HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet

    CopyOnWriteArraySet:线程安全的,底层实现使用了CopyOnWriteArrayList。

    ConcurrentSkipListSet:JDK6新增的类,支持排序。可以在构造时,自定义比较器,基于Map集合。在多线程环境下,ConcurrentSkipListSet中的contains()方法、add()、remove()、retain()等操作,都是线程安全的。但是,批量操作,比如:containsAll()、addAll()、removeAll()、retainAll()等操作,并不保证整体一定是原子操作,只能保证批量操作中的每次操作是原子性的,因为批量操作中是以循环的形式调用的单步操作,比如removeAll()操作下以循环的方式调用remove()操作。如下代码所示:

    //ConcurrentSkipListSet类型中的removeAll()方法的源码
    public boolean removeAll(Collection<?> c) {
        // Override AbstractSet version to avoid unnecessary call to size()
        boolean modified = false;
        for (Object e : c)
            if (remove(e))
                modified = true;
        return modified;
    }

    所以,在执行ConcurrentSkipListSet中的批量操作时,需要考虑加锁问题。

    注意:ConcurrentSkipListSet类不允许使用空元素(null)。

    3. HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap

    ConcurrentHashMap:线程安全,不允许空值

    ConcurrentSkipListMap:是TreeMap的线程安全版本,内部是使用SkipList跳表结构实现

    4.ConcurrentSkipListMap与ConcurrentHashMap对比如下

    (1)ConcurrentSkipListMap中的Key是有序的,ConcurrentHashMap中的Key是无序的;

    (2)ConcurrentSkipListMap支持更高的并发,对数据的存取时间和线程数几乎无关,也就是说,在数据量一定的情况下,并发的线程数越多,ConcurrentSkipListMap越能体现出它的优势。

    注意:在非对线程下尽量使用TreeMap,另外,对于并发数相对较低的并行程序,可以使用Collections.synchronizedSortedMap,将TreeMap进行包装;对于高并发程序,使用ConcurrentSkipListMap提供更高的并发度;在多线程高并发环境中,需要对Map的键值对进行排序,尽量使用ConcurrentSkipListMap。

    위 내용은 Java 스레드 안전 전략 분석 예시의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제