ThreadLocal学习使用和源码深度分析

一. TreadLocal是什么?

1.TreadLocal含义

JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal是为解决多线程并发问题所提供的一个工具类。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的变量副本,而不会影响其它线程所对应的变量副本。从而保证了线程间的数据隔离,实现了线程安全。

2.ThreadLocal和synchronized的区别
1、使用场景区别:

ThreadLocal和其他所有的同步机制都是为了解决多线程中的对同一变量的访问冲突,但两者面向的问题领域不同:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离.
Synchronized主要解决多线程共享数据同步避免出现脏数据问题。
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。
很多情况下,ThreadLocal比直接使用Synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

2、原理区别:

Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问,使得多个线程间通信时能够合理的共享数据。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的是自己的变量副本,这样就隔离了多个线程对数据的共享。
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

二. ThreadLocal的API

1.ThreadLocal定义了四个方法:
get():返回当前此线程共享变量副本中的值
set(T value):设置当前此线程中共享变量副本中的值
initialValue():返回当前此线程共享变量副本中的初始值
remove():移除当前此线程共享变量副本中的值
2.使用实例

假设每个线程都需要一个计数值记录自己做某件事做了多少次,各线程运行时都需要改变自己的计数值而且相互不影响,那么ThreadLocal就是很好的选择,这里ThreadLocal里保存的当前线程的局部变量的副本就是这个计数值。

public class SeqCount {

    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public int nextSeq() {
        seqCount.set(seqCount.get() +1);
        return seqCount.get();
    }

    public static void main(String [] args) {
        SeqCount seqCount = new SeqCount();

        SeqThread seqThread1 = new SeqThread(seqCount);
        SeqThread seqThread2 = new SeqThread(seqCount);
        SeqThread seqThread3 = new SeqThread(seqCount);
        SeqThread seqThread4 = new SeqThread(seqCount);

        seqThread1.start();
        seqThread2.start();
        seqThread3.start();
        seqThread4.start();
    }

    public static class SeqThread extends Thread {

        private SeqCount seqCount;

        public SeqThread(SeqCount seqCount) {
            this.seqCount = seqCount;
        }

        @Override
        public void run() {
            for (int i=0; i<3; i++) {
                System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
            }
        }
    }
 }

运行结果:
在这里插入图片描述

3.ThreadLocal的set()和initialValue()区别

initialValue()是在创建ThreadLocal对象初始化时,就往ThreadLocal对象的ThreadLocalMap里面存入初始数据。默认为null,一般建议重写该方法,重新赋值。
set()是通过创建好的ThreadLocal对象存入共享变量。
remove()可以清除set()存入的值,但是清除不了initialValue()存入的值。

public class ThreadLocaTest {

    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
    };
    private static ThreadLocal<Integer> seqCount2 = new ThreadLocal<Integer>();
    
    public static void main(String[] args) {
        seqCount.remove();
    	System.out.println(seqCount.get());
    	System.out.println(seqCount2.get());
    	seqCount.set(1);
    	seqCount2.set(2);
		System.out.println(seqCount.get());
		System.out.println(seqCount2.get());
		seqCount.remove();
		seqCount2.remove();
		System.out.println(seqCount.get());
		System.out.println(seqCount2.get());
	}

 }

运行结果为:

0
null
1
2
0
null

三. ThreadLocal使用案例

1.spring底层很多地方用到ThreadLocal

在这里插入图片描述
举例:spring声明式事物底层用到Threadlocal来存储connection对象,来保证一个线程内对数据库的多个操作都是来自同一个连接,从而保证一个事物的完整性。

2.数据库连接
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
} 
3.Session管理

1、HttpSession如果存放了大量的数据,会影响系统性能:
2、我们在controller层使用HttpSession比较方便,当我们想在service层或者dao层使用时,就比较麻烦了,需要从controller传值。

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

四. 解决SimpleDateFormat的线程安全

我们知道SimpleDateFormat在多线程下是存在线程安全问题的,那么将SimpleDateFormat作为每个线程的局部变量的副本就是每个线程都拥有自己的SimpleDateFormat,就不存在线程安全问题了。

