谈谈ThreadLocal和解决线程安全的关系

来源:互联网 发布:pptv网络电视怎么用 编辑:程序博客网 时间:2024/05/21 23:41

谈谈ThreadLocal和解决线程安全的关系

    博客分类: 
  • Java
ThreadLocal线程安全
在这篇文章中我粗略的就我的理解谈了一下ThreadLocal。但是很多时候我们还是会认为ThreadLocal是为了解决线程安全的问题而设计的。这篇文章就我的理解再加上该文章 
中很多朋友的回复阐述一下ThreadLocal和线程安全的关系。 

首先我们来看一下线程安全问题产生的两个前提条件: 
1. 数据共享。多个线程访问同样的数据。 
2. 共享数据是可变的。多个线程对访问的共享数据作出了修改。 

定义一个共享数据: 
Java代码  收藏代码
  1. public static int a = 0;  


多线程多该共享数据进行修改: 
Java代码  收藏代码
  1. private static void plus() {  
  2.     for (int i = 0; i < 10; i++) {  
  3.         new Thread() {  
  4.             public void run() {  
  5.                 a++;  
  6.                 try {  
  7.                     Thread.sleep(1);  
  8.                 } catch (InterruptedException e) {  
  9.                     e.printStackTrace();  
  10.                 }  
  11.                 System.out.println("plus:" + Thread.currentThread().getName() + ": " + a);  
  12.             }  
  13.         }.start();  
  14.     }  
  15. }  

输出结果: 
Java代码  收藏代码
  1. plus:Thread-55  
  2. plus:Thread-25  
  3. plus:Thread-65  
  4. plus:Thread-95  
  5. plus:Thread-16  
  6. plus:Thread-08  
  7. plus:Thread-48  
  8. plus:Thread-310  
  9. plus:Thread-710  
  10. plus:Thread-810  

很明显,在第一次输出a的值的时候,a的值就已经被其他线程修改到5了,显然线程不安全。 

利用synchronized关键字将修改a值的地方和输出的地方上锁。让这段代码在某一个时间段内始终只有一个 
线程在执行: 
Java代码  收藏代码
  1. private static void plus() {  
  2.     for (int i = 0; i < 10; i++) {  
  3.         new Thread() {  
  4.             public void run() {  
  5.                 synchronized (JavaVariable.class) {  
  6.                     a++;  
  7.                     try {  
  8.                         Thread.sleep(1);  
  9.                     } catch (InterruptedException e) {  
  10.                         e.printStackTrace();  
  11.                     }  
  12.                     System.out.println("plus:" + Thread.currentThread().getName() + ": " + a);  
  13.                 }  
  14.             }  
  15.         }.start();  
  16.     }  
  17. }  

输出结果: 
Java代码  收藏代码
  1. plus:Thread-21  
  2. plus:Thread-62  
  3. plus:Thread-73  
  4. plus:Thread-34  
  5. plus:Thread-85  
  6. plus:Thread-46  
  7. plus:Thread-07  
  8. plus:Thread-98  
  9. plus:Thread-59  
  10. plus:Thread-110  

结果正确。 

那么,如果ThreadLocal是为了解决线程安全设计的,请同样用ThreadLocal解决上面的问题。即,我需要多个线程共同修改一个值,但是要保持这个值递增,就是说后面的线程不能在前一个线程还没有输出就又去修改。 

当然可以尝试一下,首先定义一个ThreadLocal。 
Java代码  收藏代码
  1. private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {  
  2.     protected Integer initialValue() {  
  3.         return 0;  
  4.     }  
  5. };  

插一句题外话:为什么threadLocal要被声明成静态的?不声明成静态的行不行? 
我只是有一点儿想法,还不是很明白,所以暂且先不讨论这个话题,大家也可以思考一下,有什么高见感谢回复到文章后面。 
接着上面的话题,也许可以这样: 
Java代码  收藏代码
  1. private static void plus() throws Exception {  
  2.     for (int i = 0; i < 10; i++) {  
  3.         new Thread() {  
  4.             public void run() {  
  5.                 //1  
  6.                 a = threadLocal.get();  
  7.                 a++;  
  8.                   
  9.                 //2  
  10.                 threadLocal.set(a);  
  11.                 System.out.println("plus:" + Thread.currentThread().getName() + ": " + threadLocal.get());  
  12.             }  
  13.         }.start();  
  14.     }  
  15. }  

我都不想运行结果了,全部会输出1。很明显,在代码“1”的时候,每一个线程都会将threadLocal的初始值0赋值给共享变量a,因为每一个线程从threadLocal.get()拿到的值都是自己threadLocal保存的。所以,就没有所以了。 
所以对于共享变量a来讲,每个线程都会首先将自己threadLocal里面的初始值0赋值给a,然后将共享变量a+1,然后将a+1的值设置到自己的ThreadLocalMap中,其他线程就访问不到了。下一个线程来的时候又会将自己threadLocal里面的初始值0赋值给a,然后将 a+1,然后... 如此周而复始。a只是被在0和1之间改来改去,最终放到每一个线程的threadLocal里面的a+1的值就不再共享。对于a这个共享变量来讲,如果在for循环创建的某线程A即将执行代码“2”之前,被其他for循环以外的某个线程改了一把。那么存到线程A的ThreadLocalMap中的值就是被改过的值了。 

