什么是ThreadLocal?

        顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

从线程的角度看,就好像每一个线程都完全拥有该变量。

注意:ThreadLocal不是用来解决共享对象的多线程访问问题的。

     在多线程环境下,之所以会有并发问题,就是因为不同的线程会同时访问同一个共享变量,同时进行一系列的操作。

  1. //这个意思很简单,创建两个线程,a线程对全局变量+10,b线程对全局变量-10
  2. public class MultiThreadDemo {
  3. public static class Number {
  4. private int value = 0;
  5. public void increase() throws InterruptedException {
  6. //这个变量对于该线程属于局部变量
  7. value = 10;
  8. Thread.sleep(10);
  9. System.out.println("increase value: " + value);
  10. }
  11. public void decrease() throws InterruptedException {
  12. //同样这个变量对于该线程属于局部变量
  13. value = -10;
  14. Thread.sleep(10);
  15. System.out.println("decrease value: " + value);
  16. }
  17. }
  18. public static void main(String[] args) throws InterruptedException {
  19. final Number number = new Number();
  20. Thread a = new Thread(new Runnable() {
  21. @Override
  22. public void run() {
  23. try {
  24. number.increase();
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. });
  30. Thread b = new Thread(new Runnable() {
  31. @Override
  32. public void run() {
  33. try {
  34. number.decrease();
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. });
  40. a.start();
  41. b.start();
  42. }
  43. }

思考:可能运行的结果:

  1. /*运行结果(一种可能)
  2. increase value: -10
  3. decrease value: -10
  4. *
  5. *你或许在想不对啊,按常理不是一个输出10,一个输出-10嘛
  6. *原因分析:
  7. *其实很简单,就是当a执行value = 10时,还没有等到下面输出,这个时候
  8. * b线程获得cpu执行权value = -10;这个时候a在获得cpu执行权的时候输出当然是-10。
  9. * 这里的根本原因是线程的赋值和输出一起不是原子性的。
  10. */

运行结果

为了验证我上面的原因分析,我修改下代码:

  1. public void decrease() throws InterruptedException {
  2. //我在decrease()新添加这个输出,看下输出结果
  3. System.out.println("increase value: " + value);
  4. value = -10;
  5. Thread.sleep(10);
  6. System.out.println("decrease value: " + value);
  7. }

再看运行结果:(和上面分析的一样)

思考:如果在 private volatile  int value = 0;在这里加上volatile关键字结果如何?

  1. /*结果会和上面没有任何区别,为什么
  2. *volatile的特点是保证可见性,但不保证原子性,你这a获得cpu改成value = 10,
  3. *这个时候b获得线程,它是知道value变成10了,但不影响它在把值赋值成-10。
  4. */

volatile结果

所以总的来说:

      a线程和b线程会操作同一个 number 中 value,那么输出的结果是不可预测的,因为当前线程修改变量之后但是还没输出的时候,变量有可能被另外一个线程修改.

当如如果要保证输出我当前线程的值呢?

     其实也很简单:在 increase() 和 decrease() 方法上加上 synchronized 关键字进行同步,这种做法其实是将 value 的 赋值 和 打印 包装成了一个原子操作,也就是说两者要么同时进行,要不都不进行,中间不会有额外的操作。

 

     上面的例子我们可以看到a线程操作全局变量,b在去去全局成员变量是a已经修改过的。

      如果我们需要 value 只属于 increase 线程或者 decrease 线程,而不是被两个线程共享,那么也不会出现竞争问题。

     很简单,为每一个线程定义一份只属于自己的局部变量。

  1. public void increase() throws InterruptedException {
  2. //为每一个线程定义一个局部变量,这样当然就是线程私有的
  3. int value = 10;
  4. Thread.sleep(10);
  5. System.out.println("increase value: " + value);
  6. }

    不论 value 值如何改变,都不会影响到其他线程,因为在每次调用 increase 方法时,都会创建一个 value 变量,该变量只对当前调用 increase 方法的线程可见。

    借助于上面这种思想,我们可以创建一个map,将当前线程的 id 作为 key,副本变量作为 value 值,下面是一个实现

  1. public class SimpleImpl {
  2. //这个相当于工具类
  3. public static class CustomThreadLocal {
  4. //创建一个Map
  5. private Map<Long, Integer> cacheMap = new HashMap<>();
  6. private int defaultValue ;
  7. public CustomThreadLocal(int value) {
  8. defaultValue = value;
  9. }
  10. //进行封装一层,其实就是通过key得到value
  11. public Integer get() {
  12. long id = Thread.currentThread().getId();
  13. if (cacheMap.containsKey(id)) {
  14. return cacheMap.get(id);
  15. }
  16. return defaultValue;
  17. }
  18. //同样存放key,value
  19. public void set(int value) {
  20. long id = Thread.currentThread().getId();
  21. cacheMap.put(id, value);
  22. }
  23. }
  24. //这个类引用工具类,当然也可以在这里写map。
  25. public static class Number {
  26. private CustomThreadLocal value = new CustomThreadLocal(0);
  27. public void increase() {
  28. value.set(10);
  29. try {
  30. Thread.sleep(10);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. System.out.println("increase value: " + value.get());
  35. }
  36. public void decrease() {
  37. value.set(-10);
  38. try {
  39. Thread.sleep(10);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println("decrease value: " + value.get());
  44. }
  45. }
  46. public static void main(String[] args) throws InterruptedException {
  47. final Number number = new Number();
  48. Thread a = new Thread(new Runnable() {
  49. @Override
  50. public void run() {
  51. number.increase();
  52. }
  53. });
  54. Thread b = new Thread(new Runnable() {
  55. @Override
  56. public void run() {
  57. number.decrease();
  58. }
  59. });
  60. a.start();
  61. b.start();
  62. }
  63. }

思考,运行结果如何?

  1. //运行结果(其中一种):
  2. increase value: 0
  3. decrease value: -10

      按照常理来讲应该是一个10,一个-10,怎么都想不通会出现0,也没有想明白是哪个地方引起的这个线程不同步,毕竟我这里两个线程各放各的key和value值,而且key也不一样

为什么出现有一个不存在key值,而取出默认值0。

     其实原因就在HashMap是线程不安全的,并发的时候设置值,可能导致冲突,另一个没设置进去。如果这个改成Hashtable,就发现永远输出10和-10两个值

 

     其实上面的方式二实现的功能和ThreadLocal像,只不过ThreadLocal肯定更完美。

  1. public T get() { }
  2. public void set(T value) { }
  3. public void remove() { }
  4. protected T initialValue() { }

 

    get()方法:获取ThreadLocal在当前线程中保存的变量副本。

    set()方法:用来设置当前线程中变量的副本。

    remove()方法:用来移除当前线程中变量的副本。

    initialValue()方法:是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。

这里主要看get和set方法源码

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }
  9. public T get() {
  10. Thread t = Thread.currentThread();
  11. ThreadLocalMap map = getMap(t);
  12. if (map != null) {
  13. ThreadLocalMap.Entry e = map.getEntry(this);
  14. if (e != null) {
  15. @SuppressWarnings("unchecked")
  16. T result = (T)e.value;
  17. return result;
  18. }
  19. }
  20. return setInitialValue();
  21. }

通过这个可以总结出:

  (1)get和set底层还是一个ThreadLocalMap实现存取值

  (2)我们在放的时候只放入value值,那么它的key其实就是ThreadLocal类的实例对象(也就是当前线程对象)

  1. public class Test {
  2. //创建两个ThreadLocal对象
  3. ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
  4. ThreadLocal<String> stringLocal = new ThreadLocal<String>();
  5. public static void main(String[] args) throws InterruptedException {
  6. final Test test = new Test();
  7. ExecutorService executors= Executors.newFixedThreadPool(2);
  8. executors.execute(new Runnable() {
  9. @Override
  10. public void run() {
  11. test.longLocal.set(Thread.currentThread().getId());
  12. test.stringLocal.set(Thread.currentThread().getName());
  13. System.out.println(test.longLocal.get());
  14. System.out.println(test.stringLocal.get());
  15. }
  16. });
  17. executors.execute(new Runnable() {
  18. @Override
  19. public void run() {
  20. test.longLocal.set(Thread.currentThread().getId());
  21. test.stringLocal.set(Thread.currentThread().getName());
  22. System.out.println(test.longLocal.get());
  23. System.out.println(test.stringLocal.get());
  24. }
  25. });
  26. }
  27. }

思考,运行结果如何?

  1. //运行结果(其中一种可能)
  2. 11
  3. 10
  4. pool-1-thread-2
  5. pool-1-thread-1
  6. //说明已经实现了共享变量私有

运行结果

 

      最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

    同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

  1. public class ConnectionManager {
  2. private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
  3. @Override
  4. protected Connection initialValue() {
  5. Connection conn = null;
  6. try {
  7. conn = DriverManager.getConnection(
  8. "jdbc:mysql://localhost:3306/test", "username",
  9. "password");
  10. } catch (SQLException e) {
  11. e.printStackTrace();
  12. }
  13. return conn;
  14. }
  15. };
  16. public static Connection getConnection() {
  17. return connectionHolder.get();
  18. }
  19. public static void setConnection(Connection conn) {
  20. connectionHolder.set(conn);
  21. }
  22. }

     这样就保证了一个线程对应一个数据库连接,保证了事务。因为事务是依赖一个连接来控制的,如commit,rollback,都是数据库连接的方法。

  1. private static final ThreadLocal threadSession = new ThreadLocal();
  2. public static Session getSession() throws InfrastructureException {
  3. Session s = (Session) threadSession.get();
  4. try {
  5. if (s == null) {
  6. s = getSessionFactory().openSession();
  7. threadSession.set(s);
  8. }
  9. } catch (HibernateException ex) {
  10. throw new InfrastructureException(ex);
  11. }
  12. return s;
  13. }

 

     1、【Java 并发】详解 ThreadLocal 

     2、Java并发编程:深入剖析ThreadLocal 

     3、对ThreadLocal中的key和value 

 

想太多,做太少,中间的落差就是烦恼。想没有烦恼,要么别想,要么多做。少校【12】

 

版权声明:本文为qdhxhz原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/qdhxhz/p/9201038.html