现在在CPU上,摩尔定律已经失效,大家都不再追求高频率,而是越来越追求多核,阿姆达尔定律似乎更重要一些了。
并发有很多优点,包括充分利用CPU的多核提高性能,更简单的模型(跟异步的callback hell相比)以及响应更灵敏得GUI等。
但是并发也引出很多问题,最重要的是安全性,很多在单线程环境下理所当然正确的程序在并发下都变得不正确,为了写出并发安全的程序,需要付出更多的努力。本文主要介绍一下并发问题的根本原因,以及针对策略。
来看下面简单的代码,我们知道SimpleDateFormat比较费时,所以很多程序里面会定义一个static的SimpleDateFormat,但是奇怪的事情发生了。
package concurrentStudy; import java.text.ParseException; import java.text.SimpleDateFormat; /** * Created by magicalli on 2014/12/13. */ public class SimpleDateFormatTest01 { private static final SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { System.out.println(dateformat.parse("2014-12-19 11:21:21")); } catch (ParseException e) { e.printStackTrace(); } } }).start(); } } }模拟多个线程下会用到dateformat这个变量,运行之后发现很多错误,有抛出异常的,有结果错误的,如下图
究其原因,SimpleDateFormat不是线程安全的。只是SimpleDateFormat的doc(据说在JDK6之后才加进去的?)
* Date formats are not synchronized. * It is recommended to create separate format instances for each thread. * If multiple threads access a format concurrently, it must be synchronized * externally.并发安全,主要由于:
要想保证并发安全,上述3个条件,只能满足2个,于是我们有了3种解决方法(C(3, 1) == 3)。
第一种,不要多个线程访问,即加同步或者显示锁:
package concurrentStudy; import java.text.ParseException; import java.text.SimpleDateFormat; /** * Created by magicalli on 2014/12/13. */ public class SimpleDateFormatTest01 { private static final SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final Object lock = new Object(); public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { synchronized (lock) { System.out.println(dateformat.parse("2014-12-19 11:21:21")); } } catch (ParseException e) { e.printStackTrace(); } } }).start(); } } }第二种方法,不要共享变量,即“线程封闭(Thread Confinement),又大概有两种,一是栈封闭,即使用局部变量,方法里面自己new一个SimpleDateFormat出来
package concurrentStudy; import java.text.ParseException; import java.text.SimpleDateFormat; /** * Created by magicalli on 2014/12/13. */ public class SimpleDateFormatTest03 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { final SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(dateformat.parse("2014-12-19 11:21:21")); } catch (ParseException e) { e.printStackTrace(); } } }).start(); } } }但是这个有一个坏处,我们一开始说用static的目的是因为new一个SimpleDateFormat比较费时,如果在每次调用的时候new一个出来,感觉有点浪费,于是有了ThreadLocal这个神器。ThreadLocal可以简单理解为一个Map<Thread, T>,即以线程为key的map,这样可以保证每个线程有自己的SimpleDateFormat,避免了各个线程共享。
package concurrentStudy; import java.text.ParseException; import java.text.SimpleDateFormat; /** * Created by magicalli on 2014/12/13. */ public class SimpleDateFormatTest04 { private static final ThreadLocal<SimpleDateFormat> dateformat = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { System.out.println("init......."); return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10; j++) { try { System.out.println(dateformat.get().parse("2014-12-19 11:21:21")); } catch (ParseException e) { e.printStackTrace(); } } } }).start(); } } }
运行结果可以看到,即使调用了100次,也只针对每个线程new了对象,总共只有10个SimpleDateFormat对象,大大减小了开销。
其实更好的解决方法就是,不用JDK自带的Date等一切日期类!!!用Joda-time,api更友好,更简单易用,功能更强大!
package concurrentStudy; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; /** * Created by magicalli on 2014/12/13. */ public class SimpleDateFormatTest05 { private static final DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(dateTimeFormatter.parseDateTime("2014-12-19 11:21:21")); } }).start(); } } }