聊一聊Spring中的线程安全性

Spring与线程安全


Spring做为一个IOC/DI容器,帮助咱们管理了许许多多的“bean”。但其实,Spring并无保证这些对象的线程安全,须要由开发者本身编写解决线程安全问题的代码。html

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

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

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

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

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

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

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

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

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

有人可能会认为,我使用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中含有一个叫作ThreadLocalMap的内部类,该类为一个采用线性探测法实现的HashMap。它的key为ThreadLocal对象并且还使用了WeakReference,ThreadLocalMap正是用来存储变量副本的。

/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */
    static class ThreadLocalMap {
        /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ....
    }复制代码

ThreadLocal中只含有三个成员变量,这三个变量都是与ThreadLocalMap的hash策略相关的。

/** * ThreadLocals rely on per-thread linear-probe hash maps attached * to each thread (Thread.threadLocals and * inheritableThreadLocals). The ThreadLocal objects act as keys, * searched via threadLocalHashCode. This is a custom hash code * (useful only within ThreadLocalMaps) that eliminates collisions * in the common case where consecutively constructed ThreadLocals * are used by the same threads, while remaining well-behaved in * less common cases. */
    private final int threadLocalHashCode = nextHashCode();

    /** * The next hash code to be given out. Updated atomically. Starts at * zero. */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */
    private static final int HASH_INCREMENT = 0x61c88647;

    /** * Returns the next hash code. */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }复制代码

惟一的实例变量threadLocalHashCode是用来进行寻址的hashcode,它由函数nextHashCode()生成,该函数简单地经过一个增量HASH_INCREMENT来生成hashcode。至于为何这个增量为0x61c88647,主要是由于ThreadLocalMap的初始大小为16,每次扩容都会为原来的2倍,这样它的容量永远为2的n次方,该增量选为0x61c88647也是为了尽量均匀地分布,减小碰撞冲突。

/** * The initial capacity -- MUST be a power of two. */
        private static final int INITIAL_CAPACITY = 16;    

        /** * Construct a new map initially containing (firstKey, firstValue). * ThreadLocalMaps are constructed lazily, so we only create * one when we have at least one entry to put in it. */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }复制代码

要得到当前线程私有的变量副本须要调用get()函数。首先,它会调用getMap()函数去得到当前线程的ThreadLocalMap,这个函数须要接收当前线程的实例做为参数。若是获得的ThreadLocalMap为null,那么就去调用setInitialValue()函数来进行初始化,若是不为null,就经过map来得到变量副本并返回。

setInitialValue()函数会去先调用initialValue()函数来生成初始值,该函数默认返回null,咱们能够经过重写这个函数来返回咱们想要在ThreadLocal中维护的变量。以后,去调用getMap()函数得到ThreadLocalMap,若是该map已经存在,那么就用新得到value去覆盖旧值,不然就调用createMap()函数来建立新的map。

/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }复制代码

ThreadLocal的set()与remove()函数要比get()的实现还要简单,都只是经过getMap()来得到ThreadLocalMap而后对其进行操做。

/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /** * Removes the current thread's value for this thread-local * variable. If this thread-local variable is subsequently * {@linkplain #get read} by the current thread, its value will be * reinitialized by invoking its {@link #initialValue} method, * unless its value is {@linkplain #set set} by the current thread * in the interim. This may result in multiple invocations of the * {@code initialValue} method in the current thread. * * @since 1.5 */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }复制代码

getMap()函数与createMap()函数的实现也十分简单,可是经过观察这两个函数能够发现一个秘密:ThreadLocalMap是存放在Thread中的。

/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    // Thread中的源码

    /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;复制代码

仔细想一想其实就可以理解这种设计的思想。有一种广泛的方法是经过一个全局的线程安全的Map来存储各个线程的变量副本,可是这种作法已经彻底违背了ThreadLocal的本意,设计ThreadLocal的初衷就是为了不多个线程去并发访问同一个对象,尽管它是线程安全的。而在每一个Thread中存放与它关联的ThreadLocalMap是彻底符合ThreadLocal的思想的,当想要对线程局部变量进行操做时,只须要把Thread做为key来得到Thread中的ThreadLocalMap便可。这种设计相比采用一个全局Map的方法会多占用不少内存空间,但也所以不须要额外的采起锁等线程同步方法而节省了时间上的消耗。

ThreadLocal中的内存泄漏


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

/** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits, in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated with key, or null if no such */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /** * Version of getEntry method for use when key is not found in * its direct hash slot. * * @param key the thread local object * @param i the table index for key's hash code * @param e the entry at table[i] * @return the entry associated with key, or null if no such */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            // 清理key为null的Entry
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }复制代码

在上文中咱们发现了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。

参考文献


相关文章
相关标签/搜索