首页 > 其他分享 >volatile,原子变量和ThreadLocal

volatile,原子变量和ThreadLocal

时间:2024-09-01 15:23:38浏览次数:13  
标签:变量 ThreadLocalMap value ThreadLocal 线程 内存 volatile

1、多线程中的变量

 首先我介绍的是volatile关键字,其次是原子变量,最后则是ThreadLocal线程本地变量

2、java基本内存模型

  用到volatile这个关键字以及后面的原子变量之前,我们必须先了解一下什么是java基本内存模型。
  先明确几个概念:
  主内存:主内存就是所有线程共享的内存,对于一个共享变量来说,主内存存放其真实数据(本尊数据)
  线程工作内存:线程对数据操作时,都会有自己的工作内存,对共享变量操作前,会先从主内存中获取到值,操作完后在回写回去。   

3、volatile

  现在有2个线程A,B,他们要主内存中间的一个变量s=0;此时A线程要修改这个共享变量,它是先获取到这个值复制到线程工作内存里面去,然后在线程工作内存里面把这个值修改了,然后把这个值再写到主内存里面去。此时B读取这个s变量,那么值可能是0,也可能是线程A所修改的值。
  使用volatile这个关键字可以避免上述这种情况(使用锁来对变量加锁或者synchronized开销太大)。
  针对上述的例子,volatile的可见性保证了不会出现上述问题。

什么是可见性呢?
  当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。

可见性不是原子性。当遇到以下情况会有问题:
  1.多个线程同时修改变量且修改时依赖变量本身。
  2.多个volatile变量维护一个条件,若是别的线程对其多个变量修改,那么可能造成条件的不成立。

比如i=i+1,i=x。这些操作都不是原子性,对应i=i+1,执行是存在三步操作,先读取i的值,然后对i加1操作,最后将结果赋值给i。

针对上述情况于是有了原子变量(后面介绍),保证了其原子性。

volatile还有一个特性就是禁止指令重排序。那么什么是指令重排序呢?
  指令重排序:编译器的字节码的重排序。cpu指令的重排序。
  指令重排序的目的是在不改变单线程下程序的逻辑下,优化程序执行效率。对于多线程于是就有了问题。有的程序时单线程下,调换一下顺序也没什么的,但是,对于多线程,调换一下顺序,可能回到其他线程造成大的影响。
  而volatile则是解决了这个问题,他利用了内存屏障来来辅助解决了这个。

4、原子变量(cas)

  说到原子变量就不得不说CAS。
  什么是CAS呢?
  就是更新一个值的时候,查询内存中的值,和自己要更新前获取到的值是否一致,若是一致,那么更新。
  与synchronized相比,cas是乐观锁,我认为并发不会修改到我的值,不加锁,只是提前获取到值,要更新的时候在比对一下,若是内存的值和我的值一致,那么更新,否则不更新。而synchronized则是不管什么直接加锁的。因此是悲观锁。
  什么是ABA问题?
  3个线程A,B,C对cas变量a修改。A,B线程获取到了变量a,A修改变量为b,B线程阻塞,C线程获取到变量b,并把b改成了a,B线程不阻塞了,继续执行,执行成功,这就是ABA问题。这个B线程不应该执行的,但是还是执行了。如这个变量是个对象,其引用没有变化,但是具体指变了,那么会出大问题的。解决方案就是在cas变量上加个版本号或者时间戳来限定。
  具体demo就是Atomic开头的类。具体我就不详细说了。
  与加锁相比这个更加轻量级。

5、ThreadLocal

  线程本地变量是说,每个线程都有同一个变量的独有拷贝ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法。这个直接看个demo

package thread;

public class ThreadLocal001 {
static ThreadLocal local = new ThreadLocal();

public static void main(String[] args) throws InterruptedException {
    Thread child = new Thread() {
        @Override
        public void run() {
            System.out.println("child" + local.get());
            local.set(200);
            System.out.println("child" + local.get());
        }
    };
    local.set(100);
    child.start();
    child.join();
    System.out.println("main" + local.get());
}

}

  结果如下:

childnull
child200
main100


  这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义。

6、ThreadLocal原理解析。

  Thread类里面有一个属性:

ThreadLocal.ThreadLocalMap threadLocals = null;

  ThreadLocal 里面的set 方法:

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap对象。
    ThreadLocalMap map = getMap(t);
    //有则把值放进去,没有则创建ThreadLocalMap对象。
    if (map != null)
        map.set(this, value);
    else

        createMap(t, value);
}

  ThreadLocal的getMap方法:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

  ThreadLocal的createMap方法:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  我们发现值是存在当前线程的的一个内部类里面,存的就是当前threadlocal和值的键值对。

  ThreadLocalMap的构造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //创建一个数组,数组对象为entity,初始化大小为16
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //根据ThreadLocal的hash值确定其数组位置,在将值放进去
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

  ThreadLocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {

       // We don't use a fast path as with get() because it is at
       // least as common to use set() to create new entries as
       // it is to replace existing ones, in which case, a fast
       // path would fail more often than not.

       Entry[] tab = table;
       int len = tab.length;
       int i = key.threadLocalHashCode & (len-1);

       for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
           ThreadLocal<?> k = e.get();

           if (k == key) {
               e.value = value;
               return;
           }

           if (k == null) {
               replaceStaleEntry(key, value, i);
               return;
           }
       }

       tab[i] = new Entry(key, value);
       int sz = ++size;
       if (!cleanSomeSlots(i, sz) && sz >= threshold)
           rehash();
   }

  从什么这些我们可以看出来,具体的值是存在Thread对象里面的,因此不同线程之间相互没有影响。具体一点。每个Thread类里面有个属性: ThreadLocal.ThreadLocalMap threadLocals = null。显然这个属性类是ThreadLocal的内部类。我们看看ThreadLocal的get方法:

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();
}

  我们先获取到当前Thread类,然后的到其ThreadLocalMap类属性,我们的值就存在里面。这个类的属性如下:Entry[]数组,这个key和value分别是ThreadLocal,value。完美解释。

  内存泄漏的问题,我们看2段代码即可明白。

static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
       Object value;

       Entry(ThreadLocal<?> k, Object v) {
           //这里让key放到上层父类处理,使其变成弱引用
           super(k);
           value = v;
       }
   }

  首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
  因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

避免方法,先将value remove掉,

标签:变量,ThreadLocalMap,value,ThreadLocal,线程,内存,volatile
From: https://blog.51cto.com/u_15709549/11889218

相关文章