高并发下的线程安全实现——线程局部变量
2017-07-25 10:54
232 查看
今天我们来讨论另外一种线程安全的实现方法。如果说互斥同步是多线程间的数据共享,那么线程局部变量就是线程间的数据隔离。ThreadLocal把共享数据的可见范围限制在同一个线程之内,这样无须同步也能实现线程之间数据不争用的问题。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。线程局部变量实现的原理也比较简单:每个线程的Thread对象中都有一个ThreadLocalMap对象,这个map存储了一组以该线程所对应的哈希码ThreadLocal.threadLocalHashCode为键,以线程局部变量为值的K-V值对,每一个线程对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以获取相对应的变量了,这个变量就是上文中提到的线程局部变量的副本。
我们来分析一下java中最常用到的日期时间格式化工具类SimpleDateFormat。SimpleDateFormat类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类都不是线程安全的,以下代码:
没有使用任何同步手段来确保线程安全,我们使用一个测试用例来测试一下是否运行正常:
运行结果为:
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-7:Sat Jun 24 06:02:02 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:02 CST 2017
pool-1-thread-6:Tue Jul 31 06:02:20 CST 2018
pool-1-thread-8:Tue Jul 24 06:02:20 CST 2018
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
很明显,线程6和线程8输出的时间是有错误的,这是因为SimpleDateFormat和DateFormat类不是线程安全的。在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题。以下代码:
我们使用一个测试用例来测一下看是否运行正常:
运行结果:
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-6:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-8:Sat Jun 24 06:02:20 CST 2017
通过结果可以看出以上代码肯定是线程安全的,可以保证数据的正确性。但是在高并发环境下,当一个线程调用该方法时,其他想要调用此方法的线程就要阻塞,多线程并发量大的时候会对性能有一定的影响。如果系统对性能有比较高的要求,那么推荐使用ThreadLocal来隔离数据在一个线程中:
调用上面的测试用例测试的结果如下:
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-8:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-6:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-7:Sat Jun 24 06:02:20 CST 2017
使用ThreadLocal,也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。
虽然ThreadLocal相比于互斥同步在时间性能上面有一定的优势,但是需要注意它们两者所应用的场景,ThreadLocal用于数据隔离,即当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。而synchronized用于数据同步,即当多个线程需要访问或修改某个对象的时候使用synchronized来阻塞其他线程从而只允许一个线程访问或修改该对象。
我们来分析一下java中最常用到的日期时间格式化工具类SimpleDateFormat。SimpleDateFormat类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类都不是线程安全的,以下代码:
public class DateUtil { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date) throws ParseException { return sdf.format(date); } public static Date parse(String strDate) throws ParseException { return sdf.parse(strDate); } }
没有使用任何同步手段来确保线程安全,我们使用一个测试用例来测试一下是否运行正常:
@Test public void test01(){ ExecutorService service = Executors.newCachedThreadPool(); // 创建一个线程池 for (int i = 0; i < 10; i++) { Runnable runnable = new Runnable() { public void run() { try { System.out.println(Thread.currentThread().getName()+":"+DateUtil.parse("2017-06-24 06:02:20")); Thread.sleep(30000); } catch (Exception e) { System.out.println(e.getMessage()); } } }; service.execute(runnable);// 为线程池添加任务 } }
运行结果为:
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-7:Sat Jun 24 06:02:02 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:02 CST 2017
pool-1-thread-6:Tue Jul 31 06:02:20 CST 2018
pool-1-thread-8:Tue Jul 24 06:02:20 CST 2018
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
很明显,线程6和线程8输出的时间是有错误的,这是因为SimpleDateFormat和DateFormat类不是线程安全的。在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题。以下代码:
public class DateSyncUtil{ private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ synchronized(sdf){ return sdf.format(date); } } public static Date parse(String strDate) throws ParseException{ synchronized(sdf){ return sdf.parse(strDate); } } }
我们使用一个测试用例来测一下看是否运行正常:
public class SyncTest { @Test public void test01(){ ExecutorService service = Executors.newCachedThreadPool(); // 创建一个线程池 final CountDownLatch cdOrder = new CountDownLatch(1); for (int i = 0; i < 10; i++) { Runnable runnable = new Runnable() { public void run() { try { System.out.println(Thread.currentThread().getName()+":"+DateSyncUtil.parse("2017-06-24 06:02:20")); cdOrder.await(); } catch (Exception e) { e.printStackTrace(); } } }; service.execute(runnable);// 为线程池添加任务 } } }
运行结果:
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-6:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-8:Sat Jun 24 06:02:20 CST 2017
通过结果可以看出以上代码肯定是线程安全的,可以保证数据的正确性。但是在高并发环境下,当一个线程调用该方法时,其他想要调用此方法的线程就要阻塞,多线程并发量大的时候会对性能有一定的影响。如果系统对性能有比较高的要求,那么推荐使用ThreadLocal来隔离数据在一个线程中:
public class ConcurrentDateUtil { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static Date parse(String dateStr) throws ParseException { return threadLocal.get().parse(dateStr); } public static String format(Date date) { return threadLocal.get().format(date); } }
调用上面的测试用例测试的结果如下:
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-8:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-6:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-7:Sat Jun 24 06:02:20 CST 2017
使用ThreadLocal,也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。
虽然ThreadLocal相比于互斥同步在时间性能上面有一定的优势,但是需要注意它们两者所应用的场景,ThreadLocal用于数据隔离,即当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。而synchronized用于数据同步,即当多个线程需要访问或修改某个对象的时候使用synchronized来阻塞其他线程从而只允许一个线程访问或修改该对象。
相关文章推荐
- boost中asio网络库多线程并发处理实现,以及asio在多线程模型中线程的调度情况和线程安全。
- boost中asio网络库多线程并发处理实现,以及asio在多线程模型中线程的调度情况和线程安全。
- 高并发下的线程安全实现——互斥同步
- 高并发下最全线程安全的单例模式几种实现
- C#并发处理-锁OR线程安全?
- iOS并发编程指南——超级详细的指南,放弃线程,高效并发,实现完美体验吧!
- [高并发]Java高并发编程系列开山篇--线程实现
- 自己实现C语言atoi函数和线程安全版的itoa函数
- ConcurrentHashMap用分离锁实现多个线程间的并发写操作
- 黑马程序员——java第十一、十二天:多线程(创建线程1-2、多线程同步代码、实现Runnable接口、安全死锁)
- 安全稳定的实现进线程监控
- Java并发:线程安全的单例模式
- C++并发实战12:线程安全的queue
- C++实现一个线程安全且高效单例类。(懒汉与饿汉)
- JAVA 并发编程随笔【七】线程安全与共享资源
- java特种兵读书笔记(5-2)——并发之线程安全
- 同步异步-线程并发安全问题
- java并发编程:线程安全管理类--原子操作类--AtomicReferenceFieldUpdater<T,V>
- [高并发]Java高并发编程系列开山篇--线程实现
- 【Java基础总结】-了解Java线程调度、并发安全及锁优化