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。

  • 相关阅读:
    some tips
    ORA00847: MEMORY_TARGET/MEMORY_MAX_TARGET and LOCK_SGA cannot be set together
    Chapter 01Overview of Oracle 9i Database Perfomrmance Tuning
    Chapter 02Diagnostic and Tuning Tools
    变量与常用符号
    Chapter 18Tuning the Operating System
    标准输入输出
    Trace files
    DBADeveloped Tools
    Chapter 03Database Configuration and IO Issues
  • 原文地址:https://www.cnblogs.com/hup666/p/13053226.html
Copyright © 2011-2022 走看看