文章目录
- 1 简介
- 2 基本使用
- 2.1 常用方法
- 2.2 小案例
- 3 ThreadLocal与Sycronized
- 4 应用场景
- 4.1 转账案例构建
- 4.2 问题
- 4.3 解决
- 5 后记
1 简介
官方JDK源码关于ThreadLocal描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法)能保证各个线程的局部变量独立其他线程的局部变量。ThreadLocal实例通常都是某些类的私有静态内部成员,用于关联线程和现场上下文。
ThreadLocal的作用:提供线程内局部变量,不同现场间不会相互干扰;线程局部变量在线程的生命周期内起作用;减少同一个线程内多个函数或者组件之间一些共享变量传递的复杂性。
总结:
- 线程并发:在多线程并发的场景下。
- 传递数据:我们可以通过ThreadLocal在同一线程下,不同组件中传递公共变量。
- 线程隔离:每个线程的变量都是独立的,不会相互影响。
2 基本使用
2.1 常用方法
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
T get() | 获取当前线程绑定的局部变量 |
void set(T) | 设置当前线程绑定的局部变量 |
void remove() | 移除当前线程绑定的局部变量 |
2.2 小案例
- 要求:对于一个类成员变量,每个线程各自获取或者设置自己的值即实现线程隔离;单个线程设置和获取同一个变量
初始代码如下:
/**
* @author Administrator
* @date 2022-12-04 13:13
*/
public class MyDemo {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) throws InterruptedException {
MyDemo myDemo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
myDemo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("---------------");
System.out.println(Thread.currentThread().getName() + "----->" + myDemo.getContent());
});
thread.setName("线程" + i);
thread.start();
}
}
}
// 测试结果
---------------
---------------
---------------
线程1----->线程2的数据
线程2----->线程2的数据
---------------
线程4----->线程4的数据
线程0----->线程4的数据
---------------
线程3----->线程3的数据
用ThreadLocal改造上述代码如下:
/**
* @author Administrator
* @date 2022-12-04 13:13
*/
public class MyDemo {
private String content;
ThreadLocal<String> tl = new ThreadLocal<>();
public String getContent() {
// return content;
return tl.get();
}
public void setContent(String content) {
// this.content = content;
tl.set(content);
}
public void clear() {
tl.remove();
}
public static void main(String[] args) throws InterruptedException {
MyDemo myDemo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
myDemo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("---------------");
System.out.println(Thread.currentThread().getName() + "----->" + myDemo.getContent());
});
thread.setName("线程" + i);
thread.start();
// thread.join();
}
// System.out.println(myDemo.getContent());
myDemo.clear();
}
}
// 测试结果
---------------
---------------
---------------
线程0----->线程0的数据
线程1----->线程1的数据
---------------
线程4----->线程4的数据
---------------
线程3----->线程3的数据
线程2----->线程2的数据
3 ThreadLocal与Sycronized
上面的测试用例用Syncronized能不能解决呢?答案是肯定的,但是有没有区别呢,用例相对简单这里不在给出Syncronized实现,下面分析ThreadLocal和Syncronized的不同,如下表3-1所示:
Syncronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用用时间换空间的方式,只提供一份变量,让不同线程排队访问 | ThreadLocal采用以空间换时间的方式,为每一个线程提供一份变量副本,从而实现同时访问而不相互干扰 |
侧重点 | 多个线程访问共享资源的同步 | 多个线程中让每个线程之间的数据相互隔离 |
效果 | 共享资源一致性,性能损耗,失去并发性 | 每个线程之间数据隔离,每个线程在不同方法之间数据的传递,一致性 |
- 总结:在刚刚的案例中,虽然使用ThreadLocal和Syncronized都能解决问题,但是使用ThreadLocal更为合适,因为这样使程序拥有更高的并发性。
4 应用场景
通过以上内容,我们已经基本了解ThreadLocal的特点,下面我们 通过一个案例,来看下ThreadLocal的应用场景:事务操作。
4.1 转账案例构建
-
构建转账场景:有一个数据表,有2个用户张三和李四,张三给李四转账。
-
设计一些知识:mysql数据库,JDBC和c3p0连接池,maven项目。
-
项目结构如下:
-
功能简介
- AccountDao实现了转出、转入及关闭操作
- AccountService:转账和释放资源
- JdbcUtils:获取连接、事务提交、事务回滚、释放连接
4.2 问题
在多线程环境下转账我们要保证数据完整性或者一致性,比如张三账户1000,例如账户1000,无论如何转账,总的资产为2000,但是实际会遇到什么情况呢?
- 由于异常原因程序未完整运行导致数据不完整
- 多线程并发导致数据库不完整性
4.3 解决
对于第一种情况,我们需要开启事务。开启事务需要保证在多线程环境下,在一次完整转账操作中数据库连接为同一个,在多个操作下连接可共享。多次转账间数据库连接的隔离,不相互干扰。
对于第二种情况设计数据库加锁问题,这里我们不做讨论。
通过上面的讨论,我们可以使用ThreadLocal来解决第一种情况,具体涉及数据库连接获取的代码如下:
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
/**
* 获取连接
* 1. 直接从当前线程获取绑定的连接
* 2. 如果连接对象为空
* 2.1 在去连接池去获取连接
* 2.2 将此对象绑定到当前线程
*/
static ThreadLocal<Connection> tc = new ThreadLocal<>();
/**
* 获取数据库连接
* @return 数据库连接
* @throws SQLException sql异常
*/
public static Connection getConnection() throws SQLException {
Connection conn = tc.get();
if (conn == null) {
conn = ds.getConnection();
tc.set(conn);
}
return conn;
}
- 第一次获取连接为空的情况下
- 从数据库连接池获取连接
- 把连接对象绑定到当前线程
- 非第一次直接当前线程获取本地变量
通过在转账中添加/0算术异常来模拟异常情况,测试结果如下:
java.lang.ArithmeticException: / by zero
at com.gaogzhen.service.AccountService.transfer(AccountService.java:32)
at com.gaogzhen.web.AccountWeb.main(AccountWeb.java:18)
转账失败
Account(id=1, name=张三, balance=1000.00)
Account(id=2, name=李四, balance=1000.00)
完成的代码见文章末尾代码仓库地址。
5 后记
如有问题,欢迎交流讨论。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent
参考:
[1]黑马程序员Java基础教程由浅入深全面解析threadlocal[CP/OL].2020-03-24.