用ThreadLocal解决多线程安全问题


  • 什么是线程?

度娘说:线程(thread, 台湾称 执行绪)是"进程"中某个单一顺序的控制流。也被称为轻量进程(lightweight processes)。计算机科学术语,指运行中的程序的调度单位。


  • java的线程

假如我们做的是web程序,那么http的每次请求都会在一个java进程中启动,并且这个程序会生成一个线程去跑。所以java写的web程序是多线程的。我们可以通过Thread.currentThread().getName();方法查看Http每次请求的线程名称。

例如,我们在Spring中创建一个测试的Action。访问地址:http://127.0.0.1:8090/test/test/

代码如下:

  1. package com.xxx.www.test.web;  
  2.   
  3. import java.util.List;  
  4.   
  5. import org.springframework.beans.factory.annotation.Autowired;  
  6. import org.springframework.stereotype.Controller;  
  7. import org.springframework.web.bind.annotation.RequestMapping;  
  8. import org.springframework.web.bind.annotation.RequestMethod;  
  9. import org.springframework.web.bind.annotation.ResponseBody;  
  10. import org.springframework.web.servlet.ModelAndView;  
  11. import com.alios.www.test.domain.GuestbookDo;  
  12. import com.alios.www.test.service.GuestbookService;  
  13. import com.alios.www.test.service.TestService;  
  14.   
  15.   
  16. /** 
  17.  * 入口文件 
  18.  * @author zhuli 
  19.  * 
  20.  */  
  21. @Controller  
  22. @RequestMapping(value="/test")  
  23. public class IndexController {  
  24.       
  25.     /** 
  26.      * TestService 
  27.      */  
  28.     @Autowired  
  29.     private TestService TestService;  
  30.       
  31.     /** 
  32.      * 留言板 
  33.      */  
  34.     @Autowired  
  35.     private GuestbookService guestbookService;  
  36.       
  37.   
  38.     @RequestMapping(value="/test")  
  39.     @ResponseBody  
  40.     public String test() {  
  41.         return Thread.currentThread().getName();  
  42.     }  
  43.       
  44.       
  45.   
  46. }  

然后我们可以在浏览器中看到当前访问线程的名称:


但是有疑问啊,为什么每次刷新都是一样的名称呢?因为服务器请求处理速度很快,第一次请求很快就完毕,然后这个线程就空闲被释放,等你第二次再来请求的时候,还是这个线程的名称。

为了显示更加明显一点,我们做一个实验,修改一下代码:

  1. @RequestMapping(value="/test")  
  2. @ResponseBody  
  3. public String test() {  
  4.     //运行慢一点  
  5.     int i = 0;  
  6.     int j = 0;  
  7.     for (i = 0; i < 1000000000; i++) {  
  8.         j++;  
  9.         j = j + 1000;  
  10.         j = j - 1000;  
  11.     }  
  12.     //然后再控制台输出每次请求的线程名称  
  13.     System.out.print(Thread.currentThread().getName() + "\r\n");  
  14.     return Thread.currentThread().getName();  
  15. }  

