聊一聊Spring中的线程安全性

Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。

Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。

  • singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。

  • prototype:bean被定义为在每次注入时都会创建一个新的对象。

  • request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。

  • session:bean被定义为在一个session的生命周期内创建一个单例对象。

  • application:bean被定义为在ServletContext的生命周期中复用一个单例对象。

  • websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。

无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题),用完即关(或交还给连接池)。

有人可能会认为,我使用request作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误的,因为Controller默认是单例的,一个HTTP请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把Controller的scope改成prototype,实际上Struts2就是这么做的,但有一点要注意,Spring MVC对请求的拦截粒度是基于每个方法的,而Struts2是基于每个类的,所以把Controller设为多例将会频繁的创建与回收对象,严重影响到了性能。

通过阅读上文其实已经说的很清楚了,Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。

下面将通过解析ThreadLocal的源码来了解它的实现与作用,ThreadLocal是一个很好用的工具类,它在某些情况下解决了线程安全问题(在变量不需要被多个线程共享时)。

本文作者为SylvanasSun(sylvanas.sun@gmail.com),首发于SylvanasSun’s Blog
原文链接:sylvanassun.github.io/2017/11/06/…
(转载请务必保留本段声明,并且保留超链接。)

ThreadLocal


ThreadLocal是一个为线程提供线程局部变量的工具类。它的思想也十分简单,就是为线程提供一个线程私有的变量副本,这样多个线程都可以随意更改自己线程局部的变量,不会影响到其他线程。不过需要注意的是,ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。

ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。

一、何谓“ThreadLocal”

ThreadLocal是一个线程局部变量,我们都知道全局变量和局部变量的区别,拿Java举例就是定义在类中的是全局的变量,各个方法中都能访问得到,而局部变量定义在方法中,只能在方法内访问。那线程局部变量(ThreadLocal)就是每个线程都会有一个局部变量,独立于变量的初始化副本,而各个副本是通过线程唯一标识相关联的。

二、ThreadLocal的用法

(1)方法摘要

作用域类型方法描述
public T get() 返回此线程局部变量的当前线程副本中的值
protected T initialValue() 返回此线程局部变量的当前线程的“初始值”
public void remove() 移除此线程局部变量当前线程的值
public void set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值

注意事项: 
==initialValue()== 这个方法是为了让子类覆盖设计的,默认缺省null。如果get()后又remove()则可能会在调用一下此方法。 
==remove()== 移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程 读取,且这期间当前线程没有 设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue 方法。

(2)常规用法

在开始之前贴出一个公共的线程测试类

 1 public class TaskThread<T> extends Thread{
 2 
 3     private T t;
 4 
 5     public TaskThread(String threadName,T t) {
 6         this.setName(threadName);
 7         this.t = t;
 8     }
 9 
10     @Override
11     public void run() {
12         for (int i = 0; i < 2; i++) {
13 
14             try {
15                 Class[] argsClass = new Class[0];
16                 Method method = t.getClass().getMethod("getUniqueId",argsClass);
17                 int value = (int) method.invoke(t);
18                 System.out.println("thread[" + Thread.currentThread().getName() + "] --> uniqueId["+value+ "]");
19 
20             } catch (NoSuchMethodException e) {
21                 // TODO 暂不处理
22                 continue;
23 
24             } catch (IllegalAccessException e) {
25                 // TODO 暂不处理
26                 continue;
27 
28             } catch (InvocationTargetException e) {
29                 // TODO 暂不处理
30                 continue;
31 
32             }
33 
34 
35         }
36     }
37 
38 }

例1:为每个线程生成一个唯一的局部标识

 1 public class UniqueThreadIdGenerator {
 2 
 3     // 原子整型
 4     private static final AtomicInteger uniqueId = new AtomicInteger(0);
 5 
 6     // 线程局部整型变量
 7     private static final ThreadLocal <Integer> uniqueNum =
 8             new ThreadLocal < Integer > () {
 9                 @Override protected Integer initialValue() {
10                     return uniqueId.getAndIncrement();
11                 }
12             };
13 
14     //变量值
15     public static int getUniqueId() {
16         return uniqueId.get();
17     }
18 
19     public static void main(String[] args) {
20         UniqueThreadIdGenerator uniqueThreadId = new UniqueThreadIdGenerator();
21         // 为每个线程生成一个唯一的局部标识
22         TaskThread t1 = new TaskThread<UniqueThreadIdGenerator>("custom-thread-1", uniqueThreadId);
23         TaskThread t2 = new TaskThread<UniqueThreadIdGenerator>("custom-thread-2", uniqueThreadId);
24         TaskThread t3 = new TaskThread<UniqueThreadIdGenerator>("custom-thread-3", uniqueThreadId);
25         t1.start();
26         t2.start();
27         t3.start();
28     }
29 
30 }

运行结果:

