官方JDK源码关于ThreadLocal描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法)能保证各个线程的局部变量独立其他线程的局部变量。ThreadLocal实例通常都是某些类的私有静态内部成员,用于关联线程和现场上下文。
ThreadLocal的作用:提供线程内局部变量,不同现场间不会相互干扰;线程局部变量在线程的生命周期内起作用;减少同一个线程内多个函数或者组件之间一些共享变量传递的复杂性。
总结:
| 方法声明 | 描述 |
|---|---|
| ThreadLocal() | 创建ThreadLocal对象 |
| T get() | 获取当前线程绑定的局部变量 |
| void set(T) | 设置当前线程绑定的局部变量 |
| void remove() | 移除当前线程绑定的局部变量 |
初始代码如下:
/*** @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 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的数据
上面的测试用例用Syncronized能不能解决呢?答案是肯定的,但是有没有区别呢,用例相对简单这里不在给出Syncronized实现,下面分析ThreadLocal和Syncronized的不同,如下表3-1所示:
| Syncronized | ThreadLocal | |
|---|---|---|
| 原理 | 同步机制采用用时间换空间的方式,只提供一份变量,让不同线程排队访问 | ThreadLocal采用以空间换时间的方式,为每一个线程提供一份变量副本,从而实现同时访问而不相互干扰 |
| 侧重点 | 多个线程访问共享资源的同步 | 多个线程中让每个线程之间的数据相互隔离 |
| 效果 | 共享资源一致性,性能损耗,失去并发性 | 每个线程之间数据隔离,每个线程在不同方法之间数据的传递,一致性 |
通过以上内容,我们已经基本了解ThreadLocal的特点,下面我们 通过一个案例,来看下ThreadLocal的应用场景:事务操作。
构建转账场景:有一个数据表,有2个用户张三和李四,张三给李四转账。
设计一些知识:mysql数据库,JDBC和c3p0连接池,maven项目。
项目结构如下:
功能简介
在多线程环境下转账我们要保证数据完整性或者一致性,比如张三账户1000,例如账户1000,无论如何转账,总的资产为2000,但是实际会遇到什么情况呢?
对于第一种情况,我们需要开启事务。开启事务需要保证在多线程环境下,在一次完整转账操作中数据库连接为同一个,在多个操作下连接可共享。多次转账间数据库连接的隔离,不相互干扰。
对于第二种情况设计数据库加锁问题,这里我们不做讨论。
通过上面的讨论,我们可以使用ThreadLocal来解决第一种情况,具体涉及数据库连接获取的代码如下:
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
/*** 获取连接* 1. 直接从当前线程获取绑定的连接* 2. 如果连接对象为空* 2.1 在去连接池去获取连接* 2.2 将此对象绑定到当前线程*/
static ThreadLocal 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 zeroat 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)
完成的代码见文章末尾代码仓库地址。
如有问题,欢迎交流讨论。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent
参考:
[1]黑马程序员Java基础教程由浅入深全面解析threadlocal[CP/OL].2020-03-24.