ThreadLocal的作用
让每个线程都有自己的local值,为每一个线程都提供了变量的副本。这就可以让每一个线程在某个时间访问到的对象都是该线程自有的,从而让线程之间不会相互干扰。
1、线程并发:在多线程并发的场景下使用ThreadLocal
2、传递参数:可以使用ThreadLocal来传递参数,不仅可以保证不同组件之间使用的是同一个线程中的参数,还可以减少直接传参带来的耦合度提高的问题
3、线程隔离:让不同线程之间的变量相互独立
与Synchronized区别
Synchronized实现线程隔离的原理是采用同步锁,通过对特定的资源进行上锁的操作来实现。
多个线程会因为要获取上锁了的资源而排队执行,那么程序会失去原有的并发性。
而ThreadLocal是通过为Thread中的资源创建多个副本来实现的,每个线程都有属于自己的一份资源副本,互不干扰。这样就避免了排队,保障了并发性,同时也还将线程隔离开来。这种就是典型的用空间换取时间的方法。
下面是经典的银行转账例子:
Jimmy哥向Yuki转账
我们可以用synchronized来实现线程同步:
package JDBC;
import Adui.Dui;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ResourceBundle;
//事务控制,防止数据出错
public class TaskControl {
public static void main(String[] args) throws Exception {
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String url = bundle.getString("url");
String username = bundle.getString("username");
String password = bundle.getString("password");
Class.forName("com.mysql.cj.jdbc.Driver");
try {
DUtil.connect_D(url, username, password);
//开启事务,将JDBC的自动事务提交修改为false
DUtil.connection.setAutoCommit(false);
synchronized (TaskControl.class) {
//事务一,支出
String sql1 = "update money set balance = balance - ? where name = ?";
PreparedStatement ps1 = DUtil.connection.prepareStatement(sql1);
ps1.setInt(1, 10);
ps1.setString(2, "Jimmy");
ps1.executeUpdate();
//模拟异常发生
// String s = null;
// s.toString();
//事务二,收入
String sql2 = "update money set balance = balance + ? where name = ?";
PreparedStatement ps2 = DUtil.connection.prepareStatement(sql2);
ps2.setInt(1, 10);
ps2.setString(2, "Yuki");
ps2.executeUpdate();
//提交事务,事务结束
DUtil.connection.commit();
}
} catch (Exception e) {
//若有异常发生,回滚事务。然后事务结束
try {
DUtil.connection.rollback();
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
e.printStackTrace();
} finally {
DUtil.close_D();
}
}
}
但是这样会牺牲掉一定的性能。
如果使用ThreadLocal的话可以这样写,将连接对象放入ThreadLocal中进行隔离,只要需要使用连接对象就用get方法进行取出,保证每一个线程获取到的连接都是该线程私有的。
package JDBC;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ResourceBundle;
//事务控制,防止数据出错
public class TaskControl {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String url = bundle.getString("url");
String username = bundle.getString("username");
String password = bundle.getString("password");
Class.forName("com.mysql.cj.jdbc.Driver");
try {
Connection connection = DUtil.connect_D(url, username, password);
connectionHolder.set(connection);
//开启事务,将JDBC的自动事务提交修改为false
connectionHolder.get().setAutoCommit(false);
//事务一,支出
String sql1 = "update money set balance = balance - ? where name = ?";
PreparedStatement ps1 = connectionHolder.get().prepareStatement(sql1);
ps1.setInt(1, 10);
ps1.setString(2, "Jimmy");
ps1.executeUpdate();
//模拟异常发生
// String s = null;
// s.toString();
//事务二,收入
String sql2 = "update money set balance = balance + ? where name = ?";
PreparedStatement ps2 = connectionHolder.get().prepareStatement(sql2);
ps2.setInt(1, 10);
ps2.setString(2, "Yuki");
ps2.executeUpdate();
//提交事务,事务结束
connectionHolder.get().commit();
} catch (Exception e) {
//若有异常发生,回滚事务。然后事务结束
try {
if (connectionHolder.get() != null) {
connectionHolder.get().rollback();
}
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
e.printStackTrace();
} finally {
if (connectionHolder.get() != null) {
DUtil.close_D();
connectionHolder.remove();
}
}
}
}
ThreadLocald的内部结构
内部结构如上图所示
Thread中有其中的一个属性是ThreadLocalMap,这个Map的结构与HashMap类似,里面存放的是<Key,Value>结构的数据Entry,ThreadLocal作为Key。
这些我们可以查阅源码来理解。
ThreadLocal最常用的方法
1、set方法
这是将指定对象存入到ThreadLocal中的方法。
具体涉及到的源码如下:
public void set(T value) {
//先获取当前的线程对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//做一个map的判空操作,如果map不存在则需要创建,否则直接将对象存入
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
//创建Map后会将初始对象存入
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//这里是具体的存放操作,会对Map中的key
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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.refersTo(key)) {
e.value = value;
return;
}
//这里做一个过时的判断,如果key的值是null则过时,后续会被清理
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//清理过时条目并重新哈希
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2、get方法
获取指定数据的方法
具体涉及的源码如下:
//获取数据的方法
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;
}
}
//如果 ThreadLocalMap 或 Entry 为空,设置初始值并返回该初始值。
return setInitialValue();
}
//可以看到在Map中获取值的时候是以ThreadLocal作为key的
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
//设置初始值,这个默认是null,可以通过重写该方法来修改初始值
protected T initialValue() {
return null;
}
3、remove方法
在threadlocal资源使用完毕后将其释放,防止内存泄漏的发生。
//移除ThreadLocal对象的方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
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.refersTo(key)) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
还有一个问题就是为什么Entry要继承WeakReference?
首先,垃圾回收机制是会回收具有弱引用的对象的。而只要有强引用指向的对象是不会被回收的。即使ThreadLocal对象不再被程序中的其他部分引用,它仍然会被Entry强引用,从而无法被垃圾回收。这会导致内存泄漏。通过使用弱引用,当ThreadLocal对象不再被其他强引用持有时,垃圾回收器可以回收它,从而避免内存泄漏。
但是Entry中的value还在,它是被Map强引用的,所以还是会有风险。
还记得get和set是有一次对key判空的操作吧?
也就是说,如果我们忘记remove ThreadLocal,后续在下一次的set或者get过程中,也会对key为null的进行清理。