- 1. ThreadLocal使用场景和理解
- 1.1. 数据库连接管理
- ThreadLocal登场
- 1.2. ThreadLocal造成内存泄露的问题
- 1.3. Session的管理
- 1.4. java 开发手册中推荐的 ThreadLocal
- 1.5. 每个线程维护了一个“序列号”
- 2. ThreadLocal原理
- 2.1. ThreadLocal如何实现的线程隔离
1. ThreadLocal使用场景和理解 1.1. 数据库连接管理ThreadLocal会为每个线程创建单独的变量副本,避免因多线程操作共享变量导致数据不一致的现象。
看一个简单的数据库处理类,逻辑很简单一个是打开连接,一个是关闭连接。
class ConnectionManager { private static Connection connect = null; public static Connection openConnection() { if (connect == null) { connect = DriverManager.getConnection(); } return connect; } public static void closeConnection() { if (connect != null) connect.close(); } }
我们回顾一下多线程下操作共享变量安全性的保证:
互斥同步:synchroniezd 和 reentrantLock
非阻塞同步:CAS、AtomicXXX
无同步方案:栈封闭、本地存储(Thread Local)。
对于上面的代码,因为有共享变量,多线程下可能会有安全问题。
安全问题:一个线程openConnection的同时,另一个变量在closeConnection;
同步问题:不互斥,有多个线程同时进入openConnection方法,多次创建了connect。
思考一个问题:需不需要互斥同步。
假设每个线程都有自己的connect变量,各个线程线程之间拿到connect之后,相互之间是没有依赖的。即一个线程修改了这个connect,不会影响其他线程对这个connect的使用。
因为每个线程使用的是对应CPU下的缓存,而不是内存中的数据。
但这会导致服务器压力过大,因为会在方法中频繁的创建数据连接。
ThreadLocal会在每个线程中对该变量创建一个副本,即每个线程都会有这样一个副本,互不影响。这样就不会造成线程安全和性能问题。
常用的例子
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static final ThreadLocaldbConnectionLocal = new ThreadLocal () { @Override protected Connection initialValue() { try { return DriverManager.getConnection("", "", ""); } catch (SQLException e) { e.printStackTrace(); } return null; } }; public Connection getConnection() { return dbConnectionLocal.get(); } }
注意下ThreadLocal的修饰符
如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。
同时,threadLocal副本会造成内存的占用。
1.2. ThreadLocal造成内存泄露的问题
看下列代码:使用线程池来操作ThreadLocal。
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLocalDemo { static class LocalVariable { private Long[] a = new Long[1024 * 1024]; } // (1) final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); // (2) final static ThreadLocallocalVariable = new ThreadLocal (); public static void main(String[] args) throws InterruptedException { // (3) Thread.sleep(5000 * 4); for (int i = 0; i < 50; ++i) { poolExecutor.execute(new Runnable() { public void run() { // (4) localVariable.set(new LocalVariable()); // (5) System.out.println("use local varaible" + localVariable.get()); localVariable.remove(); } }); } // (6) System.out.println("pool execute over"); } }
如果使用线程池操作ThreadLocal会造成内存泄漏。
内存泄漏:指一个对象不使用了,但是一直占据着内存,具体的:对象不使用了,但是还被其他对象引用着,就会导致GC时不能回收此对象(根的可达性分析),这种情况就是内存泄漏。
因为对于线程池中没有销毁的线程,里面总是存在着
具体地,泄漏的内存 = coreThreadNum * LocalVariable对象的大小
ThreadLocal提供了一个清除线程中对象的方法, 即 remove。来避免内存泄漏的情况。
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; } } }
1.3. Session的管理
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; }
1.4. java 开发手册中推荐的 ThreadLocal
import java.text.DateFormat; import java.text.SimpleDateFormat; public class DateUtils { public static final ThreadLocaldf = new ThreadLocal (){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; }
调用
DateUtils.df.get().format(new Date());
1.5. 每个线程维护了一个“序列号”
当我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来时,使用ThreadLocal。
为每一个线程维护一个序列号。
public class SerialNum { // The next serial number to be assigned private static int nextSerialNum = 0; private static ThreadLocal serialNum = new ThreadLocal() { protected synchronized Object initialValue() { return new Integer(nextSerialNum++); } }; public static int get() { return ((Integer) (serialNum.get())).intValue(); } }
看一下set和get
先获取当前线程的map,然后set值,进行更新。如果这个线程没有map就先创建这个线程的map,并set值。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
获取当前线程的Map,如果不为空,则获取这个线程对应的值,否则初始化。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap threadLocals = getMap(t); if (threadLocals != null) { ThreadLocalMap.Entry e = threadLocals.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; }
***分析了get和set方法,对于ThreadLocal的线程隔离的实现其实很简单,就是为每个线程创建一个map,其中key存储着线程本身,value就是操作的共享变量。 ***
每个线程在get时,如果此线程没有set,则获取的是null。
参考:
https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html