浅解ThreadLocal


ThreadLocal

结合这篇博客来看,效果更好,这篇博客中关于WeakReference弱引用进行了说明,以及对于ThreadLocalHashMap的数据结构也进行了分析

多线程访问同一个共享变量时特别容易触发并发问题,特别是多线程对一个共享变量进行写入时,为了线程安全,一般使用者在访问共享变量时需要进行适当的同步

同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担,有没有一种方法可以做到当创建一个变量以后,每个线程对其访问的时候访问的是自己线程的变量呢?ThreadLocal的出现解决了这样的问题

ThreadLocal是JDK包提供的,它提供了线程本题变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个线程的一个本题副本,当多个线程操作这个变量时,实际上操作的是自己本地内存里面的变量,从而避免了线程安全问题

 

ThreadLocal使用示例

 public class ThreadLocalTest {
     //创建ThreadLocal变量
     static ThreadLocal<String> localVariable = new ThreadLocal<>();
 
     static void print(String str) {
         //打印当前线程本地内存中的localVariable变量的值
         System.out.println(str + ":" + localVariable.get());
         //清除当前线程本地内存中的localVariable变量
         localVariable.remove();
    }
 
     public static void main(String[] args) {
         //创建线程1
         new Thread(() -> {
             localVariable.set("threadOne local variable");
             print("threadOne");
             System.out.println("threadOne remove after" + ":" + localVariable.get());
        }).start();
         //创建线程2
         new Thread(() -> {
             localVariable.set("threadTwo local variable");
             print("threadTwo");
             System.out.println("threadTwo remove after" + ":" + localVariable.get());
        }).start();
    }
 }

 

实现原理

Thread类中有一个threadLocals和inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal实例种的set和get方法的时候才会创建它

所以ThreadLocal只是一个工具壳,它通过set和get方法调用当前线程的threadLocals变量

 

 public T get() {
     //获取当前线程
     Thread t = Thread.currentThread();
     //获取当前线程的threadLocals变量
     ThreadLocalMap map = getMap(t);
     //如果map不为空
     if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
        }
    }
     return setInitialValue();
 }
 //返回线程的threadLocals变量
 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }
 public void set(T value) {
     //获取当前线程
     Thread t = Thread.currentThread();
     //将当前线程作为key去查找对应的线程变量,找到则设置
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
 }

内存溢出问题

我们一般接触的变量都为强引用,这种引用一旦脱离GCRoots就会被垃圾回收掉,但是弱引用的特性是只要发现是弱引用无论是否声明都会被垃圾回收,即一旦触发GC就会引起弱引用的消失但随之而来也有一个问题如果一直不触发GC那么弱引用除非手动置空否则一直就会存在这就会导致内存溢出

每一个线程都会维护一个ThreadHashMap

 static class ThreadLocalMap {
     //继承了弱引用
     static class Entry extends WeakReference<ThreadLocal<?>> {
         /** The value associated with this ThreadLocal. */
         //没有属性key
         Object value;
         //key为threadlocal变量
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
        }
    }

 

当一个线程调用ThreadLocal的set方法设置变量时,当前线程的ThreadLocalMap里都会存放一个记录,这个记录的key为ThreadLocal的弱引用,value则为设置的值,如果当前线程一直存放在且没有调用的ThreadLocal的remove方法,并且这时候另一个线程还存在对ThreadLocal(谨记ThreadLocal是声明的变量),则当前线程的ThreadLocalMap变量里面会存在对ThreadLocal变量的引用和对value对象的引用,它们是不会被释放的这就会造成内存泄漏

考虑到这个ThreadLocal变量没有其它强依赖而且当前线程害存在的情况,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会在gc的时候被回收,但是对应的value还是会造成内存泄漏,因为这时候ThreadLocalMap里面就会存在key为null但是value不为null的桶

Random类及其局限性

所谓的Random即随机数,其实并不随机,因为随机数的产生依赖于种子数(无参构造的话会调用本地方法营造出一种随机的感觉),新的种子的产生依赖于老的种子。

 public int nextInt(int bound) {
     //如果种子小于0那么直接抛出异常
     if (bound <= 0)
         throw new IllegalArgumentException(BadBound);
     //next方法基于旧种子产生新种子并返回
     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;
 }

在单线程下都是根据旧种子产生新种子但是在多线程下有可能拿到同一个旧种子去产生新种子这违背了随机性,所以下面的这个函数要保证原子性也就是说当多个线程根据同一个老种子计算新种子的时候第一个线程的新种子被计算出来时第二个线程要丢弃自己的老种子,Random使用原子变量达到了这一目的

 protected int next(int bits) {
     //老种子和新种子
     long oldseed, nextseed;
     //将种子的值赋予给原子变量
     AtomicLong seed = this.seed;
     //下面代码是一个自旋锁
     do {
         //获取老种子
         oldseed = seed.get();
         //获取到新种子
         nextseed = (oldseed * multiplier + addend) & mask;
         //CAS操作
    } while (!seed.compareAndSet(oldseed, nextseed));
     return (int)(nextseed >>> (48 - bits));
 }

自旋锁的局限性

由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自选重试,这会降低并发性能,基于此ThreadLocalRandom孕育而生

ThreadLocalRandom

 @sun.misc.Contended("tlr")
     long threadLocalRandomSeed; //每个线程都会有一个种子和ThreadLocal相似
 
 @sun.misc.Contended("tlr")
     int threadLocalRandomProbe; //延迟加载
 
 @sun.misc.Contended("tlr")
     int threadLocalRandomSecondarySeed;
 
posted @ 2022-08-19 16:36  雙雙  阅读(32)  评论(0编辑  收藏  举报