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

951 阅读4分钟

String 相关

StringBuilder

测试方法:在多线程环境下不断往StringBuilder中写入字符,检测最后的StringBuilder长度是否与写入次数相同。

@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即可。

运行结果

多次运行测试代码,结果始终是5000,这说明StringBuffer是一个线程安全的类。

产生差别的原因

我们点开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 .

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