一、ThreadLocal是什么?
官方介绍:
1. This class provides thread-local variables. These variables differ from
2. their normal counterparts in that each thread that accesses one (via its
3. {@code get} or {@code set} method) has its own, independently initialized
4. copy of the variable. {@code ThreadLocal} instances are typically private
5. static fields in classes that wish to associate state with a thread (e.g.,
6. a user ID or Transaction ID).
官方介绍翻译:
TheadLocal类提供了一个线程局部变量,这些变量与其他普通变量的不同在于,通过get、set方法访问的线程都有其独立的初始化的变量副本。ThreadLocal实例通常是private static类型的,用于关联线程或线程上下文(比如说用户ID或者交易ID)
ThreadLocal的特点
- 多线程情况:ThreadLocal是应用于多线程并发情况下
- 传递数据:多线程情况下可以通过ThreadLocal在同一线程的不同组件中传递数据
- 线程隔离:每个线程ThreadLocal所存的变量都是独立的,不会相互影响
通俗的讲就是:就是在一个线程里放一个数据,不管中间执行了什么操作,最终通过get方法即可得到保存进去的数据,适用于各个线程间依赖不通的变量值完成操作的场景。
二、ThreadLocal的应用场景
应用场景1:多线程中保证当前线程中存入的值在取出的时候还是该值。
改造前代码:
public class ThreadLocalDemo {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadLocalDemo tld = new ThreadLocalDemo();
for (int i = 0; i < 5; i++) {
Thread t = new Thread() {
@Override
public void run() {
tld.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("====================================");
String content = tld.getContent();
System.out.println(Thread.currentThread().getName() + ":" + content);
}
};
t.setName("线程" + i);
t.start();
}
}
}
运行后期望结果:各自线程获取各自所存储的数据
运营后实际结果:
可以看出多线程情况下,在执行Thread run()方法的时候,出现了线程执行抢占的情况,打印结果不匹配。
改造后代码:使用TheadLocal存储线程局部变量,使其达到线程隔离的效果,不受线程间的影响。
public class ThreadLocalDemo {
ThreadLocal<String> tl = new ThreadLocal<>();
private String content;
public String getContent() {
String s = tl.get();
return s;
// return content;
}
public void setContent(String content) {
tl.set(content);
// this.content = content;
}
public static void main(String[] args) {
ThreadLocalDemo tld = new ThreadLocalDemo();
for (int i = 0; i < 5; i++) {
Thread t = new Thread() {
@Override
public void run() {
tld.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("====================================");
String content = tld.getContent();
System.out.println(Thread.currentThread().getName() + ":" + content);
}
};
t.setName("线程" + i);
t.start();
}
}
}
运行后的结果(符合预期):
应用场景2:转账的事务问题
问题描述:银行转账问题,用户A转出的钱需入用户B的账户,即此转账操作需是原子操作。
工程目录:
1.main函数
public static void main(String[] args) throws SQLException {
String outUser = "Jack";
String inUser = "Rose";
int money = 1000;
AccountService accountService = new AccountService();
boolean result = accountService.transfer(outUser,inUser,money);
if (result) {
System.out.println("转账成功");
} else {
System.out.println("转账失败");
}
}
2.service层
public boolean transfer(String outUser, String inUser, int money) throws SQLException {
AccountDAO ad = new AccountDAO();
Connection conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);
try {
ad.out(outUser,money,conn);
// 模拟异常
int i = 1/0;
ad.in(inUser,money,conn);
JdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
3.dao层
public void out(String outUser, int money, Connection conn) throws SQLException {
String sql = "update account set money = money - ? where name = ?";
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
}
public void in(String inUser, int money, Connection conn) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
}
4.JdbcUtils关键代码
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
public static Connection getConnection(){
return ds.getConnection();
}
这样使用connection的手动提交可以保证数据操作的原子性,但是我们可以看到service与dao层中间的传递,我们将connection对象进行了传递,使用这种传参的形式,使得service层与dao层代码紧耦合。
而且在线程并发的场景下,需要保证service层与dao层的数据连接对象connection不受影响。
所以大家会想到使用synchronized
关键字进行加锁:
public boolean transfer(String outUser, String inUser, int money) throws SQLException {
AccountDAO ad = new AccountDAO();
Connection conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);
try {
synchronized (AccountService.class) {
ad.out(outUser,money,conn);
// 模拟异常
int i = 1/0;
ad.in(inUser,money,conn);
JdbcUtils.commitAndClose(conn);
}
} catch (Exception e) {
e.printStackTrace();
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
使用synchronized
关键字可以解决多线程可能发生的连接connection对象错取得问题,但是通过代码可以看到使用synchronized
关键字在进行转账操作时对类对象进行加锁,影响了程序并发性。
至此,我们考虑使用ThreadLocal类,这主要是解决多线程中连接connection对象在线程间得隔离问题。
只需在获取connection对象时,将其保存至TheadLocal局部变量中即可。
static ThreadLocal<Connection> tl = new ThreadLocal<>();
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
public static Connection getConnection(){
Connection conn = tl.get();
if (conn == null) {
conn = ds.getConnection();
tl.set(conn);
}
return tl.get();
}
注意在释放connection连接得时候需要将ThreadLocal中存储变量remove掉:tl.remove();
通过上面得ThreadLocal
类也可解决synchronized
关键字所能解决得问题,而且效率更高。
应用场景3:
在web项目中,会遇到很多情况下将数据存储到用户的session域中,但是在后面需要获取数据的时候,需要将request对象在controller层传入service层,可能会传入util方法中,或者传入其他相关处理的类中。然而这都是服务器的用户线程中,所以可以将需要的数据放入ThreadLocal中,在需要的地方之间进行获取即可。这种方式用的比较多。
三、ThreadLocal与synchronized的区别
通过上面的应用可以看到ThreadLocal与synchronized关键字均可实现多线程中线程隔离的问题,但是两者之间处理问题的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以“时间换空间”,只提供一份变量,让线程排队访问 | 采用“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相互不干扰 |
侧重点 | 多个线程之间访问资源的同步性 | 多线程中让每个线程之间的数据相互隔离 |
四ThreadLocal内部结构
ThreadLocal内部维护的是一个类似Map的ThreadLocalMap数据结构,key为当前对象的Thread对象,值为泛型的Object。
- 每个Thread线程内部都有一个Map(ThreadLocalMap)
- Map里面存储ThreadLocal(Key)和变量的副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取或设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程并不能获取当前线程的副本值, 行程了副本的隔离,互补干扰。
ThreadLocal内部结构设计的优势:
- 之后每个Map存储的Entry数量会变少(为什么会变少?)使用原始Map存储的时候是由Thread数量决定的,现在是由ThreadLocal的数量决定的。
- 当Thread销毁后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
五、ThreadLocal源码分析
初始化方法,当用户重写了set()方法时,可使用此方法提代set()为ThreadLocal数据初始化:
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方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取当前线程在ThreadLocalMap中所存储的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
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);
}
remove方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// remove此线程在ThreadLocalMap中的Entry
m.remove(this);
}
ThreadLocalMap内部类: ThreadLocalMap是ThreadLocal类的内部静态类,这个ThreadLocalMap是ThreadLocal中的关键数据结构。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// INITIAL_CAPACITY 初始化值与Map中的初始化值一样,为16,超过2/3则扩充
// 此处是创建一个存储entry对象的数组table[]
table = new Entry[INITIAL_CAPACITY];
// 根据ThreadLocal的hash算法值计算当前key在table数组中所处的index值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
Entry 对象,继承自WeakReference(弱引用,生命周期只能存活到下次GC前),只是Key是弱引用类型的,Value并非弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通过Key获取ThreadLocalMap中的Entry对象
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);
}
代码执行的流程
- 获取当前线程
- 根据当前线程获取一个Mape
- 如果Map不为空,则在Map中以ThreadLocal的引用作为Key来在Map中获取对应的value e,否则转至E;
- 如果e不能null,则返回e.value,否则转至E
- Map为空或e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value为firstKey和firstValue创建一个新的Map。
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回,不存在则创建并返回初始值。
ThreadLocal中的Hash冲突怎么解决
ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非HashMap中所使用的链表形式,而是采用线性探测的方式,即根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率更低。
所以这里给出的建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
ThreadLocalMap的问题
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.set(new Session(1, "Misout的博客"));
// 其它业务逻辑
} finally {
threadLocal.remove();
}
ThreadLocal代码使用演示
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
//获取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
总结
- 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
- ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
- 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。
以上为个人学习的理解,如有错误,欢迎指正!