ThreadLocal
什么是 ThreadLocal?
顾名思义它是 local variable(线程局部变量). 它的功用非常简单, 就是为每一个使用该变量的线程都提供一个变量值的副本, 是每一个线程都可以独立地改变自己的副本, 而不会和其它线程的副本冲突.
从线程的角度看, 就好像每一个线程都完全拥有该变量.
注意: ThreadLocal 不是用来解决共享对象的多线程访问问题的.
一, 多线程共享成员变量
在多线程环境下, 之所以会有并发问题, 就是因为不同的线程会同时访问同一个共享变量, 同时进行一系列的操作.
1, 例如下面的形式
- // 这个意思很简单, 创建两个线程, a 线程对全局变量 + 10,b 线程对全局变量 - 10
- public class MultiThreadDemo {
- public static class Number {
- private int value = 0;
- public void increase() throws InterruptedException {
- // 这个变量对于该线程属于局部变量
- value = 10;
- Thread.sleep(10);
- System.out.println("increase value:" + value);
- }
- public void decrease() throws InterruptedException {
- // 同样这个变量对于该线程属于局部变量
- value = -10;
- Thread.sleep(10);
- System.out.println("decrease value:" + value);
- }
- }
- public static void main(String[] args) throws InterruptedException {
- final Number number = new Number();
- Thread a = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- number.increase();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- Thread b = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- number.decrease();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- a.start();
- b.start();
- }
- }
思考: 可能运行的结果:
/* 运行结果 (一种可能)
increase value: -10
decrease value: -10
*
* 你或许在想不对啊, 按常理不是一个输出 10, 一个输出 - 10 嘛
* 原因分析:
* 其实很简单, 就是当 a 执行 value = 10 时, 还没有等到下面输出, 这个时候
* b 线程获得 cpu 执行权 value = -10; 这个时候 a 在获得 cpu 执行权的时候输出当然是 - 10.
* 这里的根本原因是线程的赋值和输出一起不是原子性的.
*/
运行结果
为了验证我上面的原因分析, 我修改下代码:
- public void decrease() throws InterruptedException {
- // 我在 decrease() 新添加这个输出, 看下输出结果
- System.out.println("increase value:" + value);
- value = -10;
- Thread.sleep(10);
- System.out.println("decrease value:" + value);
- }
再看运行结果:(和上面分析的一样)
思考: 如果在 private volatile int value = 0; 在这里加上 volatile 关键字结果如何?
/* 结果会和上面没有任何区别, 为什么
*volatile 的特点是保证可见性, 但不保证原子性, 你这 a 获得 cpu 改成 value = 10,
* 这个时候 b 获得线程, 它是知道 value 变成 10 了, 但不影响它在把值赋值成 - 10.
*/
volatile 结果
所以总的来说:
a 线程和 b 线程会操作同一个 number 中 value, 那么输出的结果是不可预测的, 因为当前线程修改变量之后但是还没输出的时候, 变量有可能被另外一个线程修改.
当如如果要保证输出我当前线程的值呢?
其实也很简单: 在 increase() 和 decrease() 方法上加上 synchronized 关键字进行同步, 这种做法其实是将 value 的 赋值 和 打印 包装成了一个原子操作, 也就是说两者要么同时进行, 要不都不进行, 中间不会有额外的操作.
二, 多线程不共享全局变量
上面的例子我们可以看到 a 线程操作全局变量, b 在去去全局成员变量是 a 已经修改过的.
如果我们需要 value 只属于 increase 线程或者 decrease 线程, 而不是被两个线程共享, 那么也不会出现竞争问题.
1, 方式一
很简单, 为每一个线程定义一份只属于自己的局部变量.
- public void increase() throws InterruptedException {
- // 为每一个线程定义一个局部变量, 这样当然就是线程私有的
- int value = 10;
- Thread.sleep(10);
- System.out.println("increase value:" + value);
- }
不论 value 值如何改变, 都不会影响到其他线程, 因为在每次调用 increase 方法时, 都会创建一个 value 变量, 该变量只对当前调用 increase 方法的线程可见.
2, 方式二
借助于上面这种思想, 我们可以创建一个 map, 将当前线程的 id 作为 key, 副本变量作为 value 值, 下面是一个实现
- public class SimpleImpl {
- // 这个相当于工具类
- public static class CustomThreadLocal {
- // 创建一个 Map
- private Map<Long, Integer> cacheMap = new HashMap<>();
- private int defaultValue ;
- public CustomThreadLocal(int value) {
- defaultValue = value;
- }
- // 进行封装一层, 其实就是通过 key 得到 value
- public Integer get() {
- long id = Thread.currentThread().getId();
- if (cacheMap.containsKey(id)) {
- return cacheMap.get(id);
- }
- return defaultValue;
- }
- // 同样存放 key,value
- public void set(int value) {
- long id = Thread.currentThread().getId();
- cacheMap.put(id, value);
- }
- }
- // 这个类引用工具类, 当然也可以在这里写 map.
- public static class Number {
- private CustomThreadLocal value = new CustomThreadLocal(0);
- public void increase() {
- value.set(10);
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("increase value:" + value.get());
- }
- public void decrease() {
- value.set(-10);
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("decrease value:" + value.get());
- }
- }
- public static void main(String[] args) throws InterruptedException {
- final Number number = new Number();
- Thread a = new Thread(new Runnable() {
- @Override
- public void run() {
- number.increase();
- }
- });
- Thread b = new Thread(new Runnable() {
- @Override
- public void run() {
- number.decrease();
- }
- });
- a.start();
- b.start();
- }
- }
思考, 运行结果如何?
- // 运行结果 (其中一种):
- increase value: 0
- decrease value: -10
按照常理来讲应该是一个 10, 一个 - 10, 怎么都想不通会出现 0, 也没有想明白是哪个地方引起的这个线程不同步, 毕竟我这里两个线程各放各的 key 和 value 值, 而且 key 也不一样
为什么出现有一个不存在 key 值, 而取出默认值 0.
其实原因就在 HashMap 是线程不安全的, 并发的时候设置值, 可能导致冲突, 另一个没设置进去. 如果这个改成 Hashtable, 就发现永远输出 10 和 - 10 两个值.
三, ThreadLocal
其实上面的方式二实现的功能和 ThreadLocal 像, 只不过 ThreadLocal 肯定更完美.
1, 了解 ThreadLocal 类提供的几个方法
- public T get() { }
- public void set(T value) { }
- public void remove() { }
- protected T initialValue() { }
get() 方法: 获取 ThreadLocal 在当前线程中保存的变量副本.
set() 方法: 用来设置当前线程中变量的副本.
remove() 方法: 用来移除当前线程中变量的副本.
initialValue() 方法: 是一个 protected 方法, 一般是用来在使用时进行重写的, 它是一个延迟加载方法, 下面会详细说明.
这里主要看 get 和 set 方法源码
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
- }
通过这个可以总结出:
(1)get 和 set 底层还是一个 ThreadLocalMap 实现存取值
(2) 我们在放的时候只放入 value 值, 那么它的 key 其实就是 ThreadLocal 类的实例对象 (也就是当前线程对象)
2, 小案例
- public class Test {
- // 创建两个 ThreadLocal 对象
- ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
- ThreadLocal<String> stringLocal = new ThreadLocal<String>();
- public static void main(String[] args) throws InterruptedException {
- final Test test = new Test();
- ExecutorService executors= Executors.newFixedThreadPool(2);
- executors.execute(new Runnable() {
- @Override
- public void run() {
- test.longLocal.set(Thread.currentThread().getId());
- test.stringLocal.set(Thread.currentThread().getName());
- System.out.println(test.longLocal.get());
- System.out.println(test.stringLocal.get());
- }
- });
- executors.execute(new Runnable() {
- @Override
- public void run() {
- test.longLocal.set(Thread.currentThread().getId());
- test.stringLocal.set(Thread.currentThread().getName());
- System.out.println(test.longLocal.get());
- System.out.println(test.stringLocal.get());
- }
- });
- }
- }
思考, 运行结果如何?
- // 运行结果 (其中一种可能)
- 11
- 10
- pool-1-thread-2
- pool-1-thread-1
- // 说明已经实现了共享变量私有
运行结果
四, ThreadLocal 的应用场景
最常见的 ThreadLocal 使用场景为 用来解决 数据库连接, Session 管理等.
1, 数据库连接管理
同一事务多 DAO 共享同一 Connection, 必须在一个共同的外部类使用 ThreadLocal 保存 Connection.
- public class ConnectionManager {
- private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
- @Override
- protected Connection initialValue() {
- Connection conn = null;
- try {
- conn = DriverManager.getConnection(
- "jdbc:mysql://localhost:3306/test", "username",
- "password");
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return conn;
- }
- };
- public static Connection getConnection() {
- return connectionHolder.get();
- }
- public static void setConnection(Connection conn) {
- connectionHolder.set(conn);
- }
- }
这样就保证了一个线程对应一个数据库连接, 保证了事务. 因为事务是依赖一个连接来控制的, 如 commit,rollback, 都是数据库连接的方法.
2,Session 管理
- private static final ThreadLocal threadSession = new ThreadLocal();
- public static Session getSession() throws InfrastructureException {
- Session s = (Session) threadSession.get();
- try {
- if (s == null) {
- s = getSessionFactory().openSession();
- threadSession.set(s);
- }
- } catch (HibernateException ex) {
- throw new InfrastructureException(ex);
- }
- return s;
- }
参考
1,[Java 并发] 详解 ThreadLocal
2,Java 并发编程: 深入剖析 ThreadLocal
3, 对 ThreadLocal 中的 key 和 value
想太多, 做太少, 中间的落差就是烦恼. 想没有烦恼, 要么别想, 要么多做. 少校 [12]
来源: https://www.cnblogs.com/qdhxhz/p/9201038.html