volatile+ThreadLocal
volatile
- 一个变量修饰符,只能用来修饰变量,可以保证有序性和可见性,但是不能保证原子性,最常见的问题就是i++,他分为三个操作,在主内存中拿到原始值,进行增加,然后从工作内存写回主内存,多个线程并发执行的时候可能会有回写主内存覆盖问题。想要解决可以使用Atomic类,比如AtomicInteger,AtomicLong,AtomincReference,这些类核心是基于这个CAS机制,能把读,改,写三步合并成异步进行
- 保证可见性:当一个变量被声明为volatile的时候,他会保证对这个变量的写操作会立即刷新到主内存中,而且对这个变量的读操作会直接从主内存中读取。可见性产生的原因:由于CPU和这个内存处理上面存在一定差别,所以在这个CPU和主内存之间增加了多级高速缓存,L1,L2,L3.但是多级缓存就会带来一个数据一致性的问题,所有就通过一个缓存一致性协议来解决,比较常见的就是MESI协议,M表示数据被修改,和内存中的数据不一致,数据只存在本cache中,E和S都表示数据和内存中的数据一致,但是E是数据保存在本cache,S是存在很多个Cache中,I表示这个数据无效。当这个数据处理缓存行的时候就会先检查这个缓存行状态,这个主要是在硬件层面处理CPU缓存之间的一致性.
- 在硬件上面的关系:线程要执行的任务是跑在CPU上面的,进行运算,CPU -> L1 -> L2 -> L3 ->主内存 -> 磁盘。变量存储位置都是在主内存中的,但是CPU处理速度很快,所以中间建立了多级缓存,其中 L1 和 L2是私有的,也就是对应线程中的私有内存,所以会导致数据不一致的问题。MESI协议中,多核之间通过总线消息互相打招呼,当A要写的时候,就告诉其他核这个变量作废,标记改成M,当其他核心要读的时候,就需要把这个M标记的变量刷新到主内存中,必须保证拿到的是最新的数据。还有L3缓存是共享的。
- 多个核心之间是使用总线打招呼的,总线包括数据线,地址线,控制线。但是这个 MESI协议就是只用到地址线和控制线,先通过地址线找到核心,然后喊话使用控制线让变量失效。
- 禁止指令重排:指令重排序是指令编写的顺序和执行的顺序是不一致的,从而在多线程条件下产生可见性问题,这个操作本来是一种CPU性能优化的手段,他引入了一个叫做storeBuffer的机制,,这种优化机制会导致CPU的乱序执行,但是他也提供了内存屏障指令,上层应用可以在合适的地方插入这个内存屏障,从而避免这个指令重排序的问题。
- CPU指令重排是为了提高效率,就像有多个命令,有些变量不需要去读内存就可以直接运算,那我就可以先计算他,让那些需要读内存,消耗时间长的运算往后排,这样就大大提高了运算的利用率
- 内存屏障:就是在指令中间插一个:禁止穿越的墙!屏障前面的所有指令,必须全部执行完,前面的写操作,必须刷到缓存 / 主内存,屏障后面的指令,必须等前面做完才开始
ThreadLocal
-
每一个线程 Thread 内部都有一个 自带的Map(ThreadLocal.ThreadLocalMap threadLocals = null;),ThreadLocal就是一个简单的工具类,当我给他set值的时候,会拿到当前线程,然后自身这个类对象当做一个 key,存到map里面,这个set的value也可以设置成对象形式,比如 User,这样就可以设置多个字段像年龄,姓名,每次直接取出来这个user.所以我可以在一个线程建立多个 ThreadLocal,添加不同的对象。就好比 Thread 是一个人, ThreadLocalMap 是一个口袋,里面的ThreadLocal就相当于一个个格子,这个格子里面有各种字段。
-
这是一种解决线程安全的机制,为每一个线程创建一个共享变量副本来保证各个线程之间的一个访问和修改互不影响,也可以避免死锁
-
ThreadLocal主要有四个方法,initialValue(返回局部变量的初始值),get,set,remove。当调用这个get方法的时候会先检查ThreadLocal中是否有与之关联的值,如果没有调用这个initalValue方法初始化该值,remove方法清除ThreadLocal变量,把key和value都删除掉,如果没有及时清理会产生内存泄漏问题
-
JAVA中大部分都是强引用,只有使用 WeakReference才是弱引用,key = WeakReference(ThreadLocal) → 弱引用
-
产生这个内存泄漏主要是ThreadLocalMap的key-value部分,这里key就是Threadlocal对象,主要有两个引用源,一个是栈上的ThreadLocal引用,这个是我去 new了一个 ThreadLocal就属于强引用,但是只要我把这个 ThreadLocal 设置成等于 null,就取消掉了这个强引用,还有一个是来自ThreadLocalMap中key对他的一个引用属于弱引用,这个弱引用可以直接回收掉。第三个就是value还是有强引用,因为只要map在,Entry在,所以如果这个线程一直在线程池里面,这个value就一直存在,从而导致内存泄漏,所以最好的办法就是我们在使用完之后手动使用remove,他会把 key+value 全都删除掉。
-
但是在这个JDK21好像就有一个scopedValue就很好的解决了这个ThreadLocal的内存泄漏问题,只要是通过将数据绑定到代码执行的一个作用域而不是整个线程了,在作用域内执行完任务之后就自动清理掉绑定的值。

浙公网安备 33010602011771号