Java多线程之线程安全策略---线程不安全类与写法

一、如果一个类的对象同时可以被多个线程访问,如果不做特殊的同步和并发处理,就会容易表现出线程不安全的对象。

接下来,我们来看三组容易类和一种写法,它们经常在编程中遇到。

二、StringBuilderStringBuffer

Java里面字符串拼接提供了两个类:StringBuilderStringBuffer

(1)看它们在多线程的表现:

StringBuilder:

@Slf4j
public class StringBuilderExample {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) throws Exception {
        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");
    }
}

执行后发现,没有得到5000结果。

StringBuffer:

@Slf4j
public class StringBufferExample {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static StringBuffer stringBuffer = new StringBuffer();

    public static void main(String[] args) throws Exception {
        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:{}", stringBuffer.length());
    }

    private static void update() {
        stringBuffer.append("1");
    }
}

执行可以看到得到了5000。

所以:

StringBuilder是线程不安全的类。

StringBuffer是线程安全的类。

我们可以看一下线程安全的StringBuffer的append()实现,可以发现它是用synchronized修饰的:


(2)StringBuilderStringBuffer区别:

虽然StringBuffer是线程安全的类,但是实现的时候使用了synchronized关键字,导致这个方法同一个时间只有一个线程可以调用,影响了性能。

因此涉及到字符串拼接,并且有多线程的时候,可以用StringBuffer。但是如果在一个方法里面定义一个StringBuilder变量(堆栈封闭,线程安全)进行字符串拼接也是可以的,并且性能比StringBuffer好。

三、SimpleDateFormat JodaTime

SimpleDateFormat:

@Slf4j
public class SimpleDateFormatExample {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static void main(String[] args) throws Exception {
        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 不是一个线程安全的类,它声明的实例如果被多线程共享的时候,方法就容易出现异常。

这时我们可以让它做局部变量:


JodaTime:

@Slf4j
public class DateTimeFormatterExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

    public static void main(String[] args) throws Exception {
        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();
    }

    private static void update(int i) {
        log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
    }
}
 
 

DateTimeFormatter是JodaTime包下的一个类。

执行完之后可以看到正常输出。

JodaTime是线程安全的类。

四、ArrayList/HashSet/HashMapCollections

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 Exception {
        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);
    }
}

可以看到最后执行结果,list.size()并 不是5000。所以ArrayList是线程不安全的。

同样的,我们可以证明:

HashSet 是线程不安全的。

HashMap是线程不安全的。

五、先检查再执行操作:if(condition(a)){handle(a);}

这是线程不安全的。

假设a是线程安全的类,比如是atomic对应的对象,那么可能在判断的时候满足条件,这个时候可能两个线程都访问到了if判断,都通过了,这个时候分别去处理handle,就会触发线程不安全。

这里不安全的点在于,分成两个操作之后,即使之前的一个过程(condition)是线程安全的,但是conditionhandle间隙中不是原子性的。

要判断一个对象是否满足某个条件,满足某个条件再做某个处理的时候,要考虑这个对象是否是多线程共享的,如果是多线程共享的,一定要在这个方法上面加锁,或者保证conditionhandle是原子性的。



猜你喜欢

转载自blog.csdn.net/weixin_40459875/article/details/80294312