ThreadLocal学习

ThreadLocal

记录一套比较有参考价值的:
并发容器之ThreadLocal
一篇文章,从源码深入详解ThreadLocal内存泄漏问题

设计目的

官方的解释如下:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

大致翻译

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

官方示例:

import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal threadId =
         new ThreadLocal() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Returns the current thread's unique ID, assigning it if necessary
     public static int get() {
         return threadId.get();
     }
 }

从官方的说明和例子中可以看出,这个ThreadLocal是为了给当前线程创建私有化变量副本的。他创建堆区中的对象(可以被所有线程共享)的副本到Thread Local中给当前线程使用。也就是只要请求是一个新的线程,那么他在这个请求的任何地方拿目标对象都是该线程私有的,因为事实上只是以某个对象为原型new了一个出来。总之他的意义在于线程隔离

和Thread、ThreadLocalMap关系

ThreadLocal 和 ThreadLocalMap

**ThreadLocalMapThreadLocal的静态内部类,其内部还有一个继承了WeakReference
Entry的内部类
下面是官方源码说明:**

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.

ThreadLocal学习_第1张图片

一是这个Entry的实现是继承了WeakReference(弱引用)的,再一个他的key就是ThreadLocal本身

ThreadLocalMap 和 Thread

ThreadLocalMap是Thread的属性,正因如此,ThreadLocal才得以实现。

    /* 
     * 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /* 
     * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap, 
     * 主要用于父子线程间ThreadLocal变量的传递 
    */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

源码分析

**源码中主要的方法有
set、get、initialValue、withInitial、setInitialValue、remove,下面一个一个分析**

initialValue()

该方法由protect修饰,显然是为了重写
**该段说明大意:该方法返回当前线程的ThreadLocal变量的“初始值”。除非在此之前已经调用过set()方法设置值否则这个方法会在第一次调用get()获取变量时调用。正常情况下这个方法只会在第一次调用get()后调用一次,除非在get()后使用过remove()方法
这个实现返回了null,如果有需要初始值不是null的话,就需要重写该方法,通常使用匿名内部类的方式。**

   /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * 

This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; }

get()

