• java多线程(6)---ThreadLocal


    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】

  • 相关阅读:
    基于ZYNQ SOC视频处理的常规设计
    Alinx黑金没有实现利用一个VDMA完成视频流读写的例程
    Vitis软件平台、vitis实例、裸机SOC(SDK)程序移植
    ZYNQ PS端IIC接口使用-笔记
    用信号量实现生产者&消费者模型
    C语言 -- 内存对齐
    排序算法---希尔排序
    排序算法---直接插入排序
    网络套接字编程介绍---UDP通信
    C++实现__搜索二叉树
  • 原文地址:https://www.cnblogs.com/qdhxhz/p/9201038.html
Copyright © 2020-2023  润新知