1、ThreadLocal是什么?
- 提供线程内局部变量,不同线程之间不会相互干扰。
- ThreadLocal 实例通常来说都是
private static 修饰的,用于关联线程和线程的上下文。
减少同一个线程内的函数 或 组件之间传递变量的复杂性。
小结:
1 2 3
| 1. 线程并发:在多线程并发的场景 2. 传递数据:通过ThreadLocal在同一线程不同组件中传递公共变量。 3. 线程隔离:每个线程的变量都是独立的,不会互相影响
|
1.1、举例-线程隔离
不使用ThreadLocal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Demo {
private String content; private void setContent(String content) { this.content= content; } private String getContent() { return content; } public static void main(String[] args) { Demo demo = new Demo(); for (int i = 0; i < 5; i++) { new Thread(new Runnable() { @Override public void run() { demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("------------"); System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent()); } }, "线程" + i).start(); } } }
|

使用ThreadLocal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Demo { ThreadLocal<String> threadLocal = new ThreadLocal<>(); private String content; private void setContent(String content) { threadLocal.set(content); } private String getContent() { return threadLocal.get(); } public static void main(String[] args) { Demo demo = new Demo(); for (int i = 0; i < 5; i++) { new Thread(new Runnable() { @Override public void run() { demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("------------"); System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent()); } }, "线程" + i).start(); } } }
|

1.2、对比synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class synchronizedDemo { private String content; private void setContent(String content) { this.content= content; } private String getContent() { return content; } public static void main(String[] args) { synchronizedDemo demo = new synchronizedDemo(); for (int i = 0; i < 5; i++) { new Thread(new Runnable() { @Override public void run() { synchronized (synchronizedDemo.class) { demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("------------"); System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent()); } } }, "线程" + i).start(); } } }
|

虽然 ThreadLocal 和 Synchronized 关键字都是用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
|
Synchronized |
ThreadLocal |
| 原理 |
同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问。 |
采用 “以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而不互相干扰。 |
| 侧重点 |
多个线程之间访问资源的同步。 |
并发情况下让每个线程之间数据相互隔离。 |
1.3、ThreadLocal的好处
- 传递数据:保证每个线程保定的数据在需要的地方可以直接使用,这样避免了进行参数传递而带来的代码耦合问题。
- 线程隔离:各个线程之间的数据相互隔离但有具备并发性,同事避免了使用synchronized加锁带来的性能损耗问题。
2、案例

