侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Java多线程编程中ThreadLocal类的用法及深入

Java  /  管理员 发布于 8年前   189

ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名。

早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用。其实它还是挺有用的,不相信的话,我们一起来看看这个例子吧。

一个序列号生成器的程序,可能同时会有多个线程并发访问它,要保证每个线程得到的序列号都是自增的,而不能相互干扰。

先定义一个接口:

public interface Sequence {  int getNumber();}

每次调用 getNumber() 方法可获取一个序列号,下次再调用时,序列号会自增。

再做一个线程类:

public class ClientThread extends Thread {  private Sequence sequence;  public ClientThread(Sequence sequence) {    this.sequence = sequence;  }  @Override  public void run() {    for (int i = 0; i < 3; i++) {      System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber());    }  }}

在线程中连续输出三次线程名与其对应的序列号。

我们先不用 ThreadLocal,来做一个实现类吧。

public class SequenceA implements Sequence {  private static int number = 0;  public int getNumber() {    number = number + 1;    return number;  }  public static void main(String[] args) {    Sequence sequence = new SequenceA();    ClientThread thread1 = new ClientThread(sequence);    ClientThread thread2 = new ClientThread(sequence);    ClientThread thread3 = new ClientThread(sequence);    thread1.start();    thread2.start();    thread3.start();  }}

序列号初始值是0,在 main() 方法中模拟了三个线程,运行后结果如下:

Thread-0 => 1Thread-0 => 2Thread-0 => 3Thread-2 => 4Thread-2 => 5Thread-2 => 6Thread-1 => 7Thread-1 => 8Thread-1 => 9

由于线程启动顺序是随机的,所以并不是0、1、2这样的顺序,这个好理解。为什么当 Thread-0 输出了1、2、3之后,而 Thread-2 却输出了4、5、6呢?线程之间竟然共享了 static 变量!这就是所谓的“非线程安全”问题了。

那么如何来保证“线程安全”呢?对应于这个案例,就是说不同的线程可拥有自己的 static 变量,如何实现呢?下面看看另外一个实现吧。

public class SequenceB implements Sequence {  private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {    @Override    protected Integer initialValue() {      return 0;    }  };  public int getNumber() {    numberContainer.set(numberContainer.get() + 1);    return numberContainer.get();  }  public static void main(String[] args) {    Sequence sequence = new SequenceB();    ClientThread thread1 = new ClientThread(sequence);    ClientThread thread2 = new ClientThread(sequence);    ClientThread thread3 = new ClientThread(sequence);    thread1.start();    thread2.start();    thread3.start();  }}

通过 ThreadLocal 封装了一个 Integer 类型的 numberContainer 静态成员变量,并且初始值是0。再看 getNumber() 方法,首先从 numberContainer 中 get 出当前的值,加1,随后 set 到 numberContainer 中,最后将 numberContainer 中 get 出当前的值并返回。

是不是很恶心?但是很强大!确实稍微饶了一下,我们不妨把 ThreadLocal 看成是一个容器,这样理解就简单了。所以,这里故意用 Container 这个单词作为后缀来命名 ThreadLocal 变量。

运行结果如何呢?看看吧。

Thread-0 => 1Thread-0 => 2Thread-0 => 3Thread-2 => 1Thread-2 => 2Thread-2 => 3Thread-1 => 1Thread-1 => 2Thread-1 => 3

每个线程相互独立了,同样是 static 变量,对于不同的线程而言,它没有被共享,而是每个线程各一份,这样也就保证了线程安全。 也就是说,TheadLocal 为每一个线程提供了一个独立的副本!

搞清楚 ThreadLocal 的原理之后,有必要总结一下 ThreadLocal 的 API,其实很简单。

  • public void set(T value):将值放入线程局部变量中
  • public T get():从线程局部变量中获取值
  • public void remove():从线程局部变量中移除值(有助于 JVM 垃圾回收)
  • protected T initialValue():返回线程局部变量中的初始值(默认为 null)

为什么 initialValue() 方法是 protected 的呢?就是为了提醒程序员们,这个方法是要你们来实现的,请给这个线程局部变量一个初始值吧。

了解了原理与这些 API,其实想想 ThreadLocal 里面不就是封装了一个 Map 吗?自己都可以写一个 ThreadLocal 了,尝试一下吧。

public class MyThreadLocal<T> {  private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>());  public void set(T value) {    container.put(Thread.currentThread(), value);  }  public T get() {    Thread thread = Thread.currentThread();    T value = container.get(thread);    if (value == null && !container.containsKey(thread)) {      value = initialValue();      container.put(thread, value);    }    return value;  }  public void remove() {    container.remove(Thread.currentThread());  }  protected T initialValue() {    return null;  }}

以上完全山寨了一个 ThreadLocal,其中中定义了一个同步 Map(为什么要这样?请读者自行思考),代码应该非常容易读懂。
下面用这 MyThreadLocal 再来实现一把看看。

