Unsafe类实现CAS?
Unsafe与CAS - 五月的仓颉 - 博客园
CAS(CompareAndSet)原理
核心思想
这是一条硬件级别的原子性操作:CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。(从代码级别来看就像无锁因为没有lock,但其实硬件级别上加锁)
依赖unsafe类中的compareAndSet方法,由unsafe来保证硬件级别上的原子性操作。
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
i++操作用Synchronize锁吗
CAS要比synchronized更轻量级,i++属于一个共享变量的原子操作
CAS缺点
- ABA问题
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
ABA问题
如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。(这样看起来好像没有问题,毕竟它又改回A和原来一样了)
- 链表head->A->B->C,变成head->A->C,线程并不知道B删除,仍赋值为head->B->C
- 链表head->A->B->C,变成head->A->C,线程并不知道B删除,仍赋值为head->C
- 账户100元,小明取50元,别人汇给他50元,诈骗分子偷了50元,小明无法知道别人汇钱和偷钱。
总结:A->B->A,会漏掉一段时间窗口对A的监控,也就是说当你关心A的变化过程就要注意,如果你只是关心A是否变化就不会出现ABA。
解决方案
增加标识位,如版本号或者是时间戳,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段。即A->B->A
就变成了1A->2B->3A
。
循环时间长开销大
如果CAS
操作失败,就需要循环进行CAS
操作(循环同时将期望值更新为最新的),如果长时间都不成功的话,那么会造成CPU极大的开销。
这种循环也称为自旋
解决方法: 限制自旋次数,防止进入死循环。
只能保证一个共享变量的原子操作
CAS
的原子操作只能针对一个共享变量。
解决方法: 如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,或者可以把多个共享变量合并成一个共享变量进行CAS
操作。
Unsafe
java和c++语言的一个重要区别就是java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存,java中的unsafe类为我们提供了类似C++手动管理内存的能力。一般情况下我们不会用到这个类,因为不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”
- 可以绕过构造函数实例化
- 可以直接为变量开辟内存空间
- 可以直接给类的变量赋值
- 可以读取字节码文件构建实例
- 可以拿到父类
- 可以计算类的内存偏移量
Java魔法类:Unsafe应用解析
CAS分析AtomicInteger原理
Unsafe与CAS - 五月的仓颉 - 博客园
ThreadLocal
介绍和理解(背诵)
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
在我们使用 ThreadLocal 进行get、set操作时,其实跟 ThreadLocal 关系并不大,真正关系大的是 Thread 的 ThreadLocalMap ,而我们创建的 ThreadLocal 实例只是获取他的 hash 值然后作为 Entry 数组的下标。
- 每个线程通过 ThreadLocal 的 get() 方法拿到的是 实例(get/set方法都线程独立)
- 每个线程所访问到的是同一个 ThreadLocal 变量
threadLocal与直接线程new对象区别?
- threadLocal提供了更好的一套初始化和回收的机制。线程结束后可以回收所有副本
- 一个线程可能贯穿多个方法,如果这多个方法都使用了同一个方法,你在该方法new的新对象就会产生3个。而使用threadLocal保证了一个线程只产生一个对象。
- 减少了参数来回传递的麻烦,直接get取对象就可以了
使用场景
SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。这时候就可以在线程池内使用加上ThreadLocal包装SimpleDataFormat
,再调用initialValue让每个线程有一个SimpleDataFormat
的副本,从而解决了线程安全的问题,也提高了性能。
原理
调用链一定要讲清楚的
Thread
维护了ThreadLocalMap
,而ThreadLocalMap
里维护了Entry
数组,而每个Entry
里存的是以ThreadLocal
为key,传入的值为value的键值对。(每个entry都是一个单独map的结构)
// java.lang.Thread类里持有ThreadLocalMap的引用
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
// java.lang.ThreadLocal有内部静态类ThreadLocalMap
public class ThreadLocal<T> {
static class ThreadLocalMap {
private Entry[] table;
// ThreadLocalMap内部有Entry类,Entry的key是ThreadLocal本身,value是泛型值
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
get方法
- 获取当前线程对应的ThreadLocalMap对象
- 获取此ThreadLocalMap下的当前ThreadLocal为key对应的entry对象
- 获取entry对应的value返回即可,
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程对应的ThreadLocalMap对象。
ThreadLocalMap map = getMap(t);
// 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
if (map != null) {
// 获取此ThreadLocalMap下的entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// 若entry也获取到了
if (e != null) {
@SuppressWarnings("unchecked")
// 直接获取entry对应的value返回。
T result = (T)e.value;
return result;
}
}
// 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。
// 知识点:我早就说了,初始值方法是延迟加载,只有在get才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次!!!
return setInitialValue();
}
- 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
- 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
- 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
ThreadLocalMap map = getMap(t);
// 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
if (map != null)
map.set(this, value);
else
// 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
createMap(t, value);
}
为什么需要数组呢?没有了链表怎么解决Hash冲突呢?
一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
- 根据ThreadLocal对象的hash值,定位到table中的位置i = key.threadLocalHashCode & (len-1);
- 如果当前位置已有对象,则会寻找下一个为空的位置((i + 1 < len) ? i + 1 : 0)
- 数组达到阈值后会扩容两倍
内存泄露
内存泄露这个问题需要分两个方面回答:
- 1、
ThreadLocalMap.Entry
的key会内存泄漏吗? - 2、
ThreadLocalMap.Entry
的value会内存泄漏吗?
(描述上图的对象引用结构
Entry的key是弱引用,弱引用对象不管当前内存空间足够与否,都会回收它的内存。如果在某一时刻,将ThreadLocal
实例设置为null
,即ThreadLocal
没有强引用了,此时发生 GC ,由于ThreadLocal
实例只存在弱引用,被回收。所以key不存在内存泄漏问题。(线程池线程运行别的任务的时候threadlocal的引用就会解除,但是线程以及threadLocalMap的引用链没有解除)--不存在想要用threadlocal的时候被回收,因为你想要用的时候key一定是有外部的强引用连着的。
Entry的value是强引用,强引用对象,线程终止情况下,它可以被GC回收。但是在线程池的情景下,线程的生命周期不会结束,但是key是弱引用会被GC回收,value强引用不会回收,所以造成内存泄露:
Thread->ThreadLocalMap->Entry(key为null)->value
ThreadLocal提供了remove()
方法,该方法就是找到对应的值置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。所以用完后记得remove()
线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?
不是。栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见;而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。
ThreadLocal里的对象一定是线程安全的吗
未必,如果在每个线程中ThreadLocal.set()
进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()
获取的还是这个共享对象本身,还是有并发访问线程不安全问题。
抛出这8个问题,检验一下你到底会不会ThreadLocal,来摸个底~ - Java知音号 - 博客园
Doug Lea写的ThreadLocal怎么还是会产生内存泄漏?
Java面试必问:ThreadLocal终极篇 淦!