public class SimpleDateFormatDemo {

    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<>();

    /**
     * 获取线程的变量副本,如果不覆盖initialValue方法,第一次get将返回null,故需要创建一个DateFormat,放入threadLocal中
     * @return
     */
    public DateFormat getDateFormat() {
        DateFormat df = threadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(DATE_FORMAT);
            threadLocal.set(df);
        }
        return df;
    }

    public static void main(String [] args) {
        SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();

        MyRunnable myRunnable1 = new MyRunnable(formatDemo);
        MyRunnable myRunnable2 = new MyRunnable(formatDemo);
        MyRunnable myRunnable3 = new MyRunnable(formatDemo);

        Thread thread1= new Thread(myRunnable1);
        Thread thread2= new Thread(myRunnable2);
        Thread thread3= new Thread(myRunnable3);
        thread1.start();
        thread2.start();
        thread3.start();
    }

    public static class MyRunnable implements Runnable {

        private SimpleDateFormatDemo dateFormatDemo;

        public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {
            this.dateFormatDemo = dateFormatDemo;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" 当前时间:"+dateFormatDemo.getDateFormat().format(new Date()));
        }
    }
}

运行结果:
在这里插入图片描述

五.ThreadLocal原理分析

案例

因为connection和session只能为每一个线程独立共享,普通的将他们作为成员变量或者静态变量将是线程不安全的。通过ThreadLocal这个类,将这些共享变量保存每个线程内部的ThreadLocalMap数据区里面,从而达到线程间的数据隔离,实现线程安全。

//假如Connection、Session需要实现线程安全。
public class Demo01{
    private static ThreadLocal<Connection> connThreadLocal= new ThreadLocal<Connection>()
    private static ThreadLocal<Session> sessionThreadLocal= new ThreadLocal<Session>()
    connThreadLocal.set(connextion对象);
    sessionThreadLocal.set(session对象);
}

注意:每个ThreadLocal对象只能保存一个共享变量。所以connection、session对象分别采用两个ThreadLocal对象存储绑定。

//假如Demo02需要获取这些共享变量
public class Demo02{
    Connection conn = Demo01.connThreadLocal.get();
    Session session = Demo01.sessionThreadLocal.get();
}
源码分析

在这里插入图片描述
1.第一次向ThreadLocal存值所涉及源码。

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    //ThreadLocal内部维护的一个内部类
     static class ThreadLocalMap {
          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);
        }
         
     }
    ...
}
public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //暂不做讨论
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
}

第一次 connThreadLocal.set(connextion对象)时,会获取当前线程thread对象,然后通过getMap(thread对象)方法去当前线程中类型为ThreadLocalMap的threadLocals属性值,获取到的值为null。然后判断threadLocals如果为null的话,就通过createMap(Thread t, T firstValue),将this(当前connThreadLocal对象)和connextion对象共享变量值作为参数来新建一个ThreadLocalMap,并将其赋值给当前线程thread对象,那么在第一次set()结束后,当前线程thread对象的threadLocals就有一个新建的ThreadLocalMap了。

补充:
1、new ThreadLocalMap(this, firstValue)时,是调用的ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)这个构造器,可以看出该构造器将connThreadLocal这个ThreadLocal对象和connextion对象共享变量分别作为key、value存入到了ThreadLocalMap里面的新建的一个Entry类型的table数组里面。
2、第一次set()时做了哪些事情?
新建了一个ThreadLocalMap(同时在ThreadLocalMap里面新建了一个Entry数组),并将这个ThreadLocalMap赋值给当前线程对象的threadLocals。

2.第二次向ThreadLocal存值所涉及源码。

public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    //ThreadLocal内部维护的一个静态内部类
    static class ThreadLocalMap {
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //根据ThreadLocal的散列值,查找对应元素在数组中的位置
            int i = key.threadLocalHashCode & (len-1);
            //采用线性探测法寻找合适位置
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //key存在,直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //ThreadLocal对应的key实例不存在,new一个
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清楚陈旧的Entry(key == null的)
            // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
         
     }
    ...
}

