二.多线程设计模式篇-2.7 ThreadLocal

1.ThreadLocal是什么

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富:

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 线程间数据隔离
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,Session会话管理。

现在相信你已经对ThreadLocal有一个大致的认识了,下面我们看看如何用?

2.ThreadLocal怎么用

既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下
在这里插入图片描述
从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。

这就是TheadLocal的基本使用,是不是非常的简单。那么为什么会在数据库连接的时候使用的比较多呢?
在这里插入图片描述
上面是一个数据库连接的管理类,我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。

这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。是不是很好用。

以上主要是讲解了一个基本的案例,然后还分析了为什么在数据库连接的时候会使用ThreadLocal。下面我们从源码的角度来分析一下,ThreadLocal的工作原理。

3.ThreadLocal源码分析

在最开始的例子中,只给出了两个方法也就是get和set方法,其实还有几个需要我们注意。
在这里插入图片描述
方法这么多,我们主要来看set,然后就能认识到整体的ThreadLocal了:

  • set方法
    在这里插入图片描述
    从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。

OK,到这一步了,相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。
在这里插入图片描述
我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

还有一个getMap

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。

  • get方法
    在这里插入图片描述
    通过上面ThreadLocal的介绍相信你对这个方法能够很好的理解了,首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。

如何设置一个初始值呢?
在这里插入图片描述
原理很简单

  • remove方法
    在这里插入图片描述
    从我们的map移除即可。

OK,其实内部源码很简单,现在我们总结一波

(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal来说关键就是内部的ThreadLocalMap。

4.ThreadLocal其他几个注意的点

只要是介绍ThreadLocal的文章都会帮大家认识一个点,那就是内存泄漏问题。我们先来看下面这张图。
在这里插入图片描述
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

  • Thread中有一个map,就是ThreadLocalMap
  • ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
  • ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
  • 重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
  • 解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
  • 忘记了remove操作,ThreadLocal get/set方法也作了优化,会遍历entry key为null,然后释放
  • 为什么要用软引用,而不用强引用,这是因为在remove/get/set,需要识别entry key为null
  • ThreadLocalMap使用开发地址法,HashMap使用拉链法(也叫作链接法、链地址法,一个意思),另外一种为再散列法

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第3章 多线程(二) Java 高级程序设计 Java高级程序设计-线程(二)全文共34页,当前为第1页。 回顾 进程一般代表一个应用程序,一个进程中可以包含多个线程。 合理使用多线程能够提高程序的执行效率,处理高并发应用。 线程的创建有继承Thread类和实现Runnable接口两种方式,通过Runnable方式可以更加容易实现多线程之间资源共享。 通过sleep可以使线程进入休眠状态,通过join方法可以让线程处于等待,其他线程执行完毕后继续执行。 线程生命周期包括:新建 就绪 运行 阻塞 死亡5种状态。 Java高级程序设计-线程(二)全文共34页,当前为第2页。 本章内容 掌握同步代码块的使用 掌握同步方法的使用 理解线程死锁 掌握 ThreadLocal 类的使用 使用多线程模拟猴子采花 使用同步方法模拟购票 使用多线程模拟购物订单生成 使用 ThreadLocal 类模拟银行取款 Java高级程序设计-线程(二)全文共34页,当前为第3页。 3.1 同步代码块 线程安全问题 同步代码块的使用 使用多线程模拟猴子采花 20 25 Java高级程序设计-线程(二)全文共34页,当前为第4页。 3.1 线程安全 多线程编程时,由于系统对线程的调度具有一定的随机性,所以,使用多个线程操作同一个数据时,容易出现线程安全问题。 当多个线程访问同一个资源时,如果控制不好,也会造成数据的不正确性。 以银行取钱为例: 用户输入账户、密码,系统判断用户的账户、密码是否匹配 用户输入取款金额 系统判断账户余额是否大于取款金额 如果余额大于等于取款金额,则取款成功,否则取款失败 Java高级程序设计-线程(二)全文共34页,当前为第5页。 3.1.1 模拟银行取款 使用多线程并发模拟两个账户并发取钱的问题: 创建账户类(Account),用于封装用户的账号和余额 public class Account { // 用户账号 private String no; // 账户中余额 private double balance; public Account() { } // 构造方法用于初始化账户、余额 public Account(String no, double balance) { this.no = no; this.balance = balance; } //getter和setter省略 Java高级程序设计-线程(二)全文共34页,当前为第6页。 3.1.1 模拟银行取款 创建模拟两个线程的取款类 DrawThread,该类继承 Thread 类。取钱的业务逻辑为当余额不足时无法提取现金,当余额足够时系统吐出钞票,减少余额 public class DrawThread extends Thread { // 模拟用户账户 private Account account; // 当前线程索取钱数 private double drawAccount; //完成数据初始化工作 public DrawThread(String name, Account account, double drawAccount) { super(name); this.account = account; this.drawAccount = drawAccount; } public void run() { // 账户余额大于取钱数据 if (account.getBalance() >= drawAccount) { System.out.println(this.getName() + "\t 取款成功 ! 吐钞 :" + drawAccount); // 修改余额 account.setBalance(account.getBalance() - drawAccount); System.out.println("\t 余额 : " + account.getBalance()); } else { System.out.println(this.getName() + " 取钱失败!余额不足 "); } } } // 当多个线程同时修改同一个共享数据时,将涉及数据安全问题 Java高级程序设计-线程(二)全文共34页,当前为第7页。 3.1.1 模拟银行取款 由于多线程并发问题,一个线程执行余额操作可能未完毕,另外一个线程读取或者也在操作余额,必然会引起数据的不准确性。 这个时候需要在线程中加入对数据的保护机制,从而达到防止并发引起的数据不准确。 Java高级程序设计-线程(二)全文共34页,当前为第8页。 3.1.2 同步代码块的使用 Java中多线程中引入了同步监视器,使用同步监视器的常用方式是使用同步代码块,保

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值