那么可以看到在service到Dao层的时候,都会使用connection,那么此时将connection对象和当前线程进行绑定,这样就能保证数据的一致性,并且避免传参导致的代码耦合问题。
2.1、Service层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao; import com.itheima.transfer.utils.JdbcUtils; import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao();
try { Connection conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); ad.out(outUser, money);
ad.in(inUser, money); JdbcUtils.commitAndClose(); } catch (Exception e) { e.printStackTrace(); JdbcUtils.rollbackAndClose(); return false; } return true; } }
|
2.2、Dao层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException;
public class AccountDao {
public void out(String outUser, int money) throws SQLException { String sql = "update account set money = money - ? where name = ?"; Connection conn = JdbcUtils.getConnection(); PreparedStatement pstm = conn.prepareStatement(sql); pstm.setInt(1,money); pstm.setString(2,outUser); pstm.executeUpdate();
JdbcUtils.release(pstm); }
public void in(String inUser, int money) throws SQLException { String sql = "update account set money = money + ? where name = ?"; Connection conn = JdbcUtils.getConnection(); PreparedStatement pstm = conn.prepareStatement(sql); pstm.setInt(1,money); pstm.setString(2,inUser); pstm.executeUpdate();
JdbcUtils.release(pstm); } }
|
2.3、Utils方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.itheima.transfer.utils;
import com.mchange.v2.c3p0.ComboPooledDataSource; import java.sql.Connection; import java.sql.SQLException;
public class JdbcUtils { private static final ThreadLocal<Connection> tl = new ThreadLocal();
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
public static Connection getConnection() throws SQLException { Connection conn = tl.get(); if (conn == null) { conn = ds.getConnection(); tl.set(conn); } return conn; }
public static void release(AutoCloseable... ios) { for (AutoCloseable io : ios) { if (io != null) { try { io.close(); } catch (Exception e) { e.printStackTrace(); } } } }
public static void commitAndClose() { try { Connection conn = getConnection(); conn.commit(); tl.remove(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } }
public static void rollbackAndClose() { try { Connection conn = getConnection(); conn.rollback(); tl.remove(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
|
可以看到,在Utils方法中,getConnection的时候,此时使用了一个ThreadLocal对象,将当前Connection对象和当前线程进行绑定了;如果是第一次获取connection对象,那么就从连接池中获取,不是的话,那么直接从ThreadLocal中获取。
3、内部结构探索
3.1、内部结构
在JDK8中ThreadLocal的设计:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal对象本身,而value就是真正需要存储的值。
具体:
(1) 每个Thread线程内部都有一个Map (ThreadLocalMap)
(2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
(3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

由于每个Thread中维护一个ThreadLocalMap,Map的key为ThreadLocal对象本身,value为设置的值,这样的优势:
- 每个
Map存储的Entry数量就会变少,JDK7中的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。(ThreadLocal的数量远远小于Thread数量)
- 当
Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
3.2、核心方法
3.2.1、set
1 2 3 4 5 6 7 8 9 10 11 12 13
| public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
|
1 2 3 4 5 6 7 8 9 10
| ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; }
|
真正进行赋值:
set方法可以进行修改或者新建的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| private void set(ThreadLocal<?> key, Object value) { 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(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } 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); }
|
3.2.2、get
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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(); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| 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); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; }
|
3.2.3、remove
1 2 3 4 5 6 7 8
| public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
|
4、ThreadLocal内存泄漏
4.1、内存泄漏是什么?
不再会使用的对象或者变量占用的内存不能被回收,就是内存泄漏。
4.2、四种引用
4.2.1、强引用
一般我们 new 关键字创建的对象就是 Reference(强引用),当内存不足时,JVM 开始垃圾回收,对于强引用对象,就算是出现 OOM 也不会对该对象进行回收。
4.2.2、软引用
软引用是一种相对相对于强引用弱化了一些的引用,需要用 SoftReference 类实现,对于软引用来说,当系统内存充足时,软引用对象不会被垃圾回收,不充足时,会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
4.2.3、弱引用
弱引用需要用 WeakReference 类实现,它比软引用的生存期更短,对于弱引用对象来说,只要垃圾回收器运行,不管 JVM 内存空间是否足够,都会回收该对象占用的内存。
4.2.4、虚引用
虚引用需要 PhantomReference 类来实现,如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能会垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
4.3、Entry
1 2 3 4 5 6 7 8 9 10 11 12
| static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> { Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
|
从上述代码中可以得知,
- ThreadLocalMap是ThreadLocal的一个内部静态类,用来存储每个线程对应的变量值。Entry类来管理每个线程本地变量的key-value。
Entry是ThreadLocalMap的一个内部静态类,继承自WeakReference<ThreadLocal<?>>。Entry表示一个键值对,用于将ThreadLocal对象与其对应的变量值关联起来。
- 表明:
ThreadLocal对象在没有其他强引用对象的时候会被垃圾回收器进行回收,而Entry的声明周期也会随着结束,进而避免了内存泄漏。
4.3.1、为什么是弱引用

代码演示:
1 2 3 4 5
| public void method() { ThreadLocal<Integer> tl = new ThreadLocal<>(); tl .set(2021); tl .get(); }
|
- 当调用method的时候,会向栈中插入一条栈帧。
- new关键字创建一个ThreadLocal对象,此时tl是对象的引用
- new出的对象是一个强引用,通过set方法进行存储值,Key是ThreadLocal对象本身,Value为需要存储的值。
- Entry继承WeakReference,那么Key是弱引用指向了ThreadLocal对象。
- 当method方法执行完毕之后,栈帧销毁,此时强引用tl就不存在了。
- 但是Thread的ThreadLocalMap中的某一个Entry的key的引用还指向了ThreadLocal对象
- 如果这个Key引用是强引用,会导致Key指向的ThreadLocal对象是强引用对象不能被GC,会造成内存泄漏
- 如果这个Key引用是弱引用,会大概率减少内存泄漏的问题。使用了弱引用,就可以使ThreadLocal对象在方法执行完毕之后顺利被回收,并且Key的引用会被指向为null。

总结:
- new一个ThreadLocal对象的时候,就会有一个强引用指向这个对象。
- 调用set方法之后,线程中的ThreadLocalMap中的Entry对象中的Key指向ThreadLocal对象。
- 如果Key是强引用的话,当方法执行完,栈帧中的强引用销毁了,对象还不能被回收,此时就会造成内存泄漏。
4.3.2、为什么还是会泄漏
虽然Entry继承了弱引用,保证了Key指向的ThreadLocal对象能被及时回收,但是此时v指向的Value对象需要再ThreadLocalMap调用get、set的时候发现Key为null的时候才能回收整个的entry、Value。
为什么value还持有引用?
解答:ThreadLocal作为Thread的一个属性,如果当前线程没有手动销毁,那么ThreadLocalMap也还是存在,同理Entry的引用也持有。
所以泄露的根本原因就是因为ThreadLocal的生命周期和Thread的生命周期一样,如果线程没有主动销毁,那么entry就不会被销毁。

所以:弱引用只是帮助我们降低了内存泄漏的概率,并不能完全避免,在使用完成之后,必须手动remove这个对象。
