ThreadLocal介绍
官方介绍
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
- 线程并发: 在多线程并发的场景下
- 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
- 线程隔离: 每个线程的变量都是独立的,不会互相影响
基本使用
常用方法
在使用之前,我们先来认识几个ThreadLocal的常用方法
方法声明 | 描述 |
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
使用案例
我们来看下面这个案例 , 感受一下ThreadLocal 线程隔离的特点:
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 MyDemo1 { 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(); } } }
打印结果:
从结果来看,这样很好的解决了多线程之间数据隔离的问题,十分方便。
ThreadLocal类与synchronized关键字
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关键字是不合适的。
ThreadLocal与synchronized的区别
虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal |
原理 | 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
总结:
在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。
运用场景_事务案例
通过以上的介绍,我们已经基本了解ThreadLocal的特点。但是它具体是运用在什么场景中呢? 接下来让我们看一个案例: 事务操作。
转账案例
场景构建
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。
案例的实现主要用mysql数据库,JDBC 和 C3P0 框架。
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); } }
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; } }
工具类 : JdbcUtils
public class JdbcUtils { 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(); } } }
引入事务
案例中的转账涉及两个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; } }
所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。
JDBC中关于事务的操作的api
Connection接口的方法 | 作用 |
void setAutoCommit(false) | 禁用事务自动提交(改为手动) |
void commit(); | 提交事务 |
void rollback(); | 回滚事务 |
开启事务的注意点:
- 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个: service层开启事务的connection需要跟dao层访问数据库的connection保持一致
- 线程并发情况下, 每个线程只能操作各自的 connection
剑指JUC原理-15.ThreadLocal(中):https://developer.aliyun.com/article/1413656