然后继续在浏览器中访问:http://127.0.0.1:8090/test/test/,但是这次要连续按F5键刷新。这个时候我们看到控制台输出了四个不同的线程名称。也就是说,如果我们一个线程在忙碌中,那么会自动新生成一个线程,而线程与线程之间是互相不干扰的。


  • WEB编程中的危险用法。
    1. package com.alios.www.test.web;  
    2.   
    3. import java.util.List;  
    4.   
    5. import org.springframework.beans.factory.annotation.Autowired;  
    6. import org.springframework.stereotype.Controller;  
    7. import org.springframework.web.bind.annotation.RequestMapping;  
    8. import org.springframework.web.bind.annotation.RequestMethod;  
    9. import org.springframework.web.bind.annotation.ResponseBody;  
    10. import org.springframework.web.servlet.ModelAndView;  
    11.   
    12. import com.alios.www.test.domain.GuestbookDo;  
    13. import com.alios.www.test.service.GuestbookService;  
    14. import com.alios.www.test.service.TestService;  
    15.   
    16.   
    17. /** 
    18.  * 入口文件 
    19.  * @author zhuli 
    20.  * 
    21.  */  
    22. @Controller  
    23. @RequestMapping(value="/test")  
    24. public class IndexController {  
    25.       
    26.     /** 
    27.      * TestService 
    28.      */  
    29.     @Autowired  
    30.     private TestService TestService;  
    31.       
    32.     /** 
    33.      * 留言板 
    34.      */  
    35.     @Autowired  
    36.     private GuestbookService guestbookService;  
    37.       
    38.     private int i = 0//i是一个全局变量,会常驻内存,线程可以共享这个变量。  
    39.       
    40.       
    41.       
    42.     @RequestMapping(value="/test")  
    43.     @ResponseBody  
    44.     public String test() {  
    45.         //运行慢一点  
    46.         int i = 0;  
    47.         int j = 0;  
    48.         for (i = 0; i < 1000000000; i++) {  
    49.             j++;  
    50.             j = j + 1000;  
    51.             j = j - 1000;  
    52.         }  
    53.         //然后再控制台输出每次请求的线程名称  
    54.         this.i++;   
    55.         System.out.print(Thread.currentThread().getName() + ":" + this.i + "\r\n");  
    56.         return Thread.currentThread().getName();  
    57.     }  
    58.       
    59. }  

    结果:



    1. 因为Spring的Controller类以及Dao,Service类是单例的模式,实例化之后会常驻内存,i是全局变量,也会常驻内存中,每次HTTP请求完毕之后,全局变量不会销毁。
    2. 只有在test()函数内的局部变量才会每次请求完毕就会销毁。
    3. web开发是多线程的环境,如果随意定义全局变量,可能会导致数据之间的覆盖。


  • Spring框架如何解决多线程安全问题

    用Spring框架开发web项目,我们的dao和Service,Controller一般都是单例的,并且是无状态的,那么这些只要常驻内存就可以了。但是对于那些HTTP请求进来的Request数据是如何处理的呢,这种“状态性”对象如果单例去解决的话,那就不和上面i的变量一样,每个请求就来就会改变这个i的值,不同的请求进来就会互相造成数据的覆盖。那Spring如何处理这些Request请求的数据只在当前的线程中有效呢?

    回想一下上面我们用到的Thread对象,那么我们估计就有想法了。为什么我们能在我们的web程序中直接调用,我们先看一段创建线程的代码:

    1. package mythread;    
    2.      
    3.  public class Thread1 extends Thread    
    4.   {    
    5.       public void run()    
    6.       {    
    7.           System.out.println(this.getName());    
    8.       }    
    9.       public static void main(String[] args)    
    10.       {    
    11.           System.out.println(Thread.currentThread().getName());    
    12.           Thread1 thread1 = new Thread1();    
    13.           Thread1 thread2 = new Thread1 ();    
    14.           thread1.start();    
    15.           thread2.start();    
    16.       }    
    17.   }   

    结果:

    main
    Thread-0
    Thread-1

    从中我们可以很清晰的看到,Java能够通过Thread类来创建子线程,那么我们通过Spring框架入口进来的程序也可能是在Main函数中,通过创建子线程的方式来处理并行的请求。所以我们在我们并非请求的时候得到的线程名称是不一样的。

    既然我们可以获取我们每个程序运行的时候所在的线程环境以及线程名称等详细信息,那么我们在全局作用域中可以开辟一块内存用于管理每个线程对应的内部运行数据,这样就可以实现每个线程可以独自运行并且互相没有干扰。

    下面是一个非常简单的线程安全管理类的实现:

    1. import java.util.Collections;  
    2. import java.util.HashMap;  
    3. import java.util.Map;  
    4. //对象 薄层封装 容器类  
    5. public class MyThreadLocal {  
    6. //定义一个成员变量 同步一个hashMap  
    7. private Map valueMap=Collections.synchronizedMap(new HashMap());  
    8. //get Map 添加值  
    9. public void set(Object newValue){  
    10.    valueMap.put(Thread.currentThread(), newValue);  
    11. }  
    12. //  
    13. public Object get(){  
    14.    //当前线程  
    15.    Thread currentThread = Thread.currentThread();  
    16.    //获取当前线程的值 对象  
    17.    Object o=  
    18.    valueMap.get(currentThread);  
    19.    //本地value map 和 currentThread 比较  
    20.    if(o==null && ! valueMap.containsKey(currentThread)){  
    21.     //如果不存在  
    22.     o=initialValue();  
    23.     //则放入  
    24.     valueMap.put(currentThread, o);  
    25.    }  
    26.      
    27.     
    28.     
    29.    return o;  
    30. }  
    31.   
    32. public void remove(){  
    33.    valueMap.remove(Thread.currentThread());  
    34. }  
    35.   
    36. //注意此方法  
    37. public Object initialValue(){  
    38.    return null;  
    39. }  
    40.   
    41. }  