public class SequenceC implements Sequence {  private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() {    @Override    protected Integer initialValue() {      return 0;    }  };  public int getNumber() {    numberContainer.set(numberContainer.get() + 1);    return numberContainer.get();  }  public static void main(String[] args) {    Sequence sequence = new SequenceC();    ClientThread thread1 = new ClientThread(sequence);    ClientThread thread2 = new ClientThread(sequence);    ClientThread thread3 = new ClientThread(sequence);    thread1.start();    thread2.start();    thread3.start();  }}

 以上代码其实就是将 ThreadLocal 替换成了 MyThreadLocal,仅此而已,运行效果和之前的一样,也是正确的。

其实 ThreadLocal 可以单独成为一种设计模式,就看你怎么看了。

ThreadLocal 具体有哪些使用案例呢?

我想首先要说的就是:通过 ThreadLocal 存放 JDBC Connection,以达到事务控制的能力。

还是保持我一贯的 Style,用一个 Demo 来说话吧。用户提出一个需求:当修改产品价格的时候,需要记录操作日志,什么时候做了什么事情。

想必这个案例,只要是做过应用系统的小伙伴们,都应该遇到过吧?无外乎数据库里就两张表:product 与 log,用两条 SQL 语句应该可以解决问题:

update product set price = ? where id = ?insert into log (created, description) values (?, ?)

But!要确保这两条 SQL 语句必须在同一个事务里进行提交,否则有可能 update 提交了,但 insert 却没有提交。如果这样的事情真的发生了,我们肯定会被用户指着鼻子狂骂:“为什么产品价格改了,却看不到什么时候改的呢?”。

聪明的我在接到这个需求以后,是这样做的:

首先,我写一个 DBUtil 的工具类,封装了数据库的常用操作:

