ThreadLocal解决线程隔离问题
ThreadLocal类可以高效解决线程间数据隔离的问题
先看效果:
- 未使用ThreadLocal
所有线程在对同一个对象的同一个属性进行读写操作,产生数据冲突
public class ThreadLocalDemo {
private String var;
public String getVar() {
return var;
}
public void setVar(String var) {
this.var = var;
}
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
for(int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
// 所有线程都在操作同一个对象demo的var属性,产生并发过程中的数据冲突
demo.setVar(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------"); // 本行起到sleep(time)的作用
System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
});
thread.setName("线程" + i);
thread.start();
}
}
}
输出:
线程1 ---> 线程2的数据
-----------------------
线程2 ---> 线程3的数据 // 出现和期待结果不一样的情况
-----------------------
线程4 ---> 线程4的数据
-----------------------
线程0 ---> 线程4的数据
-----------------------
线程3 ---> 线程4的数据
可以看到并发执行时出现了问题,var的值和预期结果不一致
- 使用ThreadLocal
仅改动get/set函数
public class ThreadLocalDemo {
private String var;
ThreadLocal<String> threadLocal = new ThreadLocal<>();
public String getVar() {
return threadLocal.get(); // 通过ThreadLocal获取属性值
}
public void setVar(String var) {
threadLocal.set(var); // 通过ThreadLocal设置属性值
}
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
for(int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
demo.setVar(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------"); // 本行起到sleep(time)的作用
System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
});
thread.setName("线程" + i);
thread.start();
}
}
}
不会再出现并发过程中数据冲突的问题,每次都会取得期望的数据
-----------------------
-----------------------
-----------------------
线程3 ---> 线程3的数据
-----------------------
线程1 ---> 线程1的数据
-----------------------
-----------------------
线程6 ---> 线程6的数据
线程2 ---> 线程2的数据
线程0 ---> 线程0的数据
线程4 ---> 线程4的数据
-----------------------
线程8 ---> 线程8的数据
-----------------------
线程5 ---> 线程5的数据
-----------------------
线程7 ---> 线程7的数据
-----------------------
线程9 ---> 线程9的数据
-----------------------
线程10 ---> 线程10的数据
-----------------------
线程12 ---> 线程12的数据
-----------------------
-----------------------
线程14 ---> 线程14的数据
-----------------------
线程15 ---> 线程15的数据
-----------------------
线程18 ---> 线程18的数据
-----------------------
线程16 ---> 线程16的数据
-----------------------
线程11 ---> 线程11的数据
线程13 ---> 线程13的数据
-----------------------
线程17 ---> 线程17的数据
-----------------------
线程19 ---> 线程19的数据
下面通过源码看下为什么ThreadLocal可以解决线程间数据安全问题
- 每个Thread对象都持有一个ThreadLocal.ThreadLocalMap对象
- ThreadLocalMap是ThreadLocal的一个内部类
- ThreadLocalMap保存了很多个键值对Entry<ThreadLocal, value>
class Thread implements Runnable {
...
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
public class ThreadLocal<T> {
...
...
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
...
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
...
}
下面结合源码逐步分析main()执行过程:
main:
demo.setVar(Thread.currentThread().getName() + "的数据");
// 会执行如下代码:
ThreadLocal:
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的threadLocals属性(类型是ThreadLocal.ThreadLocalMap)
if (map != null)
map.set(this, value); // 在当前线程中放入Entry<当前ThreadLocal, var>
else
createMap(t, value);
}
可以看到使用ThreadLocal本质是在需要为线程和变量设置关联关系时,在每个线程的threadLocals属性中加入一个Entry<ThreadLocal, value>来创建一个value的副本,这样每个线程内部都有一份变量,互不干扰,通过这样一种方式完成了线程和变量的绑定,一个线程可以绑定多个变量。
下面看看如何获取绑定的变量值:
main:
System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
ThreadLocalDemo:
public String getVar() {
return threadLocal.get();
}
ThreadLocal:
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的threadLocals属性
if (map != null) { // 遍历threadLocals中所有Entry以 this(一个ThreadLocal对象)为Key值寻找
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
综上可以看到ThreadLocal本质就是为每个线程创建一个变量副本,线程之间操作的同名变量地址不同,实现了线程间的数据隔离
ThreadLocal的高效性
上述示例当然可以用synchronized来保证数据的安全
public static void main(String[] args) throws Exception {
ThreadLocalDemo demo = new ThreadLocalDemo();
for(int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
synchronized (ThreadLocalDemo.class) {
demo.setVar(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------"); // 本行起到sleep(time)的作用
System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
}
});
thread.setName("线程" + i);
thread.start();
}
}
但是显然,这样丧失了并发性,也会频繁的加锁,解锁增加上下文切换的开销
和synchronized保证数据隔离的方法不同,ThreadLocal以空间换时间,每个线程存有一个变量副本,所以效率上会更高。
synchronized侧重的是保证线程间对临界资源的同步访问问题,ThreadLocal侧重的是保证线程间数据的隔离。
ThreadLocal应用
一个经典的使用场景是,在数据库的交互过程中使用ThreadLocal来降低代码的耦合度
示例如下,一个比较常见的转账的service和dao伪代码
Service:
public class AccountService {
private AccountDAO accountDAO = new AccountDAO();
public boolean tranfer(String outUser, String inUser, int money) {
Connection connection = null;
connection = JdbcUtils.getConnection(); // 获取连接
try {
connection.setAutoCommit(false); // 开启事务
accountDAO.sub(outUser, money, connection); // outUser账号金额减少
accountDAO.add(inUser, money, connection); // inUser账号金额增加
connection.commit(); // 提交事务
connection.close();
} catch (SQLException e) {
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
return false;
}
}
Dao:
public class AccountDAO {
public void add(String userName, int money, Connection connection) {
use connection do someting...
}
public void sub(String userName, int money, Connection connection) {
use connection do someting...
}
}
JdbcUtils:
public class JdbcUtils {
public static Connection getConnection() {
从连接池获取连接并返回
}
}
在一个事务中,Service层会在DAO层调用之前开启事务,保证下面的转账动作(包括转入和转出动作)是原子操作,DAO层在执行操作时,也必须要保证和Service开启事务使用的connection是同一个对象,否则Service层开启事务的动作毫无意义。这就导致Service层必须将开启事务所使用的connection作为参数传入DAO层,结果就是在设计DAO层时,形参列表多了一个参数,Service层和DAO层高度耦合。
那么为什么不直接在DAO层进行connection的获取和事务的开启呢?
可以看下这样做的结果:
Dao:
public class AccountDAO {
public void add(String userName, int money) {
Connection connection = null;
connection = JdbcUtils.getConnection(); // 获取连接
connection.setAutoCommit(false); // 开启事务
do someting...
connection.commit(); // 提交事务
connection.close();
}
public void sub(String userName, int money) {
Connection connection = null;
connection = JdbcUtils.getConnection(); // 获取连接
connection.setAutoCommit(false); // 开启事务
do someting...
connection.commit(); // 提交事务
connection.close();
}
}
显然这样会导致每进行一次DAO层操作都要开启关闭一次连接,频繁的开启关闭本身就是对资源的一种浪费。同时,明显DAO层代码变得臃肿。
这也是为什么在数据库操作中,事务的开启关闭等对connection对象的操作放在Service层的原因。
那么现在要到达以下目的:
- 减少Service层和DAO层耦合关系,让DAO层不需要在入参中加入connection
- 保证Service层和DAO层使用同一个connection
这就要使用到ThreadLocal
对JdbcUtils进行如下修改:
public class JdbcUtils {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
private static DataSource ds;
static {
try {
ds = DruidDataSourceFactory.createDataSource(new Properties());
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
Connection connection = threadLocal.get(); // 使用ThreadLocal的get()方法获取
if(connection == null) {
connection = ds.getConnection();
threadLocal.set(connection); // 对当前Thread和connection进行绑定
}
return connection;
}
}
这样DAO层和Service层就可以如下修改:
Service:
public class AccountService {
private AccountDAO accountDAO = new AccountDAO();
public boolean tranfer(String outUser, String inUser, int money) {
Connection connection = null;
connection = JdbcUtils.getConnection(); // 获取连接
try {
connection.setAutoCommit(false); // 开启事务
accountDAO.sub(outUser, money); // outUser账号金额减少
accountDAO.add(inUser, money); // inUser账号金额增加
connection.commit(); // 提交事务
connection.close();
} catch (SQLException e) {
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
return false;
}
}
Dao:
public class AccountDAO {
public void add(String userName, int money) throws SQLException{
Connection connection = JdbcUtils.getConnection();
use connection do someting...
}
public void sub(String userName, int money) throws SQLException{
Connection connection = JdbcUtils.getConnection();
use connection do someting...
}
}
显然DAO层使用的connection一定是Service层使用的connection,原理参见前文