toshine

导航

ThreadLocal详解

一、介绍:

  ThreadLocal是Thread的局部变量,用于编多线程程序,对解决多线程程序的并发问题有一定的启示作用。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中定义了一个ThreadLocalMap,每一个Thread中都有一个该类型的变量——threadLocals——用于存储每一个线程的变量副本Map中元素的键为线程对象,而值对应线程的变量副本

 

二、ThreadLocal类接口很简单,只有4个方法:

1、void get()

2、void set(Object value)

 1  public void set(T value) {
 2         //1、获取当前线程
 3         Thread t = Thread.currentThread();
 4         //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
 5         //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
 6         ThreadLocalMap map = getMap(t);
 7         if (map != null)
 8             map.set(this, value);
 9         else
10             // 初始化thradLocalMap 并赋值
11             createMap(t, value);
12     }

3、public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 1.5 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。一般线程池中对线程的管理都会采用线程复用的方式,复用的这些线程可能永远都不会结束,也不会被垃圾回收,因此需要我们手动remove()当前线程局部变量的值。

4、protected Object initialValue()

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

三、场景:

1、说明:

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。

2、应用:

场景一:存储用户Session

 1 private static final ThreadLocal threadSession = new ThreadLocal();
 2  
 3     public static Session getSession() throws InfrastructureException {
 4         Session s = (Session) threadSession.get();
 5         try {
 6             if (s == null) {
 7                 s = getSessionFactory().openSession();
 8                 threadSession.set(s);
 9             }
10         } catch (HibernateException ex) {
11             throw new InfrastructureException(ex);
12         }
13         return s;
14     }

场景二:数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。

 1 package com.kong.threadlocal;
 2  
 3  
 4 public class ThreadLocalDemo05 {
 5     public static void main(String[] args) {
 6         User user = new User("jack");
 7         new Service1().service1(user);
 8     }
 9  
10 }
11  
12 class Service1 {
13     public void service1(User user){
14         //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
15         UserContextHolder.holder.set(user);
16         new Service2().service2();
17     }
18 }
19  
20 class Service2 {
21     public void service2(){
22         User user = UserContextHolder.holder.get();
23         System.out.println("service2拿到的用户:"+user.name);
24         new Service3().service3();
25     }
26 }
27  
28 class Service3 {
29     public void service3(){
30         User user = UserContextHolder.holder.get();
31         System.out.println("service3拿到的用户:"+user.name);
32         //在整个流程执行完毕后,一定要执行remove
33         UserContextHolder.holder.remove();
34     }
35 }
36  
37 class UserContextHolder {
38     //创建ThreadLocal保存User对象
39     public static ThreadLocal<User> holder = new ThreadLocal<>();
40 }
41  
42 class User {
43     String name;
44     public User(String name){
45         this.name = name;
46     }
47 }
48  
49 执行的结果:
50  
51 service2拿到的用户:jack
52 service3拿到的用户:jack
View Code

场景四、Spring使用ThreadLocal解决线程安全问题

TopicDao:非线程安全

1  
2 public class TopicDao {
3    //①一个非线程安全的变量
4    private Connection conn; 
5    public void addTopic(){
6         //②引用非线程安全变量
7        Statement stat = conn.createStatement();
8 9    }

TopicDao:线程安全 

 1  
 2 import java.sql.Connection;
 3 import java.sql.Statement;
 4 public class TopicDao {
 5  
 6   //①使用ThreadLocal保存Connection变量
 7 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
 8 public static Connection getConnection(){
 9          
10         //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
11         //并将其保存到线程本地变量中。
12 if (connThreadLocal.get() == null) {
13             Connection conn = ConnectionManager.getConnection();
14             connThreadLocal.set(conn);
15               return conn;
16         }else{
17               //③直接返回线程本地变量
18             return connThreadLocal.get();
19         }
20     }
21     public void addTopic() {
22  
23         //④从ThreadLocal中获取线程对应的
24          Statement stat = getConnection().createStatement();
25     }

 

四、ThreadLocal与Synchronized的区别:

ThreadLocal和Synchonized都用于解决对于多线程资源共享的问题

但是ThreadLocal与synchronized有本质的区别:

1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

2、Synchronized是利用同步锁机制,使变量或代码块在某一时刻只能被一个线程访问,采用了“以时间换空间”的方式。而ThreadLocal为每一个线程都提供了一份独立变量副本,因此可以同时访问而互不影响,采用了“以空间换时间”的方式。

五、ThreadLocal与Thread,ThreadLocalMap之间的关系  :

1、每个Thread线程内部都有一个Map(ThreadLocalMap)。

2、Map里面存储ThreadLocal对象(key)和线程的变量副本(value)。

3、Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

 

六、ThreadLocal代码案例:

从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

 1 public class ThreadLocaDemo {
 2  
 3     private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 4  
 5     static void print(String str) {
 6         //打印当前线程中本地内存中本地变量的值
 7         System.out.println(str + " :" + localVar.get());
 8         //清除本地内存中的本地变量
 9         localVar.remove();
10     }
11     public static void main(String[] args) throws InterruptedException {
12  
13         new Thread(new Runnable() {
14             public void run() {
15                 ThreadLocaDemo.localVar.set("local_A");
16                 print("A");
17                 //打印本地变量
18                 System.out.println("after remove : " + localVar.get());
19                
20             }
21         },"A").start();
22  
23         Thread.sleep(1000);
24  
25         new Thread(new Runnable() {
26             public void run() {
27                 ThreadLocaDemo.localVar.set("local_B");
28                 print("B");
29                 System.out.println("after remove : " + localVar.get());
30               
31             }
32         },"B").start();
33     }
34 }
35  
36 A :local_A
37 after remove : null
38 B :local_B
39 after remove : null
40  
View Code

七、问题解析:

1、value值为什么不存在并发问题?

因为只有一个线程能访问,每个线程都有自己的threadlocal 变量,不同的threadlocal对应于不同的value值,他们之间互不影响。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

2、ThreadLocal 内存泄露的原因

(1)没有手动删除这个 Entry
(2)CurrentThread 当前线程依然运行

由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。

3、为什么 key 要用弱引用?

事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。

4、如何正确的使用ThreadLocal?

将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

posted on 2023-04-16 17:04  加瓦开阀攻城狮  阅读(65)  评论(0)    收藏  举报

……