第二次sessionThreadLocal.set(session对象)时,从新进入到set()方法,判断当前线程thread对
象里面的threadLocals不为null,值为第一次set时新建的ThreadLocalMap。然后调用map.set(this, value)进入到ThreadLocal内部类ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法,将sessionThreadLocal对象和session对象共享变量分别作为key、value存入到Entry类型的table数组中。因为ThreadLocalMap是ThreadLocal里面的静态内部类,相当于ThreadLocalMap是被所有ThreadLocal对象所共享的,那么第二次set共享变量,就是往同一个table数组里面赋值。第二次set结束后,当前线程thread对象的threadLocals的table数组里面已经有两组键值对了。

补充:
1、第二次set()时做了哪些事情?
获取到当前线程对象的threadLocals属性,并往threadLocals的table数组里面存值。

2、向Entry数组里面存储数据原理。
这个set操作和集合Map解决散列冲突的方法不同,集合Map采用的是链地址法,这里采用的是开放定址法(线性探测)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key ==null的实例,防止内存泄漏。

3.通过threadLocal对象get()取值所涉及的源码。

public class ThreadLocal<T> {
    //需要子类重写,不重写默认值为null
    protected T initialValue() {
        return null;
    }
    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();
    }

    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;
    }  
     static class ThreadLocalMap {
          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);
        }
        ...
     }  
     ...
}

获取当前线程thread对象,获取线程对象里面的threadLocals属性值,如果threadLocals不为null,就通过当前threadLocal对象去threadLocals里面的Entry数组里面,去获取对应的value值。如果threadLocals为null,就返回initialValue()的值,并新创建一个ThreadLocalMap对象复制给当前线程对象的threadLocals属性,并将initialValue()里面的值存储在ThreadLocalMap中。

补充:
1、调用get()方法时做了哪些事情?
获取当前线程对象threadLocals是否为null,如果不为null就根据threadLocal对象去里面取值。如果为null,则返回initialValue()初始化值,并新建一个ThreadLocalMap赋值给当前线程对象的threadLocals属性。

4.通过threadLocal对象.remove()所涉及的源码。

public class ThreadLocal<T> {
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
     static class ThreadLocalMap {
          private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
        ...
     }  
     ...
}

threadLocal对象.remove(),根据当前threadLocal对象去当前线程对象的ThreadLocalMap的Entry数组里面删除对应的共享变量副本。

六 . 数据结构

每个线程对象进来ThreadLocal这个类似于工具的类都会为这个线程创建一个新的ThreadLocalMap 对象,主类里面的所有threadLocal对象和共享变量值分别作为key-value存入ThreadLocalMap 中Entry类型数组中,并将这个ThreadLocalMap 赋值给当前线层对象。从而让各个线程都有独立的存储共享变量的数据区域ThreadLocalMap ,实现了线程间的数据隔离,实现了线程安全。

1.ThreadLocalMap

public class ThreadLocal<T> {
   static class ThreadLocalMap {
    ...
   } 
}

2.Entry

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private Entry[] table;
    ...
}

Entry继承WeakReference,所以Entry对应key的引用(ThreadLocal实例)是一个弱引用。

补充:
弱引用:
描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论
当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。

3.ThreadLocal弱引用引发的内存泄漏
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,ThreadLocalMap的key为ThreadLocal实例,他是一个弱引用,我们知道弱引用有利于GC的回收,当key == null时,GC就会回收这部分空间,但value不一定能被回收,因为他和Current Thread之间还存在一个强引用的关系。由于这个强引用的关系,会导致value无法回收,如果线程对象不消除这个强引用的关系,就可能会出现OOM。有时候,我们调用ThreadLocalMap的remove()方法进行显式处理。

解决方法:
1、使用完线程共享变量后,显示调用threadLocal对象.remove方法清除线程共享变量;

参考:

枫之逆的彻底理解ThreadLocal

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值