zoukankan      html  css  js  c++  java
  • volatile 和 ThreadLocal

    一   volatile 关键字

    背景: 为了提高程序的运行效率,编译器会自动对其进行优化,把经常访问的变量缓存起来,程序在读取这些变量时,有可能会直接从缓存中来读取,而不会去内存中读取,这么做的好处是提高了程序效率,不过也有缺陷,当遇到多线程时,会遇到读取数据不一致的情况。

    用途:volatile 是1个类型修饰符,它是被设计来修饰被多线程访问和修改的变量

        被volatile 修饰的变量,系统每次用到它时都是直接从对应内存中提取,而不会利用缓存。即保证了值在不同线程中的可见性(大家看到的都是一个值 内存中的值)

    注意:volatile 只保证了变量的可读性,它其实不是线程安全的,不保证原子性。称为最轻量的同步机制。

    二   ThreadLocal  线程变量

            ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。

    跳出误区

     需要重点强调的的是,不要拿ThreadLocal和synchronized做类比,因为这种比较压根就是无意义的!sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。而ThreadLocal从本质上讲,无非是提供了一个“线程级”变量作用域,它是一种线程封闭(每个线程独享变量)技术,更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。

      没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。

    ThreadLocal类使用的4个方法

    1   void set(Object value); // 设置当前线程的线程局部变量的值。

    2   public Object get(); // 该方法返回当前线程所对应的线程局部变量。

    3   public void remove(); // 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,
    对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

    4   protected Object initialValue();// 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
    这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null

    来看个简单的例子

      假设我们要为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了.(当然下面这个例子没有完全体现出跨层级跨方法的调用,理解就可以了)

    package concurrent;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * Created by chengxiao on 2016/12/12.
     */
    public class ThreadLocalDemo {
        public static void main(String []args){
            for(int i=0;i<5;i++){
                final Thread t = new Thread(){
                    @Override
                    public void run(){
                        System.out.println("当前线程:"+Thread.currentThread().getName()+",已分配ID:"+ThreadId.get());
                    }
                };
                t.start();
            }
        }
        static   class ThreadId{
            //一个递增的序列,使用AtomicInger原子变量保证线程安全
            private static final AtomicInteger nextId = new AtomicInteger(0);
            //线程本地变量,为每个线程关联一个唯一的序号
            private static final ThreadLocal<Integer> threadId =
                    new ThreadLocal<Integer>() {
                        @Override
                        protected Integer initialValue() {
                            return nextId.getAndIncrement();//相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
                        }
                    };
    
           //返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
            public static int get() {
                return threadId.get();
            }
        }
    }

    执行结果,可以看到每个线程都分配到了一个唯一的ID,同时在此线程范围内的"任何地点",我们都可以通过ThreadId.get()这种方式直接获取。

    当前线程:Thread-4,已分配ID:1
    当前线程:Thread-0,已分配ID:0
    当前线程:Thread-2,已分配ID:3
    当前线程:Thread-1,已分配ID:4
    当前线程:Thread-3,已分配ID:2 

    set操作,为线程绑定变量

    public void set(T value) {
        Thread t = Thread.currentThread();//1.首先获取当前线程对象
            ThreadLocalMap map = getMap(t);//2.获取该线程对象的ThreadLocalMap
            if (map != null)
                map.set(this, value);//如果map不为空,执行set操作,以当前threadLocal对象为key,实际存储对象为value进行set操作
            else
                createMap(t, value);//如果map为空,则为该线程创建ThreadLocalMap
        }

    可以看到,ThreadLocal不过是个入口,真正的变量是绑定在线程上的。

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;//线程对象持有ThreadLocalMap的引用
    }

    下面给是Thread类中的定义,每个线程对象都拥有一个ThreadLocalMap对象

     ThreadLocal.ThreadLocalMap threadLocals = null;

    现在,我们能看出ThreadLocal的设计思想了:

    1.ThreadLocal仅仅是个变量访问的入口;

    2.每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;

    3.ThreadLocalMap以当前的threadlocal对象为key,以真正的存储对象为value。get时通过threadlocal实例就可以找到绑定在当前线程上的对象。

    乍看上去,这种设计确实有些绕。我们完全可以在设计成Map<Thread,T>这种形式,一个线程对应一个存储对象。ThreadLocal这样设计的目的主要有两个:

      一是可以保证当前线程结束时相关对象能尽快被回收;二是ThreadLocalMap中的元素会大大减少,我们都知道map过大更容易造成哈希冲突而导致性能变差。

    我们再来看看get方法

     public T get() {
         Thread t = Thread.currentThread();//1.首先获取当前线程
             ThreadLocalMap map = getMap(t);//2.获取线程的map对象
             if (map != null) {//3.如果map不为空,以threadlocal实例为key获取到对应Entry,然后从Entry中取出对象即可。
                 ThreadLocalMap.Entry e = map.getEntry(this);
                 if (e != null)
                     return (T)e.value;
             }
             return setInitialValue();//如果map为空,也就是第一次没有调用set直接get(或者调用过set,又调用了remove)时,为其设定初始值
         }

    ThreadLocal 3种实现demo

    • 法一.在用户类中定义一个ThreadLocal实例属性(通常是静态实例), 把ThreadLocal就当做一个map操作,该set就set,key不用指定,默认key就是ThreadLocal自身
    例1: 下面是hibernate文档里提供的辅助类HibernateUtil
    ThreadLocal 模式管理hibernate Session
    每个线程都能获得一个session的副本
    
    public class HibernateUtil {
    
        public static final ThreadLocal session =new ThreadLocal();
    
        public static final SessionFactory sessionFactory;
    
       static {
          try {
            sessionFactory = new Configuration().configure().buildSessionFactory();
          } catch (Throwable ex) {
               throw new ExceptionInInitializerError(ex);
          }   
            }
        
         public static Session currentSession () throws HibernateException {
            Session s = session.get ();
            if(s == null) {
              s = sessionFactory.openSession ();
              session.set(s);
               }
             return s;
           }
           
        public static void closeSession() throws HibernateException {
               Session s = session.get ();
            if(s != null) {
                s.close();
            }
            session.set(null);
        }
    }
    View Code
    • 法二.用户类继承标准ThreadLocal类,覆写initialValue(),增加初始化实例语句

    ThreadLocal源码

    ppublic class ThreadLocal
    {
     private Map values = Collections.synchronizedMap(new HashMap());
                                  
     public Object get()
     {
      Thread curThread = Thread.currentThread();
      Object o = values.get(curThread);
    
      if (o == null && !values.containsKey(curThread))
      {
       o = initialValue();
       values.put(curThread, o);
      }
      
            return o;
     }
    
     public void set(Object newValue)
     {
      values.put(Thread.currentThread(), newValue);
     }
    
     public Object initialValue()
     {
      return null;
     }
    }
     
    View Code

    用户类继承标准ThreadLocal类,覆写initialValue()

    public class Test1 extends ThreadLocal{
        @Override
      public Object initialValue()
      {
          //return null;
       return 你要保存副本的实例变量;
      }
    
    //get(),set()都不必覆写,因为标准ThreadLocal下都是Object类型.
    }
    View Code
    • 法三. 在类内建匿名类或内部类,实际是法一和法二的结合
    例1:设一个匿名类
    SerialNum类,为每一个类分配一个序号:
    public class SerialNum
    {
     // The next serial number to be assigned
     private static int nextSerialNum = 0;
       
     private static ThreadLocal serialNum = new ThreadLocal()  {
         @override
      protected synchronized Object initialValue()
      {
       return new Integer(nextSerialNum++);        
      }
     };
    
     public static int get()
     {
      return ((Integer) (serialNum.get())).intValue();
     }
    }
    
    SerialNum类的使用非常简单:
    int t1 = SerialNum.get(); 即可。
    View Code

    线程独享变量?

     还有一个会引起疑惑的问题,我们说ThreadLocal为每一个线程维护一个独立的变量副本,那么是不是说各个线程之间真正的做到对于对象的“完全自治”而不对其他线程的对象产生影响呢?其实这已经不属于对于ThreadLocal的讨论,而是你出于何种目的去使用ThreadLocal。如果我们为一个线程关联的对象是“完全独享”的,也就是每个线程拥有一整套的新的 栈中的对象引用+堆中的对象,那么这种情况下是真正的彻底的“线程独享变量”,相当于一种深度拷贝,每个线程自己玩自己的,对该对象做任何的操作也不会对别的线程有任何影响。

      另一种更普遍的情况,所谓的独享变量副本,其实也就是每个线程都拥有一个独立的对象引用,而堆中的对象还是线程间共享的,这种情况下,自然还是会涉及到对共享资源的访问操作,依然会有线程不安全的风险。所以说,ThreadLocal无法解决线程安全问题。

      所以,需不需要完全独享变量,进行完全隔离,就取决于你的应用场景了。可以想象,对象过大的时候,如果每个线程都有这么一份“深拷贝”,并发又比较大,对于服务器的压力自然是很大的。像web开发中的servlet,servlet是线程不安全的,一请求一线程,多个线程共享一个servlet对象;而早期的CGI设计中,N个请求就对应N个对象,并发量大了之后性能自然就很差。

    实际运用

            ThreadLocal在连接池,spring的事务管理,包括Hibernate的session管理等都有出现,在web开发中,有时会用来管理用户会话 HttpSession,
    web交互中这种典型的一请求一线程的场景似乎比较适合使用ThreadLocal,但是需要特别注意的是,由于此时session与线程关联,
    而tomcat这些web服务器多会采用线程池机制,也就是说线程是可复用的,所以在每一次进入的时候都需要重新进行set,或者在结束时及时remove。

  • 相关阅读:
    Java知识体系总结(2021版)
    第三篇 makefile的伪目标
    第二篇 makefile的基本结构
    第一篇 make与makefile介绍
    hdu 1994 利息计算
    Python中Class中的object是什么意思?
    编译语言和解释语言有什么不同?
    从文本文件hello.txt到可执行文件hello
    classmethod和staticmethod
    Uninstall NetBeans
  • 原文地址:https://www.cnblogs.com/hup666/p/13053226.html
Copyright © 2011-2022 走看看