ThreadLocal初体验
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 */ public class ThreadLocal<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 使用案例
我们来看下面这个案例
public class MyDemo { private String content; private String getContent() { return content; } private void setContent(String content) { this.content = content; } public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("-----------------------"); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } }); thread.setName("线程" + i); thread.start(); } } }
打印结果:
从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用 ThreadLocal 的方式来解决这个问题的例子。
public class MyDemo { private static ThreadLocal<String> tl = new ThreadLocal<>(); private String content; private String getContent() { return tl.get(); } private void setContent(String content) { tl.set(content); } public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { 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代码块实现的效果:
public class Demo02 { private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } public static void main(String[] args) { Demo02 demo02 = new Demo02(); for (int i = 0; i < 5; i++) { Thread t = new Thread(){ @Override public void run() { 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关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用'以时间换空间'的方式, 只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用'以空间换时间'的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步性 | 多线程中让每个线程之间的数据相互隔离 |
总结: 在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。
2. 运用场景_事务案例
通过以上的介绍,我们已经基本了解ThreadLocal的特点。但是它具体的应用是在哪里呢? 现在让我们一起来看一个ThreadLocal的经典运用场景: 事务。
2.1 转账案例
2.1.1 场景构建
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。
案例的实现就简单的用mysql数据库,JDBC 和 C3P0 框架实现。以下是详细代码 :
(1) 项目结构
2) 数据准备
-- 使用数据库 use test; -- 创建一张账户表 create table account( id int primary key auto_increment, name varchar(20), money double ); -- 初始化数据 insert into account values(null, 'Jack', 1000); insert into account values(null, 'Rose', 1000);
(3) C3P0配置文件和工具类
c3p0-config> <!-- 使用默认的配置读取连接池对象 --> <default-config> <!-- 连接参数 --> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property> <property name="user">root</property> <property name="password">1234</property> <!-- 连接池参数 --> <property name="initialPoolSize">5</property> <property name="maxPoolSize">10</property> <property name="checkoutTimeout">3000</property> </default-config> </c3p0-config>
4) 工具类 : JdbcUtils
public class JdbcUtils { // c3p0 数据库连接池对象属性 private static final ComboPooledDataSource ds = new ComboPooledDataSource(); // 获取连接 public static Connection getConnection() throws SQLException { return ds.getConnection(); } //释放资源 public static void release(AutoCloseable... ios){ for (AutoCloseable io : ios) { if(io != null){ try { io.close(); } catch (Exception e) { e.printStackTrace(); } } } } public static void commitAndClose(Connection conn) { try { if(conn != null){ //提交事务 conn.commit(); //释放连接 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } public static void rollbackAndClose(Connection conn) { try { if(conn != null){ //回滚事务 conn.rollback(); //释放连接 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }
5) dao层代码 : AccountDao
public class AccountDao { public void out(String outUser, int money) throws SQLException { 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); } public void in(String inUser, int money) throws SQLException { 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
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); try { // 转出 ad.out(outUser, money); // 转入 ad.in(inUser, money); } catch (Exception e) { e.printStackTrace(); return false; } return true; } }
(7) web层代码 : AccountWeb
public class AccountWeb { public static void main(String[] args) { // 模拟数据 : Jack 给 Rose 转账 100 String outUser = "Jack"; String inUser = "Rose"; int money = 100; AccountService as = new AccountService(); boolean result = as.transfer(outUser, inUser, money); if (result == false) { System.out.println("转账失败!"); } else { System.out.println("转账成功!"); } } }
2.1.2 引入事务
案例中的转账涉及两个DML操作: 一个转出,一个转入。这些操作是需要具备原子性的,不可分割。不然就有可能出现数据修改异常情况。
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); try { // 转出 ad.out(outUser, money); // 模拟转账过程中的异常 int i = 1/0; // 转入 ad.in(inUser, money); } catch (Exception e) { e.printStackTrace(); return false; } return true; } }
所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。
(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 类
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); //线程并发情况下,为了保证每个线程使用各自的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); return false; } return true; } } }
(2) AccountDao 类 (这里需要注意的是: connection不能在dao层释放,要在service层,不然在dao层释放,service层就无法使用了)
public class AccountDao { public void out(Connection conn, String outUser, int money) throws SQLException{ 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); } public void in(Connection conn, String inUser, int money) throws SQLException { 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
public class JdbcUtils { //ThreadLocal对象 : 将connection绑定在当前线程中 private static final ThreadLocal<Connection> tl = new ThreadLocal(); // c3p0 数据库连接池对象属性 private static final ComboPooledDataSource ds = new ComboPooledDataSource(); // 获取连接 public static Connection getConnection() throws SQLException { //取出当前线程绑定的connection对象 Connection conn = tl.get(); if (conn == null) { //如果没有,则从连接池中取出 conn = ds.getConnection(); //再将connection对象绑定到当前线程中 tl.set(conn); } return conn; } //释放资源 public static void release(AutoCloseable... ios) { for (AutoCloseable io : ios) { if (io != null) { try { io.close(); } catch (Exception e) { e.printStackTrace(); } } } } public static void commitAndClose() { try { Connection conn = getConnection(); //提交事务 conn.commit(); //解除绑定 tl.remove(); //释放连接 conn.close(); } catch (SQLException e) { e.printStackTrace(); } } public static void rollbackAndClose() { try { Connection conn = getConnection(); //回滚事务 conn.rollback(); //解除绑定 tl.remove(); //释放连接 conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
(2) AccountService类的修改:不需要传递connection对象
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); 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(); return false; } return true; } }
(3) AccountDao类的修改:照常使用
public class AccountDao { public void out(String outUser, int money) throws SQLException { 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); } public void in(String inUser, int money) throws SQLException { 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方案有两个突出的优势:
-
传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
-
线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失