3 ThreadLocalRandom与ThreadLocal

Random介绍:

Random: 随机数的生成需要一个默认的种子,这个种子其实是一个long类型的数字,你可以在创建Random对象时通过构造函数指定,如果不指定则在默认构造函数内部生成一个默认的值。

新的随机数的生成需要两个步骤:● 首先根据老的种子生成新的种子。● 然后根据新的种子来计算新的随机数。

复制代码
public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);

        //根据旧种子产生新种子
        int r = next(31);
        //根据新种子产生随机数
        int m = bound - 1;
        if ((bound & m) == 0)  // i.e., bound is a power of 2
            r = (int)((bound * (long)r) >> 31);
        else {
            for (int u = r;
                 u - (r = u % bound) + m < 0;
                 u = next(31))
                ;
        }
        return r; 
}
复制代码
1
 

产生的种子过程

复制代码
protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
}
复制代码

每个Random实例里面都有一个原子性的种子变量用来记录当前的种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以ThreadLocalRandom应运而生。

 

ThreadLocalRandom介绍:

之前讲解到ThreadLocal:ThreadLocal,它通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理,Random的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争。

image.png

如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能。

image.png

 

 

ThreadLocalRandom原理:

image.png

ThreadLocalRandom类继承了Random类并重写了nextInt方法,在ThreadLocalRandom类中并没有使用继承自Random类的原子性种子变量。在ThreadLocalRandom中并没有存放具体的种子,具体的种子存放在具体的调用线程的threadLocalRandomSeed变量里面。ThreadLocalRandom类似于ThreadLocal类,就是个工具类。当线程调用ThreadLocalRandom的current方法时,ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量,也就是初始化种子。

如果当前线程中threadLocalRandomProbe的变量值为0(默认情况下线程的这个变量值为0),则说明当前线程是第一次调用ThreadLocalRandom的current方法,那么就需要调用localInit方法计算当前线程的初始化种子变量。这里为了延迟初始化,在不需要使用随机数功能时就不初始化Thread类中的种子变量,这是一种优化。

复制代码
public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
    }

static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }
复制代码

 

当调用ThreadLocalRandom的nextInt方法时,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的threadLocalRandomSeed变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是,threadLocalRandomSeed变量就是Thread类里面的一个普通long变量,它并不是原子性变量。其实道理很简单,因为这个变量是线程级别的,所以根本不需要使用原子性变量,可以参考ThreadLocal的原理。

复制代码
public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        int r = mix32(nextSeed());
        int m = bound - 1;
        if ((bound & m) == 0) // power of two
            r &= m;
        else { // reject over-represented candidates
            for (int u = r >>> 1;
                 u + m - (r = u % bound) < 0;
                 u = mix32(nextSeed()) >>> 1)
                ;
        }
        return r;
}
复制代码

 

使用r = UNSAFE.getLong(t, SEED)获取当前线程中threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,而后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量中。

final long nextSeed() {
        Thread t; long r; // read and update per-thread seed
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
}

 

ThreadLocalRandom线程安全如何保证:

当多线程通过ThreadLocalRandom的current方法获取ThreadLocalRandom的实例时,其实获取的是同一个实例。但是由于具体的种子是存放在线程里面的,所以在ThreadLocalRandom的实例里面只包含与线程无关的通用算法,所以它是线程安全的。

 

ThreadLocal

  • threadLocal原理
ThreadLocal提供线程局部变量,每一个线程在访问ThreadLocal实例的时候(通过其get与set方法)都有自己的,独立的初始化变量副本。Threadlocal实例通常是类中的私有静态字段,使用它的目的是希望将状态与线程关联起来。
//ThreadLocal的方法
public void set(T value) {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null)
            map.set(this, value); // 使用this引用作为key,既做到了变量相关,又满足key不可变的要求。
       else
           createMap(t, value);
}
//ThreadLocalMap的方法
复制代码
private void set(ThreadLocal<?> key, Object value) {
           // map中就是使用Entry[]保留所有的entry实例
           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;
               }
           }
           //不存在就新建一个entry
            tab[i] = new Entry(key, value);
           int sz = ++size;
           if (!cleanSomeSlots(i, sz) && sz >= threshold)
               rehash();
 }

//ThreadLocal的方法
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();
}

