首頁 > Java > java教程 > 主體

Java線程安全性策略的實例分析

PHPz
發布: 2023-04-20 19:13:06
轉載
1092 人瀏覽過

    一、不可變物件

    不可變物件需要滿足的條件

    (1)物件建立以後其狀態就不能修改

    (2)物件所有網域都是final類型

    (3)物件是正確建立的(在物件建立期間,this引用沒有溢出)

    對於不可變對象,可以參考JDK中的String類別

    final關鍵字:類別、方法、變數

    (1)修飾類別:該類別不能被繼承,String類,基礎類型的包裝類(如Integer、Long等)都是final類型。 final類別中的成員變數可以根據需要設定為final類型,但是final類別中的所有成員方法,都會被隱式的指定為final方法。

    (2)修飾方法:鎖定方法不被繼承類別修改;效率。注意:一個類別的private方法會被隱式的指定為final方法

    (3)修飾變數:基本資料型別變數(數值被初始化後不能再修改)、引用型別變數(初始化之後則不能再指向其他的物件)

    在JDK中提供了一個Collections類,這個類別中提供了很多以unmodifiable開頭的方法,如下:

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

    其中Collections.unmodifiableXXX方法中的XXX可以是Collection、List、Set、Map…

    此時,將我們自己建立的Collection、List、Set、Map,傳遞到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>
    登入後複製

    二、線程封閉

    (1)Ad-hoc線程封閉:程式控制實現,最糟糕,忽略

    (2)堆疊封閉:局部變量,無並發問題

    (3)ThreadLocal線程封閉:特別好的封閉方法

    三、線程不安全類別與寫法

    1. StringBuilder -> StringBuffer

    StringBuilder:執行緒不安全;

    StringBuffer:執行緒不安全;

    ##字串拼接當涉及多執行緒操作時,使用StringBuffer實作

    在一個具體的方法中,定義一個字串拼接對象,此時可以使用StringBuilder實作。因為在一個方法內部定義局部變數進行使用時,屬於堆疊封閉,只有一個執行緒會使用變量,不涉及多執行緒對變數的操作,使用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);}

    ##注意:這種寫法是線程不安全的! ! ! ! !

    兩個執行緒同時執行這種操作,同時對if條件進行判斷,且a變數是執行緒共享的,如果兩個執行緒都滿足if條件,則兩個執行緒會同時執行handle (a)語句,此時,handle(a)語句就可能不是線程安全的。

    不安全的點在於兩個操作中,即使前面的執行過程是線程安全的,後面的過程也是線程安全的,但是前後執行過程的間隙不是原子性的,因此,也會引發線程不安全的問題。

    實際過程中,遇到if(condition(a)){handle(a);}類別的處理時,考慮a是否是執行緒共享的,如果是執行緒共享的,則需要在整個執行方法上加鎖,或保證if(condition(a)){handle(a);}的前後兩個運算(if判斷和程式碼執行)是原子性的。

    四、線程安全-同步容器

    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();
            }
        }
    }
    登入後複製

    Stack:繼承自Vector,先進後出。

    2. HashMap -> HashTable(Key, Value都不能為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中文網其他相關文章!

    相關標籤:
    來源:yisu.com
    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    熱門教學
    更多>
    最新下載
    更多>
    網站特效
    網站源碼
    網站素材
    前端模板
    關於我們 免責聲明 Sitemap
    PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!