线程安全问题
java.text中的三大格式化类:
1、NumberFormat
2、MessageFormat
3、DateFormat(SimpleDateFormat)
除了NumberFormat外,其他两个都不是线程安全的。
NumberFormat中使用的属性都是不变的,而SimpleDateFormat等却使用了可变但没有同步的属性,所以在多线程访问的条件下会产生线程安全问题,即格式不正确的问题。
假设我们要提供一个供并发方法的格式化工具方法,需要提供线程安全和高性能,如何做呢?
不恰当的使用
1、Stack局部变量方式,如
public static String format(Date date) {
SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formater.format(date);
}
这样在高并发方法下是线程安全的,因为format是stack上分配的变量,不会和其他线程共享,然后不足就是每一次执行格式化,都需要new一个SimpleDateFormat,这样在高并发上,内存消耗不容忽视
2、类上静态常量
private static final SimpleDateFormat formater = new SimpleDateFormat();
......
public static String format(Date date) {
return formater.format(date);
}
在单线程测试下,格式化1000万记录,第一种方式花费17秒多,而第二种方式仅为7秒多。但在多线程的条件下,这样一来就线程不安全了,怎么办呢?首先想到的时候synchronized,如下所示:
public static synchronized String format(Date date) {
//SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formater.format(date);
}
但这样处理的话,是线程安全了,但使所有并发访问变成了串行化访问,而一个简单的格式化类,用脚趾想也应该是无状态的,支持高性能访问。
正确、高效使用
综合考虑上面的两种情况,比较完善的实现应该具备以下特征:
- 不能每一次格式化都new SimpleDateFormat,但也不能所有线程共用一个SimpleDateFormat实例
- 格式化方法不能增加synchronized限制,增加了synchronized必然降低吞吐量
因为 SimpleDateFormat是mutable(可变)的并且共享在多个线程,为了安全访问,必然要用同步(synchronization),这样看来上面两个特征是互相矛盾,不竟然。SimpleDateFormat是mutable,这个除了改写SimpleDateFormat外,如果还想用SimpleDateFormat的话是没法改变的,那现在既然因为SimpleDateFormat共享引起的,那我不共享可不可以?可以。
下面是并发技术领域技术泰斗、JCP专家、JUC(java.util.concurrent)实现者Doug Lea经典书籍《Java concurrency in practice》中的一段话(3.3 Thread Confinement):
Accessing shared, mutable data requires using synchronization; one way to avoid this requirement is to not share. If data is only accessed from a single thread, no synchronization is needed. This technique, thread confinement, is one of the simplest ways to achieve thread safety. When an object is confined to a thread, such usage is automatically thread-safe even if the confined object itself is not |
通过上面的描述,我们得知,虽然SimpleDateFormat不是线程安全的,但通过把对SimpleDateFormat的访问局限在单个线程,对SimpleDateFormat的使用自然也就线程安全,这种技术叫Thead Configment,Java提供的实现是ThreadLocal。
改进
改进的方法就是一个线程一个SimpleDateFormat实例,这样每一个线程访问format都是串行化,对SimpleDateFormat的访问自然就线程安全了。同时因应用的所能支持的线程数是有限的,如一般都低于1万(按照实践经验准则支持1万线程高效运行,而不是context switch的话,CPU数目少说也的50核吧),这样SimpleDateFormat的实例也不会太多,对内存的消耗也可以忽略。
最后实现方式如下:
private static ThreadLocal formaterHolder
= new ThreadLocal() {
public SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String format(Date date) {
return formaterHolder.get().format(date);
}
formaterHolder是formater的各个线程的持有者,不同的线程调用formaterHolder.get()获的自己线程的formater。各个线程的formater通过initialValue在线程第一次使用时初始化
总结
正确写出一个程序比较难,而正确写出一个并发程序更难,而且还不是同一数量级的难度。写出一个好的高并发程序,掌握一门语言仅是一个必要条件,而理解和领会并发编程艺术才是充分条件。