脑图
概述
前两篇博客,我们说了 通过 不可变变量 和 线程封闭 这两种方式来实现线程安全。
这里我们来介绍下很常见的线程不安全的类
所谓线程不安全的类,是指一个类的实例对象可以同时被多个线程访问,如果不做同步或线程安全的处理,就会表现出线程不安全的行为,比如逻辑处理错误、抛出异常等。
字符串拼接之StringBuilder、StringBuffer
-
StringBuilder
一个可变的字符序列。它继承于AbstractStringBuilder
,实现了CharSequence
接口。 -
StringBuffer
也是继承于AbstractStringBuilder
的子类; -
StringBuilder
和StringBuffer
不同,前者是非线程安全的,后者是线程安全的。
StringBuilder (线程不安全)
运行结果
结果不等于5000,在多线程情况下,StringBuild是线程不安全的.
StringBuffer (线程安全)
运行结果
结果等于5000,在多线程情况下,StringBuffer是线程安全的.
小结
既然StringBuffer是线程安全的, 那为何JDK还要提供StringBuilder呢?
StringBuffer之所以是线程安全的原因是几乎所有的方法都加了synchronized关键字,所以是线程安全的。 synchronized 同一时间只能有一个线程访问,所以性能会相对较差。
在上篇博文 并发编程-线程安全策略之线程封闭中,我们了解到 线程封闭可以确保线程安全,其中线程封闭的一种实现方式时堆栈封闭,说白了就是局部变量。
所以推荐在堆栈封闭等线程安全的环境下(方法中的局部变量)应该首先选用StringBuilder。
时间相关的类 SimpleDateFormat、第三方库joda-time、JDK8提供的类
SimpleDateFormat 的实例对象在多线程共享使用的时候会抛出转换异常,正确的使用方法应该是采用堆栈封闭,将其作为方法内的局部变量而不是全局变量,在每次调用方法的时候才去创建一个SimpleDateFormat实例对象,这样利用堆栈封闭就不会出现并发问题。
另一种方式是使用第三方库joda-time的DateTimeFormatter类 或者JDK8新提供的类 : 不可变类且线程安全 LocalDate 、java.time.LocalTime 和LocaldateTime 新的Date和Time类DateTimeFormatter
SimpleDateFormat (线程不安全的写法)
执行结果:表现出了异常
SimpleDateFormat (线程安全的写法-堆栈封闭)
没有抛出异常
线程安全,无异常
joda-time (线程安全)
线程安全,无异常
JDK8的时间处理类(线程安全)
线程安全,无异常
Collections 中线程不安全的类
通常情况下,我们都是在方法内部作为局部变量使用这些集合类,很少会触发线程不安全的问题。
如果在某些情况下定义成static,而且多个线程可以修改的时候就容易出现多线程不安全的问题。
ArrayList (线程不安全)
计数错误,线程不安全
HashSet (线程不安全)
计数错误,线程不安全
HashMap (线程不安全)
计数错误,线程不安全
注意事项 (先检查后执行-- 线程不安全)
有一种写法需要注意,即便是线程安全的对象,在这种写法下也可能会出现线程不安全的行为,这种写法就是先检查后执行
if(condition(a)){
handle(a);
}
在这个操作里,可能会有两个线程同时通过if的判断,然后去执行了处理方法,那么就会出现两个线程同时操作一个对象,从而出现线程不安全的行为。这种写法导致线程不安全的主要原因是因为这里分成了两步操作,这个过程是非原子性的,所以就会出现线程不安全的问题。