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