综上所述: 
Java的ThreadLocal不是设计用来解决多线程安全问题的,事实证明也解决不了,共享变量a还是会被随意更改。ThreadLocal无能为力。所以,一般用ThreadLocal都不会将一个共享变量放到线程的ThreadLocal中。一般来讲,存放到ThreadLocal中的变量都是当前线程 
本身就独一无二的一个变量。其他线程本身就不能访问,存到ThreadLocal中只是为了方便在程序中同一个线程之间传递这个变量。 
所以,ThreadLocal和解决线程安全没有关系。 
例如Hibernate的代码: 
Java代码  收藏代码
  1. public static final ThreadLocal<session> sessions =      
  2.                                            new ThreadLocal<session>();   
  3. public static Session currentSession() throws HibernateException {     
  4.    Session s = sessions.get();     
  5.    //如果从当前线程变量中获取的session为空,直接open一个session放到当前线程变量中。  
  6.    //值得注意的是,这里的session引用s是在方法体内声明的,属于局部变量,其他线程根本访问不到。  
  7.    //就是说,这里的session s本身就不是一个共享对象,本身就是当前线程独一无二的存在。  
  8.    //下一个线程来的时候,从sessions.get()拿出来的session s又是下一个线程的线程变量中的值。如果为空,同样创建。  
  9.    //所以,session在线程变量内部还是外部都是没有和任何线程共享的。  
  10.    if(s == null) {     
  11.         s = sessionFactory.openSession();     
  12.         sessions.set(s);     
  13.    }     
  14.           


补充一个问题: 
当使用线程池的时候,由于ThreadLocal的设计原理是将一个ThreadLocalMap的引用作为Thread的一个属性,利用当前ThreadLocal作为key,保存的变量值作为value保存在当前线程的ThreadLocalMap中的。所以ThreadLocalMap是伴随的Thread本身的存在而存在的,只要Thread不被回收,ThreadLocalMap就存在。因此,对于线程池来讲,重复利用一个Thread就等于在重复利用Thread的ThreadLocalMap,所以ThreadLocalMap里面保存的数据可能会被多次使用。 
例子: 
Java代码  收藏代码
  1. private static void plus() {  
  2.     Executor executor = Executors.newFixedThreadPool(2);  
  3.     for (int i = 0; i < 10; i++) {  
  4.         executor.execute(new Runnable() {  
  5.             public void run() {  
  6.                 a = threadLocal.get();  
  7.                 threadLocal.set(++a);  
  8.                 System.out.println("plus:" + Thread.currentThread().getName() + ": " + a);  
  9.   
  10.             }  
  11.         });  
  12.     }  
  13. }  

输出结果: 
Java代码  收藏代码
  1. plus:pool-1-thread-11  
  2. plus:pool-1-thread-12  
  3. plus:pool-1-thread-13  
  4. plus:pool-1-thread-14  
  5. plus:pool-1-thread-15  
  6. plus:pool-1-thread-16  
  7. plus:pool-1-thread-17  
  8. plus:pool-1-thread-18  
  9. plus:pool-1-thread-19  
  10. plus:pool-1-thread-21  

可以看出,对于线程池中的两个线程来讲,线程1一直在被重复使用,虽然每次都是从ThreadLocal获取值,但是都是同一个ThreadLocalMap,所以值是递增的。对于线程2来讲,和线程1不是一个线程,不是一个ThreadLocalMap,所以值依然是1。 

线程重复利用,又不想重复利用其ThreadLocalMap中的值怎么办呢? 
我的理解是: 
一般情况下,我们自己用的话ThreadLocal里面都存放的是无状态的对象,只是便于在同一个线程中参数传递,所以个人认为即使重复利用也没有关系。但是如果ThreadLocal里面存放的是有状态的对象的话,在线程使用结束后直接将当前线程的ThreadLocalMap里的值设为初始值就可以了,或者直接remove掉。 
比如 Struts2里面的ActionContext: 
Java代码  收藏代码
  1. public class ActionContext implements Serializable {  
  2.     static ThreadLocal actionContext = new ThreadLocal();  
  3.     ....  
  4.     public static void setContext(ActionContext context) {  
  5.         actionContext.set(context);  
  6.     }  
  7. }  

在struts2的org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter类中,就会无论如何在请求结束后清除当前线程的ThreadLocalMap中的值: 
Java代码  收藏代码
  1. finally {  
  2.             prepare.cleanupRequest(request);  
  3.         }  

Java代码  收藏代码
  1. public void cleanupRequest(HttpServletRequest request) {  
  2.     ActionContext.setContext(null);  
  3. }  
原创粉丝点击