高并发下的线程安全实现——线程局部变量

来源:互联网 发布:北大方正字体淘宝收费 编辑:程序博客网 时间:2024/06/04 20:12

今天我们来讨论另外一种线程安全的实现方法。如果说互斥同步是多线程间的数据共享,那么线程局部变量就是线程间的数据隔离。ThreadLocal把共享数据的可见范围限制在同一个线程之内,这样无须同步也能实现线程之间数据不争用的问题。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。线程局部变量实现的原理也比较简单:每个线程的Thread对象中都有一个ThreadLocalMap对象,这个map存储了一组以该线程所对应的哈希码ThreadLocal.threadLocalHashCode为键,以线程局部变量为值的K-V值对,每一个线程对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以获取相对应的变量了,这个变量就是上文中提到的线程局部变量的副本。
我们来分析一下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来阻塞其他线程从而只允许一个线程访问或修改该对象。

原创粉丝点击