【併發編程】常見的線程不安全類和寫法

String 相關

StringBuilder

測試方法:在多線程環境下不斷往StringBuilder中寫入字符,檢測最後的StringBuilder長度是否與寫入次數相同。java

@Slf4j
public class StringExample1 {

    /** * 請求總數 */
    public static int clientTotal = 5000;
    /** * 同時併發執行線程數 */
    public static int threadTotal = 200;

    public static StringBuilder stringBuilder = new StringBuilder();

    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();
        log.info("size:{}", stringBuilder.length());
    }

    private static void update(){

        stringBuilder.append("1");
    }
}
複製代碼

運行結果安全

能夠看到StringBuilder的長度是小於5000的,這說明StringBuilder是一個線程不安全的類。多線程

StringBuffer

接下來咱們來測試一下StringBuffer的線程安全性併發

測試代碼只需把上面的StringBuilder改爲StringBuffer便可。app

運行結果框架

屢次運行測試代碼,結果始終是5000,這說明StringBuffer是一個線程安全的類。ide

產生差異的緣由

咱們點開StringBuffer的源碼看一下性能

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
複製代碼

能夠看到StringBuffer的方法基本上都加了synchronized關鍵字來保證線程安全。測試

在性能上StringBuilder要好於StringBuffer .ui

SimpleDateFormat -> JodaTime

SimpleDateFormat

錯誤寫法

測試方法:使用SimpleDateFormatparse 方法轉換日期格式,拋出相應的異常。

@Slf4j
public class DateFormatExample1 {

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

    private static void update(){

        try {
            simpleDateFormat.parse("20180208");
        } catch (Exception e) {
            log.error("parse exception", e);
        }

    }
複製代碼

運行結果

拋出大量異常。所以這種寫法是錯誤的,緣由在於SimpleDateFormat 不是線程安全的對象。

正確寫法

在前面的錯誤寫法中應用堆棧封閉的思想,將SimpleDateFormat 每次聲明一個新的變量來使用。

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

    private static void update(){

        try {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
            simpleDateFormat.parse("20180208");
        } catch (Exception e) {
            log.error("parse exception", e);
        }

    }
}
複製代碼

運行結果

再也不出現異常。

JodaTime

測試方法與以前相同

@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++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (Exception e){
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update(){

        DateTime.parse("20180208", dateTimeFormatter).toDate();

    }
}
複製代碼

運行結果沒有拋出異常。若是以爲這樣不夠嚴謹也能夠在update方法中把每次的日期打印出來,結果必定是5000條日期。

在實際項目中更推薦使用JodaTime中的DateTime,它與SimpleDateFormat的區別不單單在於線程安全方面,在實際處理方面也有更多的優點,這裏就不展開來說了。

集合類相關

ArrayList

仍是以前的測試框架

@Slf4j
public class ArrayListExample {

    /** * 請求總數 */
    public static int clientTotal = 5000;
    /** * 同時併發執行線程數 */
    public static int threadTotal = 200;

    private static List<Integer> list = new ArrayList<>();

    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();
        log.info("size:{}", list.size());
    }

    private static void update(int i){

        list.add(i);
    }
}
複製代碼

運行結果

結果小於5000,說明ArrayList 是線程不安全的。

HashSet

檢測邏輯與上面相同,結果小於5000,說明HashSet 也是線程不安全的。

HashMap

結果同上,線程不安全。

線程不安全的寫法

先檢查再執行: if(condition(a)) {handle(a);}

在實際開發中若是要這樣寫必定要確認這個a是不是多線程共享的,若是是共享的必定要在上面加個鎖或者保證這兩個操做是原子性的才能夠。

Written by Autu

2019.7.19

相關文章
相關標籤/搜索