什么是线程不安全类?
如果一个类的对象同时可以被多个线程访问,如果不做特殊的同步与并发处理,就很容易表现出线程不安全的现象,比如抛出异常,比如逻辑处理错误等,这种类就是线程不安全类。
StringBuilder->StringBuffer
@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 (InterruptedException e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", stringBuilder.length());
}
public static void update() {
stringBuilder.append("1");
}
}
输出结果与我们预期的不一致。StringBuilder是一个线程不安全的类。
我们将StringBuilder换成StringBuffer,可以得到预期的效果。说明StringBuffer是线程安全的。
查看StringBuffer的append方法,发现这个方法与其他方法前添加了synchronized关键字。
StringBuffer因为使用了synchronized关键字,因此在使用的时候会有性能损耗的,因此在做字符串拼接时涉及到多线程可以考虑StringBuffer来处理。
但是很多时候,我们往往在一个方法里面做字符串拼接单独,定义一个StringBuilder变量就可以了。因为在一个方法内部定义局部变量时属于堆栈封闭,这时只有单个线程可以操作对象,不涉及到线程安全问题了。
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 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 (InterruptedException e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
}
public static void update() {
try {
simpleDateFormat.parse("20190729");
} catch (ParseException e) {
e.printStackTrace();
log.error("parse Exception" + e);
}
}
}
运行时,会抛出异常:
Exception in thread "pool-1-thread-3" java.lang.NumberFormatException: For input string: "E.177"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.vincent.example.commonUnsafe.DateFormatExample1.update(DateFormatExample1.java:50)
at com.vincent.example.commonUnsafe.DateFormatExample1.lambda$main$0(DateFormatExample1.java:34)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
这是线程不安全的,simpleDateFormat不是一个线程安全的类,一种解决办法是将SimpleDateFormat simpleDateFormat = new SimpleDateFormat()放到方法内,封闭堆栈,修改update方法如下:
public static void update() {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
simpleDateFormat.parse("20190729");
} catch (ParseException e) {
e.printStackTrace();
log.error("parse Exception" + e);
}
}
JodaTime是线程安全的:
@Slf4j
@ThreadSafe
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++) {
final int count = i;
executorService.execute(() ->{
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (InterruptedException e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
}
public static void update(int i) {
log.info("{}, {}", i, DateTime.parse("20190729",dateTimeFormatter).toDate());
}
}
ArrayList,HashSet,HashMap等Collections
通常我们使用这些集合类时他们的对象通常声明在方法里面作为局部变量来使用,很少触发线程不安全的问题,但是一旦定义成static的时候而且多个线程可以进行修改的时候就会容器出问题。例如下面的代码:
@Slf4j
public class ArrayListExample {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
private static List 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 (InterruptedException e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}",list.size()) ;
}
public static void update(int i) {
list.add(i);
}
}
输出结果不是我们所预期的。
同样适用HashSet,HashMap也无法输出正确的结果。这些都是线程不安全的。
后面会介绍这些集合对应的线程安全类。
先检查在执行 if(condition(a)) {handle(a);}
为什么这种写法是线程不安全的?假设a是线程安全的类,即使if(condition(a))是线程安全的操作,handle(a)也是线程安全的,但是两个结合起来就不是线程安全的了,并不是原子性的。
Atomic类在自增的时候,底层实现是通过CAS原理来保证原子性的跟新。
实际过程中,如果遇到这种情况要判断一个对象是否满足某个条件,然后做某个操作,一定先要考虑这个对象是否多线程共享的,如果是多线程共享的一定要在上面加锁,或者保证操作是原子性的才可以。否则会触发线程不安全的。