文章目录
1. ThreadLocal介绍
1.1 官方介绍
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/publicclassThreadLocal<T>{...
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间
不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数
或组件之间一些公共变量传递的复杂度。
总结:
1. 线程并发: 在多线程并发的场景下
2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3. 线程隔离: 每个线程的变量都是独立的,不会互相影响
1.2 基本使用
1.2.1 常用方法
在使用之前,我们先来认识几个ThreadLocal的常用方法
方法声明描述ThreadLocal()创建ThreadLocal对象public void set( T value)设置当前线程绑定的局部变量public T get()获取当前线程绑定的局部变量public void remove()移除当前线程绑定的局部变量
1.2.2 使用案例
我们来看下面这个案例, 感受一下ThreadLocal 线程隔离的特点:
publicclassMyDemo{privateString content;privateStringgetContent(){return content;}privatevoidsetContent(String content){this.content = content;}publicstaticvoidmain(String[] args){MyDemo demo =newMyDemo();for(int i =0; i <5; i++){Thread thread =newThread(newRunnable(){@Overridepublicvoidrun(){
demo.setContent(Thread.currentThread().getName()+"的数据");System.out.println("-----------------------");System.out.println(Thread.currentThread().getName()+"--->"+ demo.getContent());}});
thread.setName("线程"+ i);
thread.start();}}}
打印结果:
从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用 ThreadLocal 的方式来解决这个问题的例子。
publicclassMyDemo{privatestaticThreadLocal<String> tl =newThreadLocal<>();privateString content;privateStringgetContent(){return tl.get();}privatevoidsetContent(String content){
tl.set(content);}publicstaticvoidmain(String[] args){MyDemo demo =newMyDemo();for(int i =0; i <5; i++){Thread thread =newThread(newRunnable(){@Overridepublicvoidrun(){
demo.setContent(Thread.currentThread().getName()+"的数据");System.out.println("-----------------------");System.out.println(Thread.currentThread().getName()+"--->"+ demo.getContent());}});
thread.setName("线程"+ i);
thread.start();}}}
打印结果:
从结果来看,这样很好的解决了多线程之间数据隔离的问题,十分方便。
1.3 ThreadLocal类与synchronized关键字
1.3.1 synchronized同步方式
这里可能有的朋友会觉得在上述例子中我们完全可以通过加锁来实现这个功能。我们首先来看一下用synchronized代码块实现的效果:
publicclassDemo02{privateString content;publicStringgetContent(){return content;}publicvoidsetContent(String content){this.content = content;}publicstaticvoidmain(String[] args){Demo02 demo02 =newDemo02();for(int i =0; i <5; i++){Thread t =newThread(){@Overridepublicvoidrun(){synchronized(Demo02.class){
demo02.setContent(Thread.currentThread().getName()+"的数据");System.out.println("-------------------------------------");String content = demo02.getContent();System.out.println(Thread.currentThread().getName()+"--->"+ content);}}};
t.setName("线程"+ i);
t.start();}}}
打印结果:
从结果可以发现, 加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题, 在这个案例中使用synchronized关键字是不合适的。
1.3.2 ThreadLocal与synchronized的区别
虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronizedThreadLocal原理同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离
总结: 在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,
但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。
2. 运用场景_事务案例
通过以上的介绍,我们已经基本了解ThreadLocal的特点。但是它具体是运用在什么场景中呢? 接下来让我们看一个案例: 事务操作。
2.1 转账案例
2.1.1 场景构建
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。
案例的实现主要用mysql数据库,JDBC 和 C3P0 框架。以下是详细代码 :
(1) 项目结构
(2) 数据准备
-- 使用数据库use test;-- 创建一张账户表createtable account(
id intprimarykeyauto_increment,
name varchar(20),
money double);-- 初始化数据insertinto account values(null,'Jack',1000);insertinto account values(null,'Rose',0);
(3) C3P0配置文件和工具类
<c3p0-config><!-- 使用默认的配置读取连接池对象 --><default-config><!-- 连接参数 --><propertyname="driverClass">com.mysql.jdbc.Driver</property><propertyname="jdbcUrl">jdbc:mysql://localhost:3306/test</property><propertyname="user">root</property><propertyname="password">1234</property><!-- 连接池参数 --><propertyname="initialPoolSize">5</property><propertyname="maxPoolSize">10</property><propertyname="checkoutTimeout">3000</property></default-config></c3p0-config>
(4) 工具类 : JdbcUtils
packagecom.itheima.transfer.utils;importcom.mchange.v2.c3p0.ComboPooledDataSource;importjava.sql.Connection;importjava.sql.SQLException;publicclassJdbcUtils{// c3p0 数据库连接池对象属性privatestaticfinalComboPooledDataSource ds =newComboPooledDataSource();// 获取连接publicstaticConnectiongetConnection()throwsSQLException{return ds.getConnection();}//释放资源publicstaticvoidrelease(AutoCloseable... ios){for(AutoCloseable io : ios){if(io !=null){try{
io.close();}catch(Exception e){
e.printStackTrace();}}}}publicstaticvoidcommitAndClose(Connection conn){try{if(conn !=null){//提交事务
conn.commit();//释放连接
conn.close();}}catch(SQLException e){
e.printStackTrace();}}publicstaticvoidrollbackAndClose(Connection conn){try{if(conn !=null){//回滚事务
conn.rollback();//释放连接
conn.close();}}catch(SQLException e){
e.printStackTrace();}}}
(5) dao层代码 : AccountDao
packagecom.itheima.transfer.dao;importcom.itheima.transfer.utils.JdbcUtils;importjava.sql.Connection;importjava.sql.PreparedStatement;importjava.sql.SQLException;publicclassAccountDao{publicvoidout(String outUser,int money)throwsSQLException{String sql ="update account set money = money - ? where name = ?";Connection conn =JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();JdbcUtils.release(pstm,conn);}publicvoidin(String inUser,int money)throwsSQLException{String sql ="update account set money = money + ? where name = ?";Connection conn =JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();JdbcUtils.release(pstm,conn);}}
(6) service层代码 : AccountService
packagecom.itheima.transfer.service;importcom.itheima.transfer.dao.AccountDao;importjava.sql.SQLException;publicclassAccountService{publicbooleantransfer(String outUser,String inUser,int money){AccountDao ad =newAccountDao();try{// 转出
ad.out(outUser, money);// 转入
ad.in(inUser, money);}catch(Exception e){
e.printStackTrace();returnfalse;}returntrue;}}
(7) web层代码 : AccountWeb
packagecom.itheima.transfer.web;importcom.itheima.transfer.service.AccountService;publicclassAccountWeb{publicstaticvoidmain(String[] args){// 模拟数据 : Jack 给 Rose 转账 100String outUser ="Jack";String inUser ="Rose";int money =100;AccountService as =newAccountService();boolean result = as.transfer(outUser, inUser, money);if(result ==false){System.out.println("转账失败!");}else{System.out.println("转账成功!");}}}
2.1.2 引入事务
案例中的转账涉及两个DML操作: 一个转出,一个转入。这些操作是需要具备原子性的,不可分割。不然就有可能出现数据修改异常情况。
publicclassAccountService{publicbooleantransfer(String outUser,String inUser,int money){AccountDao ad =newAccountDao();try{// 转出
ad.out(outUser, money);// 模拟转账过程中的异常int i =1/0;// 转入
ad.in(inUser, money);}catch(Exception e){
e.printStackTrace();returnfalse;}returntrue;}}
所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。
(1) JDBC中关于事务的操作的api
Connection接口的方法作用void setAutoCommit(false)禁用事务自动提交(改为手动)void commit();提交事务void rollback();回滚事务
(2) 开启事务的注意点:
- 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个: service层开启事务的connection需要跟dao层访问数据库的connection保持一致
- 线程并发情况下, 每个线程只能操作各自的 connection
2.2 常规解决方案
2.2.1 常规方案的实现
基于上面给出的前提, 大家通常想到的解决方案是 :
- 传参: 从service层将connection对象向dao层传递
- 加锁
以下是代码实现修改的部分:
(1 ) AccountService 类
packagecom.itheima.transfer.service;importcom.itheima.transfer.dao.AccountDao;importcom.itheima.transfer.utils.JdbcUtils;importjava.sql.Connection;publicclassAccountService{publicbooleantransfer(String outUser,String inUser,int money){AccountDao ad =newAccountDao();//线程并发情况下,为了保证每个线程使用各自的connection,故加锁synchronized(AccountService.class){Connection conn =null;try{
conn =JdbcUtils.getConnection();//开启事务
conn.setAutoCommit(false);// 转出
ad.out(conn, outUser, money);// 模拟转账过程中的异常// int i = 1/0;// 转入
ad.in(conn, inUser, money);//事务提交JdbcUtils.commitAndClose(conn);}catch(Exception e){
e.printStackTrace();//事务回滚JdbcUtils.rollbackAndClose(conn);returnfalse;}returntrue;}}}
(2) AccountDao 类 (这里需要注意的是: connection不能在dao层释放,要在service层,不然在dao层释放,service层就无法使用了)
packagecom.itheima.transfer.dao;importcom.itheima.transfer.utils.JdbcUtils;importjava.sql.Connection;importjava.sql.PreparedStatement;importjava.sql.SQLException;publicclassAccountDao{publicvoidout(Connection conn,String outUser,int money)throwsSQLException{String sql ="update account set money = money - ? where name = ?";//注释从连接池获取连接的代码,使用从service中传递过来的connection// Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();//连接不能在这里释放,service层中还需要使用// JdbcUtils.release(pstm,conn);JdbcUtils.release(pstm);}publicvoidin(Connection conn,String inUser,int money)throwsSQLException{String sql ="update account set money = money + ? where name = ?";// Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();// JdbcUtils.release(pstm,conn);JdbcUtils.release(pstm);}}
2.2.2 常规方案的弊端
上述方式我们看到的确按要求解决了问题,但是仔细观察,会发现这样实现的弊端:
- 直接从service层传递connection到dao层, 造成代码耦合度提高
- 加锁会造成线程失去并发性,程序性能降低
2.3 ThreadLocal解决方案
2.3.1 ThreadLocal方案的实现
像这种需要在项目中进行数据传递和线程隔离的场景,我们不妨用ThreadLocal来解决:
(1) 工具类的修改: 加入ThreadLocal
packagecom.itheima.transfer.utils;importcom.mchange.v2.c3p0.ComboPooledDataSource;importjava.sql.Connection;importjava.sql.SQLException;publicclassJdbcUtils{//ThreadLocal对象 : 将connection绑定在当前线程中privatestaticfinalThreadLocal<Connection> tl =newThreadLocal();// c3p0 数据库连接池对象属性privatestaticfinalComboPooledDataSource ds =newComboPooledDataSource();// 获取连接publicstaticConnectiongetConnection()throwsSQLException{//取出当前线程绑定的connection对象Connection conn = tl.get();if(conn ==null){//如果没有,则从连接池中取出
conn = ds.getConnection();//再将connection对象绑定到当前线程中
tl.set(conn);}return conn;}//释放资源publicstaticvoidrelease(AutoCloseable... ios){for(AutoCloseable io : ios){if(io !=null){try{
io.close();}catch(Exception e){
e.printStackTrace();}}}}publicstaticvoidcommitAndClose(){try{Connection conn =getConnection();//提交事务
conn.commit();//解除绑定
tl.remove();//释放连接
conn.close();}catch(SQLException e){
e.printStackTrace();}}publicstaticvoidrollbackAndClose(){try{Connection conn =getConnection();//回滚事务
conn.rollback();//解除绑定
tl.remove();//释放连接
conn.close();}catch(SQLException e){
e.printStackTrace();}}}
(2) AccountService类的修改:不需要传递connection对象
packagecom.itheima.transfer.service;importcom.itheima.transfer.dao.AccountDao;importcom.itheima.transfer.utils.JdbcUtils;importjava.sql.Connection;publicclassAccountService{publicbooleantransfer(String outUser,String inUser,int money){AccountDao ad =newAccountDao();try{Connection conn =JdbcUtils.getConnection();//开启事务
conn.setAutoCommit(false);// 转出 : 这里不需要传参了 !
ad.out(outUser, money);// 模拟转账过程中的异常// int i = 1 / 0;// 转入
ad.in(inUser, money);//事务提交JdbcUtils.commitAndClose();}catch(Exception e){
e.printStackTrace();//事务回滚JdbcUtils.rollbackAndClose();returnfalse;}returntrue;}}
(3) AccountDao类的修改:照常使用
packagecom.itheima.transfer.dao;importcom.itheima.transfer.utils.JdbcUtils;importjava.sql.Connection;importjava.sql.PreparedStatement;importjava.sql.SQLException;publicclassAccountDao{publicvoidout(String outUser,int money)throwsSQLException{String sql ="update account set money = money - ? where name = ?";Connection conn =JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();//照常使用// JdbcUtils.release(pstm,conn);JdbcUtils.release(pstm);}publicvoidin(String inUser,int money)throwsSQLException{String sql ="update account set money = money + ? where name = ?";Connection conn =JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();// JdbcUtils.release(pstm,conn);JdbcUtils.release(pstm);}}
2.3.2 ThreadLocal方案的好处
从上述的案例中我们可以看到, 在一些特定场景下,ThreadLocal方案有两个突出的优势:
- 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
- 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
版权归原作者 zoeil 所有, 如有侵权,请联系我们删除。