【并发编程】常见的线程不安全类和写法

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

相关文章
相关标签/搜索