• Java多线程——ThreadLocal类的原理和使用


    Java多线程——ThreadLocal类的原理和使用

    摘要:本文主要学习了ThreadLocal类的原理和使用。

    概述

    是什么

    ThreadLocal可以用来维护一个变量,提供了一个ThreadLocalMap内部类,用来对变量进行设置、获取、删除等操作,原理类似于集合的Map,在Thread类里也提供了一个ThreadLocalMap类型的变量。

    在使用ThreadLocal维护变量时,实际上是通过ThreadLocalMap进行维护的,使用的是当前线程里的ThreadLocalMap对象,保存的key是ThreadLocal的实例本身。

    ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    使用场景

    在一个线程中,需要共享某个资源,希望不管是在哪个类中使用该资源,都能保证该资源都是同一个,只会被创建一次,这就需要使用TheadLocal来实现。当然,也可以是用饿汉式,或者懒汉式的线程安全方式实现。

    使用static修饰

    由于ThreadlocalMap中的key是this引用,this引用也就是ThreadLocal的实例对象,也就是说,即便是在同一个线程里,ThreadLocal实例对象如果不是同一个,那么通过get()方法得到的就不是同一个值。

    所以,要想在同一个线程里,每次得到的值都是同一个,就必须使this指针指向同一个对象,这就解释了为什么Threadlocal都使用静态变量来保存。

    但是需要注意的是,如果使用了static关键字,有可能导致形成内存泄漏所需要的条件。

    常用方法

    set()方法

    设置当前线程对应的线程局部变量的值。先取出当前线程对应的ThreadLocalMap对象。如果存在将当前ThreadLocal对象作为key,传入的值作为value,放到map里。如果不存在则通过线程创建一个ThreadLocalMap对象。

    1 public void set(T value) {
    2     Thread t = Thread.currentThread();
    3     ThreadLocalMap map = getMap(t);
    4     if (map != null)
    5         map.set(this, value);
    6     else
    7         createMap(t, value);
    8 }

    get()方法

    返回当前线程所对应的线程局部变量。和set类似,也是先取出当前线程对应的ThreadLocalMap对象。如果存在就直接从map取出ThreadLocal对应的value返回。如果不存在则调用setInitialValue()方法,该方法会创建一个ThreadLocalMap对象,并且调用initialValue()方法设置初始值并返回initialValue。

     1 public T get() {
     2     Thread t = Thread.currentThread();
     3     ThreadLocalMap map = getMap(t);
     4     if (map != null) {
     5         ThreadLocalMap.Entry e = map.getEntry(this);
     6         if (e != null) {
     7             @SuppressWarnings("unchecked")
     8             T result = (T)e.value;
     9             return result;
    10         }
    11     }
    12     return setInitialValue();
    13 }

    remove()方法

    将当前线程局部变量的值删除,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。需要注意的是,如果remove之后又调用了get()方法,会重新初始化一次,即再次调用initialValue()方法,除非在这之前调用set()方法设置过值。

    1 public void remove() {
    2     ThreadLocalMap m = getMap(Thread.currentThread());
    3     if (m != null)
    4         m.remove(this);
    5 }

    initialValue()方法

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

    1 protected T initialValue() {
    2     return null;
    3 }

    使用实例

    使用static关键字修饰资源操作类里的ThreadLocal类型的成员变量

    以下代码模拟了三个线程,每个线程分别表示一个User用户资源,每个用户分别进行了Login操作和Logout操作,代码如下:

     1 public class Demo {
     2     public static void main(String[] args) {
     3         // 模拟三个线程使用。
     4         for (int i = 0; i < 3; i++) {
     5             new Thread() {
     6                 @Override
     7                 public void run() {
     8                     LoginService.login(new UserHandler().getUser());
     9                     LogoutService.logout(new UserHandler().getUser());
    10                 }
    11             }.start();
    12         }
    13     }
    14 }
    15 
    16 // 业务操作类,模拟Login操作。
    17 class LoginService {
    18     public static void login(User user) {
    19         System.out.println(Thread.currentThread().getName() + " User " + user.hashCode() + " login ...");
    20     }
    21 }
    22 
    23 // 业务操作类,模拟Logout操作。
    24 class LogoutService {
    25     public static void logout(User user) {
    26         System.out.println(Thread.currentThread().getName() + " User " + user.hashCode() + " logout ...");
    27     }
    28 }
    29 
    30 // 资源操作类,提供获取资源的方法。
    31 class UserHandler {
    32     // 私有,其他类不能直接操作ThreadLocal类,避免内存泄露。
    33     private static ThreadLocal<User> tl = new ThreadLocal<User>() {
    34         
    35         // 重写初始化方法,首次使用时调用。
    36         protected User initialValue() {
    37             return new User();
    38         };
    39     };
    40     
    41     // 公有,获取资源的方法。
    42     public User getUser() {
    43         return tl.get();
    44     }
    45 }
    46 
    47 // 资源类,不允许除了资源操作类以外的类使用。
    48 class User {
    49     private String name;
    50 
    51     public String getName() {
    52         return name;
    53     }
    54 
    55     public void setName(String name) {
    56         this.name = name;
    57     }
    58 }

    运行结果如下:

    1 Thread-0 User 103821930 login ...
    2 Thread-2 User 412303685 login ...
    3 Thread-1 User 443387063 login ...
    4 Thread-0 User 103821930 logout ...
    5 Thread-2 User 412303685 logout ...
    6 Thread-1 User 443387063 logout ...

    结果说明:

    可以发现,在一个线程里,进行操作的始终是一个用户资源类。

    不使用static关键字修饰资源操作类里的ThreadLocal类型的成员变量

    将资源操作类里的修饰ThreadLocal类型的成员变量的static关键字去掉,其他部分不变,部分代码如下:

     1 // 资源操作类,提供获取资源的方法。
     2 class UserHandler {
     3     // 私有,其他类不能直接操作ThreadLocal类,避免内存泄露。
     4     private ThreadLocal<User> tl = new ThreadLocal<User>() {
     5         
     6         // 重写初始化方法,首次使用时调用。
     7         protected User initialValue() {
     8             return new User();
     9         };
    10     };
    11     
    12     // 公有,获取资源的方法。
    13     public User getUser() {
    14         return tl.get();
    15     }
    16 }

    运行结果如下:

    1 Thread-1 User 1127398600 login ...
    2 Thread-0 User 1988907679 login ...
    3 Thread-2 User 1532003980 login ...
    4 Thread-2 User 1601112382 logout ...
    5 Thread-1 User 1916516959 logout ...
    6 Thread-0 User 1924384918 logout ...

    结果说明:

    因为没有使用static关键字修饰,导致set()方法设置资源对象的时候,key不是同一个,从而导致在一个线程里调用get()方法的时候得到了不同的资源对象。

    内存泄漏

    产生原因

    1)ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在

    如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条由Thread到ThreaLocalMap,再到Entry里的value的强引用链,导致value对应的Object一直无法被回收,产生内存泄露。

    查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。

    2)线程结束了,ThreadLocal一直存在

    用static修饰的ThreadLocal,导致ThreadLocal的生命周期和持有它的类一样长,意味着这个ThreadLocal不会被GC。这种情况下,如果不手动删除,Entry的key永远不为null,弱引用就失去了意义,理所当然的无法通过调用set(),get(),remove()方法清除value,如果当前线程结束了,就导致了Entry的内存泄漏。

    解决办法

    每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

    在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

  • 相关阅读:
    王艳 201771010127《面向对象程序设计(java)》第三周学习总结
    王艳 201771010127《面向对象程序设计(java)》第二周学习总结
    刘志梅2017710101152.《面向对象程序设计(java)》第十三周学习总结
    刘志梅2017710101152.《面向对象程序设计(java)》第十二周学习总结
    刘志梅2017710101152.《面向对象程序设计(java)》第十一周学习总结
    刘志梅 2017710101152《面向对象程序设计(java)》 第十周学习总结
    刘志梅 201771010115 《面向对象程序设计(java)》 第九周学习总结
    刘志梅 201771010115 《面向对象程序设计(java)》 第八周学习总结
    刘志梅 201771010115 《面向对象程序设计(java)》 第七周学习总结
    刘志梅201771010115.《面向对象程序设计(java)》第六周学习总结
  • 原文地址:https://www.cnblogs.com/shamao/p/11103364.html
Copyright © 2020-2023  润新知