文章目录
- 1. ThreadLocal使用场景和理解
- 1.1. 数据库连接管理
- ThreadLocal登场
- 1.2. ThreadLocal造成内存泄露的问题
- 1.3. Session的管理
- 1.4. java 开发手册中推荐的 ThreadLocal
- 1.5. 每个线程维护了一个“序列号”
- 2. ThreadLocal原理
- 2.1. ThreadLocal如何实现的线程隔离
ThreadLocal会为每个线程创建单独的变量副本,避免因多线程操作共享变量导致数据不一致的现象。
1. ThreadLocal使用场景和理解
1.1. 数据库连接管理
看一个简单的数据库处理类,逻辑很简单一个是打开连接,一个是关闭连接。
classConnectionManager{privatestaticConnection connect=null;publicstaticConnectionopenConnection(){if(connect==null){connect=DriverManager.getConnection();}returnconnect;}publicstaticvoidcloseConnection(){if(connect!=null)connect.close();}}我们回顾一下多线程下操作共享变量安全性的保证:
互斥同步:synchroniezd 和 reentrantLock
非阻塞同步:CAS、AtomicXXX
无同步方案:栈封闭、本地存储(Thread Local)。
对于上面的代码,因为有共享变量,多线程下可能会有安全问题。
安全问题:一个线程openConnection的同时,另一个变量在closeConnection;
同步问题:不互斥,有多个线程同时进入openConnection方法,多次创建了connect。
思考一个问题:需不需要互斥同步。
假设每个线程都有自己的connect变量,各个线程线程之间拿到connect之后,相互之间是没有依赖的。即一个线程修改了这个connect,不会影响其他线程对这个connect的使用。
因为每个线程使用的是对应CPU下的缓存,而不是内存中的数据。
但这会导致服务器压力过大,因为会在方法中频繁的创建数据连接。
ThreadLocal登场
ThreadLocal会在每个线程中对该变量创建一个副本,即每个线程都会有这样一个副本,互不影响。这样就不会造成线程安全和性能问题。
常用的例子
importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.SQLException;publicclassConnectionManager{privatestaticfinalThreadLocal<Connection>dbConnectionLocal=newThreadLocal<Connection>(){@OverrideprotectedConnectioninitialValue(){try{returnDriverManager.getConnection("","","");}catch(SQLException e){e.printStackTrace();}returnnull;}};publicConnectiongetConnection(){returndbConnectionLocal.get();}}注意下ThreadLocal的修饰符
如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。
同时,threadLocal副本会造成内存的占用。
1.2. ThreadLocal造成内存泄露的问题
看下列代码:使用线程池来操作ThreadLocal。
importjava.util.concurrent.LinkedBlockingQueue;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;publicclassThreadLocalDemo{staticclassLocalVariable{privateLong[]a=newLong[1024*1024];}// (1)finalstaticThreadPoolExecutor poolExecutor=newThreadPoolExecutor(5,5,1,TimeUnit.MINUTES,newLinkedBlockingQueue<>());// (2)finalstaticThreadLocal<LocalVariable>localVariable=newThreadLocal<LocalVariable>();publicstaticvoidmain(String[]args)throws InterruptedException{// (3)Thread.sleep(5000*4);for(inti=0;i<50;++i){poolExecutor.execute(newRunnable(){publicvoidrun(){// (4)localVariable.set(newLocalVariable());// (5)System.out.println("use local varaible"+localVariable.get());localVariable.remove();}});}// (6)System.out.println("pool execute over");}}如果使用线程池操作ThreadLocal会造成内存泄漏。
内存泄漏:指一个对象不使用了,但是一直占据着内存,具体的:对象不使用了,但是还被其他对象引用着,就会导致GC时不能回收此对象(根的可达性分析),这种情况就是内存泄漏。
因为对于线程池中没有销毁的线程,里面总是存在着<ThreadLocal, LocalVariable> 的强引用,而对于ThreadLocalMap 对于key虽然是弱引用,但是强引用不释放,弱引用就会一直有值,同时创建的Local Variable对象也不会释放,就造成了内存泄漏。
具体地,泄漏的内存 = coreThreadNum * LocalVariable对象的大小
ThreadLocal提供了一个清除线程中对象的方法, 即 remove。来避免内存泄漏的情况。
privatevoidremove(ThreadLocal<?>key){Entry[]tab=table;intlen=tab.length;inti=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的管理
privatestaticfinalThreadLocal threadSession=newThreadLocal();publicstaticSessiongetSession()throws InfrastructureException{Session s=(Session)threadSession.get();try{if(s==null){s=getSessionFactory().openSession();threadSession.set(s);}}catch(HibernateException ex){thrownewInfrastructureException(ex);}returns;}1.4. java 开发手册中推荐的 ThreadLocal
importjava.text.DateFormat;importjava.text.SimpleDateFormat;publicclassDateUtils{publicstaticfinalThreadLocal<DateFormat>df=newThreadLocal<DateFormat>(){@OverrideprotectedDateFormatinitialValue(){returnnewSimpleDateFormat("yyyy-MM-dd");}};}调用
DateUtils.df.get().format(newDate());1.5. 每个线程维护了一个“序列号”
当我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来时,使用ThreadLocal。
为每一个线程维护一个序列号。
publicclassSerialNum{// The next serial number to be assignedprivatestaticintnextSerialNum=0;privatestaticThreadLocal serialNum=newThreadLocal(){protectedsynchronized ObjectinitialValue(){returnnewInteger(nextSerialNum++);}};publicstaticintget(){return((Integer)(serialNum.get())).intValue();}}2. ThreadLocal原理
2.1. ThreadLocal如何实现的线程隔离
看一下set和get
先获取当前线程的map,然后set值,进行更新。如果这个线程没有map就先创建这个线程的map,并set值。
publicvoidset(T value){Thread t=Thread.currentThread();ThreadLocalMap map=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}voidcreateMap(Thread t,T firstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}获取当前线程的Map,如果不为空,则获取这个线程对应的值,否则初始化。
publicTget(){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;returnresult;}}returnsetInitialValue();}privateTsetInitialValue(){T value=initialValue();Thread t=Thread.currentThread();ThreadLocalMap map=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);returnvalue;}***分析了get和set方法,对于ThreadLocal的线程隔离的实现其实很简单,就是为每个线程创建一个map,其中key存储着线程本身,value就是操作的共享变量。 ***
每个线程在get时,如果此线程没有set,则获取的是null。
参考:
https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html