ThreadLocal

上面的实现只是简单模拟了一下多线程安全的管理。Java提供了ThreadLocal类,让我们更好的可以实现对象管理。

ThreadLocal提供了四个主要的方法:

设置当前线程的线程局部变量的值。
方法说明
void set(Object value)设置当前线程的线程局部变量的值。
public Object get()该方法返回当前线程所对应的线程局部变量。
public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

看下面的例子代码:
  1. package com.alios.www.test.web;  
  2.   
  3. import java.util.List;  
  4.   
  5. import org.springframework.beans.factory.annotation.Autowired;  
  6. import org.springframework.stereotype.Controller;  
  7. import org.springframework.web.bind.annotation.RequestMapping;  
  8. import org.springframework.web.bind.annotation.RequestMethod;  
  9. import org.springframework.web.bind.annotation.ResponseBody;  
  10. import org.springframework.web.servlet.ModelAndView;  
  11.   
  12. import com.alios.www.test.domain.GuestbookDo;  
  13. import com.alios.www.test.service.GuestbookService;  
  14. import com.alios.www.test.service.TestService;  
  15.   
  16.   
  17. /** 
  18.  * 入口文件 
  19.  * @author zhuli.zhul 
  20.  * 
  21.  */  
  22. @Controller  
  23. @RequestMapping(value="/test")  
  24. public class IndexController {  
  25.       
  26.     /** 
  27.      * TestService 
  28.      */  
  29.     @Autowired  
  30.     private TestService TestService;  
  31.       
  32.     /** 
  33.      * 留言板 
  34.      */  
  35.     @Autowired  
  36.     private GuestbookService guestbookService;  
  37.       
  38.     private static ThreadLocal<Integer> iLocal = new ThreadLocal<Integer>(); //实例化一个ThreadLocal对象  
  39.     public static int getI() {  
  40.         if (iLocal.get() == null) { //如果不存在,默认设置为1  
  41.             iLocal.set(0);  
  42.             return 0;  
  43.         } else {  
  44.             int i = iLocal.get(); //存在,i++  
  45.             i++;  
  46.             iLocal.set(i);  
  47.             return i;  
  48.         }  
  49.     }  
  50.       
  51.     @RequestMapping(value="/test")  
  52.     @ResponseBody  
  53.     public String test() {  
  54.         //运行慢一点  
  55.         int i = 0;  
  56.         int j = 0;  
  57.         for (i = 0; i < 1000000000; i++) {  
  58.             j++;  
  59.             j = j + 1000;  
  60.             j = j - 1000;  
  61.         }  
  62.         System.out.print(Thread.currentThread().getName() + " : " +getI()+ "\r\n"); //控制台输出i  
  63.         return Thread.currentThread().getName();  
  64.     }  
  65.       
  66. }  

结果:



所以上面的i++,实现了在同一线程中的自增。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值