Java并发编程之常见线程不安全类与写法

线程不安全类:一个类的对象可以被多个线程访问修改,并且没有做任何的同步或者并发处理,就很容易出现线程安全问题。以下介绍几种常见的线程不安全类及其写法:

一、StringBuilder,StringBuffer

字符串拼接主要提供了两个类:

StringBuilder,StringBuffer

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

运行结果:

执行5000次的结果,总长度为4992。可以推断出StringBuilder为线程不安全。

同样使用StringBuffer做测试:

@Slf4j
public class StringExample2 {

    // 请求总数
    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次的结果,总长度为5000。可以推断出StringBuffer为线程安全。

查看StringBuffer源码实现:

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

      .... ...
    @Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }

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

StringBuffer的大部分方法都是加了synchronize关键字的,因此它是一个线程安全的类。但是线程安全的同时,它在一定程度上带来的效率上的损耗。

二、SimpleDateFormat和JodaTime

@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 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是一个非线程安全的类。我们可以用堆栈封闭的方式来实现线程安全(将SimpleDateFormat作为方法的局部变量):

@Slf4j
public class DateFormatExample2 {

    // 请求总数
    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 simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
            simpleDateFormat.parse("20180208");
        } catch (Exception e) {
            log.error("parse exception", e);
        }
    }
}

但是,使用堆栈封闭的方式,必定会不断的创建与销毁对象,带来一定的资源损耗。另外一种方式就是使用JodaTime这个类:

import org.joda.time.format.DateTimeFormatter;

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

为什么SimpeDateFormat线程不安全呢,分析一下源码:

SimpeDateFormat继承于DateFormat,而DateFormat又继承于Format这个类。

public abstract class DateFormat extends Format {
    
    /**
     * The {@link Calendar} instance used for calculating the date-time fields
     * and the instant of time. This field is used for both formatting and
     * parsing.
     *
     * <p>Subclasses should initialize this field to a {@link Calendar}
     * appropriate for the {@link Locale} associated with this
     * <code>DateFormat</code>.
     * @serial
     */
    protected Calendar calendar;


    .... ...

    public Date parse(String source) throws ParseException
    {
        ParsePosition pos = new ParsePosition(0);
        Date result = parse(source, pos);
        if (pos.index == 0)
            throw new ParseException("Unparseable date: \"" + source + "\"" ,
                pos.errorIndex);
        return result;
    }

}

DateFormat下有Calendar这个全局变量。再看下SimpeDateFormat 下parse的具体实现(非完整方法,这里只截取重要的部分),看以看到有个calb.establish(calendar).getTime的方法。

public class SimpleDateFormat extends DateFormat {

 @Override
    public Date parse(String text, ParsePosition pos)
    {
       ... ...
        CalendarBuilder calb = new CalendarBuilder();

        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }

}

继续看下calb.establish(calendar)的具体实现:

class CalendarBuilder {


   Calendar establish(Calendar cal) {
        ...  ....

        cal.clear();
   
         ... ...

        cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);

        return cal;
    }
}

可以看到establish()方法是先执行calendar.clear()再执行calendar.setWeekDate()。这里就有个问题了。 如果线程A调用了 simpeDateFormat.parse(), 并且进行了calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了simpeDateFormat.parse(), 这时候线程B也执行了simpeDateFormat.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用simpeDateFormat.parse()并顺利结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date。从而导致了线程不安全问题。

除了以上两个类,还有ArrayList、HashSet、HashMap等Collections类也是线程不安全的。这些集合类的线程不安全主要是因为底层数组扩容时造成的数据丢失。

 

线程不安全的写法

典型的不安全写法是先检查,再执行if( condition(a) ){ handle(a) } 。假设一个全局变量即使是线程安全的,但是因为先检查后执行这个操作是非原子性操作,所以这种写法很容易造成线程不安全,在平时写代码过程中需要多加注意,可以用加锁的方式来规避掉该问题。

猜你喜欢

转载自blog.csdn.net/qq_34871626/article/details/81636745