//每个线程的局部变量都是唯一的
thread[custom-thread-2] --> uniqueId[0]
thread[custom-thread-2] --> uniqueId[0]
thread[custom-thread-1] --> uniqueId[0]
thread[custom-thread-1] --> uniqueId[0]
thread[custom-thread-3] --> uniqueId[0]
thread[custom-thread-3] --> uniqueId[0]

例2:为每个线程创建一个局部唯一的序列

 1 public class UniqueSequenceGenerator {
 2 
 3     // 线程局部整型变量
 4     private static final ThreadLocal <Integer> uniqueNum =
 5             new ThreadLocal < Integer > () {
 6                 @Override protected Integer initialValue() {
 7                     return 0;
 8                 }
 9             };
10 
11     //变量值
12     public static int getUniqueId() {
13         uniqueNum.set(uniqueNum.get() + 1);
14         return uniqueNum.get();
15     }
16 
17     public static void main(String[] args) {
18         UniqueSequenceGenerator uniqueThreadId = new UniqueSequenceGenerator();
19         // 为每个线程生成内部唯一的序列号
20         TaskThread t1 = new TaskThread<UniqueSequenceGenerator>("custom-thread-1", uniqueThreadId);
21         TaskThread t2 = new TaskThread<UniqueSequenceGenerator>("custom-thread-2", uniqueThreadId);
22         TaskThread t3 = new TaskThread<UniqueSequenceGenerator>("custom-thread-3", uniqueThreadId);
23         t1.start();
24         t2.start();
25         t3.start();
26     }
27 
28 }

运行结果:

thread[custom-thread-2] --> uniqueId[1]
thread[custom-thread-2] --> uniqueId[2]
thread[custom-thread-1] --> uniqueId[1]
thread[custom-thread-1] --> uniqueId[2]
thread[custom-thread-3] --> uniqueId[1]
thread[custom-thread-3] --> uniqueId[2]

三、ThreadLocal的原理(摘自网上)

(1)源码解析

源码实现片段:set

 1 /** 
 2     * Sets the current thread's copy of this thread-local variable 
 3     * to the specified value.  Most subclasses will have no need to 
 4     * override this method, relying solely on the {@link #initialValue} 
 5     * method to set the values of thread-locals. 
 6     * 
 7     * @param value the value to be stored in the current thread's copy of 
 8     *        this thread-local. 
 9     */  
10    public void set(T value) {  
11        Thread t = Thread.currentThread();  
12        ThreadLocalMap map = getMap(t);  
13        if (map != null)  
14            map.set(this, value);  
15        else  
16            createMap(t, value);  
17    } 

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

==线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。== 这个就是实现原理

源码实现片段:getMap、createMap

 1 /** 
 2  * Get the map associated with a ThreadLocal. Overridden in 
 3  * InheritableThreadLocal. 
 4  * 
 5  * @param  t the current thread 
 6  * @return the map 
 7  */  
 8 ThreadLocalMap getMap(Thread t) {  
 9     return t.threadLocals;  
10 }  
11   
12 /** 
13  * Create the map associated with a ThreadLocal. Overridden in 
14  * InheritableThreadLocal. 
15  * 
16  * @param t the current thread 
17  * @param firstValue value for the initial entry of the map 
18  * @param map the map to store. 
19  */  
20 void createMap(Thread t, T firstValue) {  
21     t.threadLocals = new ThreadLocalMap(this, firstValue);  
22 }  

源码实现片段:get

 1 /** 
 2  * Returns the value in the current thread's copy of this 
 3  * thread-local variable.  If the variable has no value for the 
 4  * current thread, it is first initialized to the value returned 
 5  * by an invocation of the {@link #initialValue} method. 
 6  * 
 7  * @return the current thread's value of this thread-local 
 8  */  
 9 public T get() {  
10     Thread t = Thread.currentThread();  
11     ThreadLocalMap map = getMap(t);  
12     if (map != null) {  
13         ThreadLocalMap.Entry e = map.getEntry(this);  
14         if (e != null)  
15             return (T)e.value;  
16     }  
17     return setInitialValue();  
18 }  

源码实现片段:setInitialValue

 1 /** 
 2     * Variant of set() to establish initialValue. Used instead 
 3     * of set() in case user has overridden the set() method. 
 4     * 
 5     * @return the initial value 
 6     */  
 7    private T setInitialValue() {  
 8        T value = initialValue();  
 9        Thread t = Thread.currentThread();  
10        ThreadLocalMap map = getMap(t);  
11        if (map != null)  
12            map.set(this, value);  
13        else  
14            createMap(t, value);  
15        return value;  
16    }  
17    
18    //获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键
19    //进行查找的,这当然和前面set()方法的代码是相呼应的。进一步地,我们可以创建不同的
20    //ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不
21    //同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同
22    //的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个
23    //HashMap对象中存储一个键值对和多个键值对一样,仅此而已。

四、ThreadLocal实际用途

例1:在数据库管理中的连接管理类是下面这样的:(摘自网上)

 1 public class ConnectionManager {
 2     private static Connection connect = null;
 3 
 4     public static Connection getConnection() {
 5         if(connect == null){
 6             connect = DriverManager.getConnection();
 7         }
 8         return connect;
 9     }
10 
11     ...
12 
13 } 