//ThreadLocalMap的方法
private Entry getEntry(ThreadLocal<?> key) {
           int i = key.threadLocalHashCode & (table.length - 1);
           Entry e = table[i];
           if (e != null && e.get() == key)
               return e;
           else
               return getEntryAfterMiss(key, i, e);
}

public void remove() {
   ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
        m.remove(this);
     }
}
复制代码
想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到了当前线程中的一个Map中去了。
思考:
1 ThreadLocal中的ThreadLocalMap的数据结构与关系?
Thread中有一个成员变量ThreadLocalMap。,ThreadLocalMap中有一个静态内部类Entry.  Entry是对ThreadLocal的弱引用。ThreadLocal中有一个静态内部类ThreadLocalMap
2 Entry的key是弱引用,为什么?
每个Thread对象维护着ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry进行存储。
调用ThreadLocal的set方法时,实际是往ThreadLocalMap设置值,key是ThreadLocal对象, value是传进来的对象
调用ThreadLocal的get方法时, 实际是从ThreadLocalMap中获取值
ThreadLocal本身并不存储值,它只是以自己作为key从线程的ThreadLocalMap获取value, 正因为如此,ThreadLocal实现了线程隔离,获取当前线程的局部变量值,不受其他线程影响。
3 ThreadLocal内存泄露问题?
虚拟机栈栈中的ThreadLocal Ref 与堆上的ThreadLocal是强引用的关系,当虚拟机栈中的ThreadLocal Ref 用完之后,准备被回收时,如果entry与堆上的ThreadLocal不是弱引用的话,那么堆上的ThreadLocal对象就不会被回收。而如果是弱引用,当发生Gc时,ThreadLocal用完之后,不存在强引用,而唯一存在一个弱引用,此时ThreadLocal对象就可以被回收了。虽然弱引用可以让堆上的ThreadLocal对象被回收,但是Entry对象还因为线程的存在,有引用可达。ThreadLocal被回收之后,entry里面的key是null, 此时这个entry成为了陈旧项,value对象在线程存在时,仍然会存在,故而仍然有线程泄露的风险。因此就需要某种机制,来对Entry里面的key为null的value进行释放。ThreadLocal采用了线性探测来清除陈旧项(replaceStaleEntry与getEntryAfterMiss方法都干了这个活),从而防止了内存泄漏。当然,我们也可以在用完ThreadLocal之后,手动调用remove方法,去除线程中ThreadLocalMap中的这个entry.
4 ThreadLocal中为什么要加remove方法?
主要是为了及时地将线程中TreadLocalMap中的以这个ThreadLocal为key的entry中的value对象删除。这样就可以避免因为线程长期存在,而ThreadLocal实例用完之后导致的内存泄露问题。
总结:
ThreadLocal并不解决线程间共享数据的问题,而是为每个线程提供了一个独立的变量副本。从而避免线程变量的线程安全问题。由于每个线程都有一个执属于自己的ThreadLocalMap,并维护了ThreadLocal与实例之间的映射关系,因此就不存在线程安全以及锁的问题。ThreadLocalMap的entry对ThreadLocal弱引用,避免了ThreadLocal无法被回收的问题ThreadLocal自身的set、get、remove方法最终会调用expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收键位null的entry对象的value值(实例),以及entry对象本身,从而防止内存泄露。
调用set方法时,会采样清理,全量清理,扩容时还会继续检查
调用get方法时,没有直接命中,向后环形查找时会进行清理
调用remove方法时, 除了清理当前entry, 还会向后继续清理
 
  • 四大引用是什么,分别有什么特点:
1 强引用、软引用、弱引用、虚引用
强引用:发生gc的时候,只要对象还有引用,就不会被回收
软引用:发生gc的时候,内存够用就不会回收,内存不够时,就会回收。可以及时的避免oom。
Map<String,SoftReference<BitMap>> imageCache = new HashMap<String,SoftReference<BitMap>>();
弱引用:发生gc的时候,马上就会回收。
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅仅持有虚引用,那么它和没有任何引用一样,在任何时候都可能被垃圾收集器回收。它不能单独使用也不通过通过它访问对象。虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的目的是跟踪对象被垃圾回收的状态。仅仅是提供了确保对象被finalize之后做某些事情的机制。PhantomReference的get方法总是返回null.
四种引用垃圾回收的场景:
posted @   小兵要进步  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)

侧边栏
点击右上角即可分享
微信分享提示