ホームページ  >  記事  >  Java  >  Java スレッド セーフティ戦略の分析例

Java スレッド セーフティ戦略の分析例

PHPz
PHPz転載
2023-04-20 19:13:061112ブラウズ

    1. 不変オブジェクト

    不変オブジェクトが満たす必要がある条件

    (1) オブジェクトの作成後、その状態を維持することはできません。修正される必要があります

    (2) オブジェクトのすべてのフィールドは最終型です

    (3) オブジェクトは正しく作成されます (オブジェクトの作成中に、この参照はオーバーフローしません)

    不変オブジェクトについては、JDK の String クラスを参照してください。

    最後のキーワード: クラス、メソッド、変数

    (1) 変更されたクラス: このクラスは継承できません、String クラス、ラッパー クラス基本型 (Integer、Long など) はすべて Final 型です。最終クラスのメンバー変数は必要に応じて最終型に設定できますが、最終クラスのすべてのメンバー メソッドは暗黙的に最終メソッドとして指定されます。

    (2) 変更方法: ロック方法は継承クラスによって変更されないため、効率的です。注: クラスのプライベート メソッドは、暗黙的に最終メソッドとして指定されます。

    (3) 変更される変数: 基本データ型変数 (初期化後の値は変更できません)、参照型変数 (初期化後の値は変更できません)

    は、JDK で Collections クラスを提供します。このクラスは、次のように unmodifiable で始まる多くのメソッドを提供します。

    Collections.unmodifiableXXX: Collection、List、Set , Map...

    Collections.unmodifiableXXX メソッドの XXX には、Collection、List、Set、Map を指定できます...

    このとき、Collection、List、Set、および自分で作成したマップ Collections.unmodifiableXXX メソッドでは、不変になります。このとき、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: スレッドが安全ではありません;

    StringBuffer: スレッドが安全ではありません;

    文字列の連結マルチスレッド操作の場合は、StringBuffer を使用して

    特定のメソッドで文字列スプライシング オブジェクトを定義します。これは StringBuilder を使用して実装できます。ローカル変数がメソッド内で定義されて使用されるとき、スタックは閉じられるため、その変数を使用するのは 1 つのスレッドだけです。複数のスレッドによる変数の操作は含まれません。StringBuilder を使用するだけです。

    2. SimpleDateFormat -> JodaTime

    SimpleDateFormat: スレッド セーフではないため、そのオブジェクトのインスタンス化を特定の時刻フォーマット メソッドに入れてスレッド セーフを実現できます
    JodaTime: スレッド セーフ

    #SimpleDateFormat のスレッド セーフでないコード例は次のとおりです。

    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 およびその他の Collections コレクション クラス これは、スレッド非安全なクラスです。

    4. まず確認してから実行します: if(condition(a)){handle(a);}

    注: この書き方はスレッドセーフではありません。 ! ! ! !

    この操作を 2 つのスレッドが同時に実行し、同時に if 条件を判定し、変数をスレッド間で共有し、両方のスレッドが if 条件を満たした場合に 2 つのスレッドが実行します。 (a) ステートメント、現時点では、handle(a) ステートメントはスレッドセーフではない可能性があります。

    安全でない点は、2 つの操作において、前の実行プロセスがスレッドセーフであっても、後続のプロセスもスレッドセーフですが、前後の実行プロセス間のギャップがアトミックではないことです。そのため、スレッドの安全性を脅かす問題も発生します。

    実際の処理では、if(condition(a)){handle(a);} クラスが発生した場合、 a がスレッドで共有されているかどうかを考慮し、スレッドで共有されている場合は実行する必要があります。メソッドにロックを追加するか、if(condition(a)){handle(a);} の前後の 2 つの操作 (if 判定とコード実行) がアトミックであることを確認します。

    4. スレッド セーフ - 同期コンテナ

    1. ArrayList -> Vector、Stack

    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();
            }
        }
    }

    スタック: Vector から継承され、先入れ後出し。

    2. HashMap -> HashTable(Key, Value not be null)

    HashMap: スレッドは安全ではありません;

    HashTable: スレッドは安全です、HashTable を使用する場合は注意してください。 Key も Value も null にすることはできません;

    3. Collections.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 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。