在单线程的情况下这样写并没有问题,但如果在多线程情况下回出现线程安全的问题。你可能会说用同步关键字或锁来保障线程安全,这样做当然是可行的,但考虑到性能的问题所以这样子做并是很优雅。 
下面是改造后的代码:

 1 public class ConnectionManager {
 2 
 3     private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
 4 
 5     public static Connection getConnection() {
 6         if(connThreadLocal.get() != null)
 7             return connThreadLocal.get();
 8         
 9         //获取一个连接并设置到当前线程变量中
10         Connection conn = getConnection();
11         connThreadLocal.set(conn);
12         return conn;
13     }
14     
15     ...
16 
17 }

例2:日期格式(摘自网上)

使用这个日期格式类主要作用就是将枚举对象转成Map而map的值则是使用ThreadLocal存储,那么在实际的开发中可以在同一线程中的不同方法中使用日期格式而无需在创建日期格式的实例。

 1 public class DateFormatFactory {
 2 
 3     public enum DatePattern {
 4 
 5         TimePattern("yyyy-MM-dd HH:mm:ss"),
 6         DatePattern("yyyy-MM-dd");
 7 
 8         public String pattern;
 9 
10         private DatePattern(String pattern) {
11             this.pattern = pattern;
12         }
13     }
14 
15     private static final Map<DatePattern, ThreadLocal<DateFormat>> pattern2ThreadLocal;
16 
17     static {
18         DatePattern[] patterns = DatePattern.values();
19         int len = patterns.length;
20         pattern2ThreadLocal = new HashMap<DatePattern, ThreadLocal<DateFormat>>(len);
21 
22         for (int i = 0; i < len; i++) {
23             DatePattern datePattern = patterns[i];
24             final String pattern = datePattern.pattern;
25 
26             pattern2ThreadLocal.put(datePattern, new ThreadLocal<DateFormat>() {
27                 @Override
28                 protected DateFormat initialValue() {
29                     return new SimpleDateFormat(pattern);
30                 }
31             });
32         }
33     }
34 
35     //获取DateFormat
36     public static DateFormat getDateFormat(DatePattern pattern) {
37         ThreadLocal<DateFormat> threadDateFormat = pattern2ThreadLocal.get(pattern);
38         //不需要判断threadDateFormat是否为空
39         return threadDateFormat.get();
40     }
41 
42     public static void main(String[] args) {
43          String dateStr = DateFormatFactory.getDateFormat(DatePattern.TimePattern).format(new Date());
44          System.out.println(dateStr);
45     }
46 
47 
48 }

ThreadLocal中的内存泄漏


我们要考虑一种会发生内存泄漏的情况,如果ThreadLocal被设置为null后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value会形成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数的源码为例。

 1 /**
 2          * Get the entry associated with key.  This method
 3          * itself handles only the fast path: a direct hit of existing
 4          * key. It otherwise relays to getEntryAfterMiss.  This is
 5          * designed to maximize performance for direct hits, in part
 6          * by making this method readily inlinable.
 7          *
 8          * @param  key the thread local object
 9          * @return the entry associated with key, or null if no such
10          */
11         private Entry getEntry(ThreadLocal<?> key) {
12             int i = key.threadLocalHashCode & (table.length - 1);
13             Entry e = table[i];
14             if (e != null && e.get() == key)
15                 return e;
16             else
17                 return getEntryAfterMiss(key, i, e);
18         }
19 
20         /**
21          * Version of getEntry method for use when key is not found in
22          * its direct hash slot.
23          *
24          * @param  key the thread local object
25          * @param  i the table index for key's hash code
26          * @param  e the entry at table[i]
27          * @return the entry associated with key, or null if no such
28          */
29         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
30             Entry[] tab = table;
31             int len = tab.length;
32 
33             // 清理key为null的Entry
34             while (e != null) {
35                 ThreadLocal<?> k = e.get();
36                 if (k == key)
37                     return e;
38                 if (k == null)
39                     expungeStaleEntry(i);
40                 else
41                     i = nextIndex(i, len);
42                 e = tab[i];
43             }
44             return null;
45         }

在上文中我们发现了ThreadLocalMap的key是一个弱引用,那么为什么使用弱引用呢?使用强引用key与弱引用key的差别如下:

  • 强引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的强引用,如果不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。

  • 弱引用key:ThreadLocal被设置为null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在之后调用set()、getEntry()和remove()函数时会清除所有key为null的Entry。

但要注意的是,ThreadLocalMap仅仅含有这些被动措施来补救内存泄漏问题。如果你在之后没有调用ThreadLocalMap的set()、getEntry()和remove()函数的话,那么仍然会存在内存泄漏问题。

在使用线程池的情况下,如果不及时进行清理,内存泄漏问题事小,甚至还会产生程序逻辑上的问题。所以,为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。



原文地址:https://www.cnblogs.com/fnlingnzb-learner/p/10479184.html