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
public class ThreadLocal001 {
static ThreadLocal local = new ThreadLocal();
}
结果如下:
这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义。
6、ThreadLocal原理解析。
Thread类里面有一个属性:
ThreadLocal 里面的set 方法:
ThreadLocal的getMap方法:
ThreadLocal的createMap方法:
我们发现值是存在当前线程的的一个内部类里面,存的就是当前threadlocal和值的键值对。
ThreadLocalMap的构造方法:
ThreadLocalMap的set方法:
从什么这些我们可以看出来,具体的值是存在Thread对象里面的,因此不同线程之间相互没有影响。具体一点。每个Thread类里面有个属性: ThreadLocal.ThreadLocalMap threadLocals = null。显然这个属性类是ThreadLocal的内部类。我们看看ThreadLocal的get方法:
我们先获取到当前Thread类,然后的到其ThreadLocalMap类属性,我们的值就存在里面。这个类的属性如下:Entry[]数组,这个key和value分别是ThreadLocal,value。完美解释。
内存泄漏的问题,我们看2段代码即可明白。
首先来说,如果把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掉,