public class DBUtil {  // 数据库配置  private static final String driver = "com.mysql.jdbc.Driver";  private static final String url = "jdbc:mysql://localhost:3306/demo";  private static final String username = "root";  private static final String password = "root";  // 定义一个数据库连接  private static Connection conn = null;  // 获取连接  public static Connection getConnection() {    try {      Class.forName(driver);      conn = DriverManager.getConnection(url, username, password);    } catch (Exception e) {      e.printStackTrace();    }    return conn;  }  // 关闭连接  public static void closeConnection() {    try {      if (conn != null) {        conn.close();      }    } catch (Exception e) {      e.printStackTrace();    }  }}

里面搞了一个 static 的 Connection,这下子数据库连接就好操作了,牛逼吧!

然后,我定义了一个接口,用于给逻辑层来调用:

public interface ProductService {  void updateProductPrice(long productId, int price);}

根据用户提出的需求,我想这个接口完全够用了。根据 productId 去更新对应 Product 的 price,然后再插入一条数据到 log 表中。

其实业务逻辑也不太复杂,于是我快速地完成了 ProductService 接口的实现类:

public class ProductServiceImpl implements ProductService {  private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?";  private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)";  public void updateProductPrice(long productId, int price) {    try {      // 获取连接      Connection conn = DBUtil.getConnection();      conn.setAutoCommit(false); // 关闭自动提交事务(开启事务)      // 执行操作      updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新产品      insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日志      // 提交事务      conn.commit();    } catch (Exception e) {      e.printStackTrace();    } finally {      // 关闭连接      DBUtil.closeConnection();    }  }  private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception {    PreparedStatement pstmt = conn.prepareStatement(updateProductSQL);    pstmt.setInt(1, productPrice);    pstmt.setLong(2, productId);    int rows = pstmt.executeUpdate();    if (rows != 0) {      System.out.println("Update product success!");    }  }  private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception {    PreparedStatement pstmt = conn.prepareStatement(insertLogSQL);    pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));    pstmt.setString(2, logDescription);    int rows = pstmt.executeUpdate();    if (rows != 0) {      System.out.println("Insert log success!");    }  }}

代码的可读性还算不错吧?这里我用到了 JDBC 的高级特性 Transaction 了。暗自庆幸了一番之后,我想是不是有必要写一个客户端,来测试一下执行结果是不是我想要的呢? 于是我偷懒,直接在 ProductServiceImpl 中增加了一个 main() 方法:

public static void main(String[] args) {  ProductService productService = new ProductServiceImpl();  productService.updateProductPrice(1, 3000);}

我想让 productId 为 1 的产品的价格修改为 3000。于是我把程序跑了一遍,控制台输出:

Update product success!Insert log success!

应该是对了。作为一名专业的程序员,为了万无一失,我一定要到数据库里在看看。没错!product 表对应的记录更新了,log 表也插入了一条记录。这样就可以将 ProductService 接口交付给别人来调用了。

几个小时过去了,QA 妹妹开始骂我:“我靠!我才模拟了 10 个请求,你这个接口怎么就挂了?说是数据库连接关闭了!”。

听到这样的叫声,让我浑身打颤,立马中断了我的小视频,赶紧打开 IDE,找到了这个 ProductServiceImpl 这个实现类。好像没有 Bug 吧?但我现在不敢给她任何回应,我确实有点怕她的。

我突然想起,她是用工具模拟的,也就是模拟多个线程了!那我自己也可以模拟啊,于是我写了一个线程类:

public class ClientThread extends Thread {  private ProductService productService;  public ClientThread(ProductService productService) {    this.productService = productService;  }  @Override  public void run() {    System.out.println(Thread.currentThread().getName());    productService.updateProductPrice(1, 3000);  }}

我用这线程去调用 ProduceService 的方法,看看是不是有问题。此时,我还要再修改一下 main() 方法:

// public static void main(String[] args) {//   ProductService productService = new ProductServiceImpl();//   productService.updateProductPrice(1, 3000);// }  public static void main(String[] args) {  for (int i = 0; i < 10; i++) {    ProductService productService = new ProductServiceImpl();    ClientThread thread = new ClientThread(productService);    thread.start();  }}

我也模拟 10 个线程吧,我就不信那个邪了!

运行结果真的让我很晕、很晕:

Thread-1Thread-3Thread-5Thread-7Thread-9Thread-0Thread-2Thread-4Thread-6Thread-8Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)at java.lang.reflect.Constructor.newInstance(Constructor.java:513)at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)at com.mysql.jdbc.Util.getInstance(Util.java:386)at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)

我靠!竟然在多线程的环境下报错了,果然是数据库连接关闭了。怎么回事呢?我陷入了沉思中。于是我 Copy 了一把那句报错信息,在百度、Google,还有 OSC 里都找了,解答实在是千奇百怪。

我突然想起,既然是跟 Connection 有关系,那我就将主要精力放在检查 Connection 相关的代码上吧。是不是 Connection 不应该是 static 的呢?我当初设计成 static 的主要是为了让 DBUtil 的 static 方法访问起来更加方便,用 static 变量来存放 Connection 也提高了性能啊。怎么搞呢?

于是我看到了 OSC 上非常火爆的一篇文章《ThreadLocal 那点事儿》,终于才让我明白了!原来要使每个线程都拥有自己的连接,而不是共享同一个连接,否则线程1有可能会关闭线程2的连接,所以线程2就报错了。一定是这样!

我赶紧将 DBUtil 给重构了:

public class DBUtil {  // 数据库配置  private static final String driver = "com.mysql.jdbc.Driver";  private static final String url = "jdbc:mysql://localhost:3306/demo";  private static final String username = "root";  private static final String password = "root";  // 定义一个用于放置数据库连接的局部线程变量(使每个线程都拥有自己的连接)  private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();  // 获取连接  public static Connection getConnection() {    Connection conn = connContainer.get();    try {      if (conn == null) {        Class.forName(driver);        conn = DriverManager.getConnection(url, username, password);      }    } catch (Exception e) {      e.printStackTrace();    } finally {      connContainer.set(conn);    }    return conn;  }  // 关闭连接  public static void closeConnection() {    Connection conn = connContainer.get();    try {      if (conn != null) {        conn.close();      }    } catch (Exception e) {      e.printStackTrace();    } finally {      connContainer.remove();    }  }}

我把 Connection 放到了 ThreadLocal 中,这样每个线程之间就隔离了,不会相互干扰了。

此外,在 getConnection() 方法中,首先从 ThreadLocal 中(也就是 connContainer 中) 获取 Connection,如果没有,就通过 JDBC 来创建连接,最后再把创建好的连接放入这个 ThreadLocal 中。可以把 ThreadLocal 看做是一个容器,一点不假。

同样,我也对 closeConnection() 方法做了重构,先从容器中获取 Connection,拿到了就 close 掉,最后从容器中将其 remove 掉,以保持容器的清洁。

这下应该行了吧?我再次运行 main() 方法:

Thread-0Thread-2Thread-4Thread-6Thread-8Thread-1Thread-3Thread-5Thread-7Thread-9Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!Update product success!Insert log success!

总算是解决了

您可能感兴趣的文章:

  • 简单分析Java线程编程中ThreadLocal类的使用
  • 实例讲解Java并发编程之ThreadLocal类
  • Java 中ThreadLocal类详解
  • 快速了解Java中ThreadLocal类
  • 深入解析Java中ThreadLocal线程类的作用和用法
  • Java ThreadLocal用法实例详解
  • java线程本地变量ThreadLocal详解
  • java 中ThreadLocal 的正确用法
  • Java多线程编程之ThreadLocal线程范围内的共享变量
  • Java 并发编程之ThreadLocal详解及实例
  • Java ThreadLocal类应用实战案例分析


  • 上一条:
    java和php区别大吗?
    下一条:
    为Java程序员准备的10分钟Perl教程
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在java中实现的脱敏工具类代码示例分享(0个评论)
    • zookeeper安装流程步骤(0个评论)
    • 在java中你背的“八股文”可能已经过时了(2个评论)
    • 在php8.0+版本中使用属性来增加值代码示例(3个评论)
    • java 正则表达式基础,实例学习资料收集大全 原创(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-11
    • 2018-03
    • 2020-03
    • 2023-05
    • 2023-11
    • 2024-01
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客