该方法用于获取ThreadLocal中的对象,首先拿到当前线程线程对象,通过当前线程对象获取对应的ThreadLocalMap。判断map是否为空,然后根据当前的ThreadLocal对象查找对应对象,如果不为空就返回,否则调用setInitialValue()

 /**
     * 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();
    }

setInitialValue()

当get()时没有值的话,则会调用该方法,该方法会调用initialValue()设置初始值。并且从这里可以看出,其实在存储的时候不同的是Map,而不是map中的key。在取值的时候是使用象同的key在各个线程中拿到不同的map从而拿到不同的value。


 /**
     * 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;
    }

### set()
该方法用于给当前线程的LocalThread变量设置值,大多数子类不需要使用这个方法,只依赖于initialValue()方法来设置线程局部变量的值。

    /**
     * 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);
    }

### remove()
移除变量,如果在调用这个方法后再次调用get(),那么会再次执行initialValue()。

   /**
    * 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);
    }

withInitial()

该方法java8引入,为了可以使用lambda来设置初始值,如:

** private static ThreadLocal threadLocal =
ThreadLocal.withInitial(() -> new Cpc0010Manager());**
    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param  the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static  ThreadLocal withInitial(Supplier supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

可能存在的内存泄露

从下图可以看到,ThreadLocalMap的key对ThreadLocal使用了弱引用,当ThreadLocal成为null以后Map中的key会被回收,但是该Map中的Entry因为有强引用则不会被回收,则造成了key为null的Entry的存在,从而造成内存泄露。因此源码中为了避免这种情况在set()、get()、remove()时都会清除key为null的Entry。

**使用时应避免ThreadLocal和线程池一起使用,不仅容易OOM而且会造成脏读。
更多可参见:**

ThreadLocal内存泄漏真因探究

ThreadLocal学习_第2张图片

get()路线

这里是一个正常的get()方法,当ThreadLocalMap不为空的时候则调用getEntry(this)

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();
    }
    

getEntry()方法如下,这是一个快速命中的策略,直接命中现有key,如果没有命中就会调用getEntryAfterMiss(),这样设计的目的是为了最大限度的提高性能。

    
    /**
         * 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);
        }
        

getEntryAfterMiss()是当目标key的hash值没有直接命中而进行后续处理的方法。当key没有命中时会以当前下标开始遍历Entry,并使用expungeStaleEntry()把遇到的key为null的给清除。

        /**
         * 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;

            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;
        }
        

该方法就是将当前key为null的Entry和value也设为null,然后从当前index向后遍历,将key为null的Entry和value也设为null,以便GC。方法返回下一个Entery为null的下标。

         /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            //向后遍历,直到Entry为null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                //如果key为null,就设置该Entery和value为null
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                //处理hash重复情况
                    int h = k.threadLocalHashCode & (len - 1);
                    //如果不是,则设置当前index的Entry为null
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

set()路线

/**
     * 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);
    }

调用map.set(),遍历Map,如果key已经存在则替换,在遍历的同时发现key为null则调用replaceStaleEntry()。否则就新建然后存入,存入时调用cleanSomeSlots(),去除旧Entry。

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry()方法用于替换陈旧的Entery。

 /**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         */
        private void replaceStaleEntry(ThreadLocal key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

cleanSomeSlots()方法,主要用于控制清除“脏Entry”的执行,他会现在hash表范围内搜索是否有“脏Entry”,如果有的话就调用expungeStaleEntry()在全表范围内进一步扫描清理。这个方法的设计是为了提高效率。

/**
         * Heuristically scan some cells looking for stale entries.
         * This is invoked when either a new element is added, or
         * another stale one has been expunged. It performs a
         * logarithmic number of scans, as a balance between no
         * scanning (fast but retains garbage) and a number of scans
         * proportional to number of elements, that would find all
         * garbage but would cause some insertions to take O(n) time.
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         *
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         *
         * @return true if any stale entries have been removed.
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

测试Demo

共享类

待共享的bean为Cpc0010Manager,则在该类中添加私有静态变量ThreadLocal,并重写initialValue()方法设置初始值,添加通用的get(),set()方法。

public class Cpc0010Manager {
    int id;
    String name;

    private static ThreadLocal threadLocal = new ThreadLocal(){
        @Override
        protected Cpc0010Manager initialValue(){
            Cpc0010Manager cpc0010Manager=new Cpc0010Manager();
            cpc0010Manager.id=0;
            cpc0010Manager.name="name";
            return cpc0010Manager;
        }
    };

    public int getId() {
        return id;
    }

    public Cpc0010Manager setId(int id) {
        this.id = id;
        return this;
    }

    public String getName() {
        return name;
    }

    public Cpc0010Manager setName(String name) {
        this.name = name;
        return this;
    }

    public  Cpc0010Manager getThreadLocal() {
        return threadLocal.get();
    }

    public  void setThreadLocal(Cpc0010Manager cpc0010Manager) {
        threadLocal.set(cpc0010Manager);
    }
}

线程类

添加构造方法,接受Cpc0010Manager的实例,并在run方法中调用。打印的时候分别打印传入Cpc0010Manager的属性和ThreadLocal中的属性。

 public class Cpc0010Thread implements Runnable {
    Cpc0010Manager cpc0010Manager;

    public Cpc0010Thread(Cpc0010Manager manager) {
        cpc0010Manager = manager;
    }

    @Override
    public void run() {
         System.out.println(Thread.currentThread().toString() + ":" + cpc0010Manager.getThreadLocal() + "---" + cpc0010Manager.getId() + cpc0010Manager.getThreadLocal().getName());
    }
}

测试类

测试时分别改变id和name观察变化。sleep是为了保证程序的先后顺序。

public static void main(String[] args) throws InterruptedException {
       Cpc0010Manager cpc0010Manager=new Cpc0010Manager();
        Cpc0010Thread t1=new Cpc0010Thread(cpc0010Manager);
        Cpc0010Thread t2=new Cpc0010Thread(cpc0010Manager);
        Cpc0010Thread t3=new Cpc0010Thread(cpc0010Manager);
        cpc0010Manager.getThreadLocal();
        new Thread(t1).start();
        Thread.sleep(1000L);
        cpc0010Manager.setId(2);
        new Thread(t2).start();
        cpc0010Manager.getThreadLocal().setName("newName");
        Thread.sleep(1000L);
        new Thread(t3).start();
    }

结果及小结

控制台输出结果如下:

TThread[Thread-1,5,main]:com.crrcdt.res.config.Cpc0010Manager@1c5bee7d---0localName
Thread[Thread-2,5,main]:com.crrcdt.res.config.Cpc0010Manager@3990d676---2localName
Thread[Thread-3,5,main]:com.crrcdt.res.config.Cpc0010Manager@236e9199---2localName

小结:传入的Cpc0010Manager对象是同一个实例,而从ThreadLocal中获取的都是本地私有的副本,他们的copy的对象的属性是从initialValue()方法而来,除了初始设置的值相同以外,并无其他关联。感悟就是ThreadLocal唯一解决的就是在各个线程中不用自己new的问题。

实际应用

hibernate中的session管理

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
 
/**
 * 升级的MySessionFactory 线程局部模式
 * @author xuliugen
 */
public class HibernateUtil {
    private static SessionFactory sessionFactory = null;
    // 使用线程局部模式
    private static ThreadLocal threadLocal = new ThreadLocal();
 
    /*
     * 默认的构造函数
     */
    private HibernateUtil() {
 
    }
 
    /*
     * 静态的代码块
     */
    static {
        sessionFactory = new Configuration().configure().buildSessionFactory();
    }
 
    /*
     * 获取全新的的session
     */
    public static Session openSession() {
        return sessionFactory.openSession();
    }
 
    /*
     * 获取和线程关联的session
     */
    public static Session getCurrentSession() {
        Session session = threadLocal.get();
        // 判断是是是否得到
        if (session == null) {
            session = sessionFactory.openSession();
            // 把session放到 threadLocal,相当于该session已经于线程绑定
            threadLocal.set(session);
        }
        return session;
    }
}

总结

  1. ThreadLocal为每一个线程提供一个实例,防止互相影响
  2. ThreadLocal是一个操作类,真正起作用的原因在于Thread中引用了ThreadLocalMap
  3. ThreadLocal的initialValue()和withInitial()用于设定初始value
  4. ThreadLocal在调用remov()、get()、set()时会自动清除key为null的entry
  5. 由于ThreadLocalMap的Entry的key是弱引用,所以在下次gc时,会回收没有被引用的ThreadLocal,而value不会被回收,所以key为null的Entry不会被回收。
  6. ThreadLocal与线程时一起使用时,要避免内存泄漏和脏读问题
  7. ThreadLocalMap是ThreadLocal的静态内部类,并使用Entry自行实现了map功能
  8. ThreadLocalMap使用开放地址法解决hash冲突,因为经常修改
  9. ThreadLocalMap的初始大小为16,加载因子为2/3,hash表可用大小为16*2/3=10
  10. 清除“脏Entry”主要通过expungeStaleEntry()、replaceStaleEntry()、cleanSomeSlots()

你可能感兴趣的:(java,多线程)