从ThreadLocal到Scope再到FastThreadLocal (一)

这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战

引用

引用部分的总结参考 juejin.cn/post/684490…

强引用

默认的引用,jvm宁可oom也不会被垃圾回收器回收

不再使用时,手动置为null弱化引用

Object strongObject = new Object();
object = null
复制代码

方法中的强引用,引用在栈中,实际变量在堆中

ArrayList 中的 clear 方法,是对其中的数组逐个设置为 null,而不是直接把整个数组置为 null

public void clear() {
    modCount++;
    final Object[] es = elementData;
    for (int to = size, i = size = 0; i < to; i++)
      es[i] = null;
}
复制代码

软引用

一般用于内存缓存,特点是平常可以有,内存不足时可以被回收

内存充足不回收,内存不足则可能被回收

可以与一个引用队列关联,当所引用的对象被回收时,会将软引用放进队列中

ReferenceQueue<String> queue = new ReferenceQueue<>();
String abc = "123";
SoftReference<String> soft = new SoftReferenct<>(abc, queue);
复制代码

队列用于在oom时,jvm根据队列回收长时间不使用的对象。回收时会将所引用的对象置为 null,再通知垃圾收集器收集

注意:调用 System.gc() 不一定会触发垃圾回收,因为这个只是通知,具体啥时候执行gc由具体的垃圾收集器自己决定

ReferenceQueue 存在的意义在于,进行回收时优先回收长时间不使用的

弱引用

被gc扫描到了就回收,但是gc是一个优先级很低的线程,不一定很快就发现

String str = "123";
WeakReference<String> weakReference = new WeakReferenct<>(str);
复制代码

其他行为与软引用类似

用途

一个对象偶尔使用,希望在使用时随时就能获取到,但又不影响此对象的垃圾回收,那么就应该使用Weak Referenct来记住此对象

虚引用

虚引用拿不到具体数据,其 get 方法直接返回 null

public T get() {
  return null;
}
复制代码

主要被用来跟踪对象被垃圾收集器回收的情况

NIO中使用虚引用管理堆外内存

String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
复制代码

ThreadLocal

ThreadLocal基础

基本信息

用于为每个线程保存一份独有的同类型数据,其实现在于其内部独有的一个 ThreadLocalMap ,并在每一个 Thread 中存有 ThreadLocalMap 的变量

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码

因为这里的map每一个都是线程内部独有的,因此不存在线程安全问题

常用方法

ThreadLocal 中的主要方法如下

public T get();
boolean isPresent();
public void set(T value);
public void remove();
private T setInitialValue();
复制代码

同时包含一个静态工厂方法

提供一个初始值的 supplier

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier);
复制代码

有意思的hashCode

ThreadLocalThreadLocalMap 作为key使用,具体计算时使用其一个hashCode属性

  1. key设计成弱引用,随时可能被gc,因此会留下更多空槽
  2. hash碰撞成,通过线性探测的开放地址法解决冲突
  3. 通过魔数减少hash冲突
// hashCode相关属性和方法
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
  new AtomicInteger();
private static int nextHashCode() {
  return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 一个计算hashCode的魔数
private static final int HASH_INCREMENT = 0x61c88647;
复制代码

这里有意思的就是 0x61c88647

搜了多篇文章,基本都是说该数得到的hash值较为分散,在于黄金分割比例,但是基本都是一笔带过,没有细说

这里贴一篇推荐的文章 ThreadLocal的hash算法(关于 0x61c88647) - 掘金 (juejin.cn)

ThreadLocalMap

static class ThreadLocalMap {
  static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
  }
}
复制代码
  • 初始大小为16
  • 负载因子为2/3
  • 各种操作的时候,需要考虑 Entrykeynull 的情况(即弱引用被回收)

Thread.exit()

正如其注释如言

该方法由系统调用,给了线程在真正退出之前进行清理操作的机会

其中 threadLocals = null; 相当于对于 ThreadLocalMap 进行了垃圾回收,因此其生命周期与 Thread 的生命周期一致

/**
 * This method is called by the system to give a Thread
 * a chance to clean up before it actually exits.
*/
private void exit() {
  if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
    TerminatingThreadLocal.threadTerminated();
  }
  if (group != null) {
    group.threadTerminated(this);
    group = null;
  }
  /* Aggressively null out all reference fields: see bug 4006245 */
  target = null;
  // 这里设置
  threadLocals = null;
  inheritableThreadLocals = null;
  inheritedAccessControlContext = null;
  blocker = null;
  uncaughtExceptionHandler = null;
}
复制代码

但是如果一直没有退出呢?则可能导致内存泄露!

内存泄露问题

问题核心原因

ThreadLocalMapkey 可能被回收,但是 valueentry 本身只有在 set get remove 操作时发现 key 为空才进行删除回收,即 value 采取的是懒删除策略

问题触发

线程被放回线程池不再使用,或者再次使用时不再调用 set get remove 方法

其中的删除单个格子的方法

private int expungeStaleEntry(int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;

  // 删除value以及entry本身
  tab[staleSlot].value = null;
  tab[staleSlot] = null;
  // 缩小size
  size--;

  // 对后续的数据重新进行hash,直到碰到null
  // 因为删除一个之后会导致后面的hash改变(后面的hash可能是冲突后线性探测得到的)
  Entry e;
  int i;
  for (i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    	// ......
    }
  }
  return i;
}
复制代码

如何解决泄露问题

参考 Netty 实现,对 Runnable 进行封装,执行完即进行清理动作,使用Runnable的地方都以此来代替

final class FastThreadLocalRunnable implements Runnable {
    private final Runnable runnable;

    @Override
    public void run() {
        try {
            runnable.run();
        } finally {
          	// 这里进行清理
            FastThreadLocal.removeAll();
        }
    }
}
复制代码

普通的业务最佳实践

try {
    // 其它业务逻辑
} finally {
    threadLocal.remove();
}
复制代码

Scope

github地址: github.com/PhantomThie…

是对ThreadLocal的一种高级封装

其特性如下:

  • 显示的声明Scope的范围
  • 强类型
  • 可以在线程池中安全的使用,并防止泄露
  • 只支持jdk1.8

注意:其本身并没有解决 ThreadLocal 的内存泄露问题

核心定义

ScopeKey

public final class ScopeKey<T> {
		// 默认数值
    private final T defaultValue;
  	// 数值初始化Supplier
    private final Supplier<T> initializer;
  	// 是否开启null保护(为null时是否可以返回)
    private final boolean enableNullProtection;
}
复制代码

Scope

public final class Scope {
  	// ThreadLocal的Value为Scope
    private static final SubstituteThreadLocal<Scope> SCOPE_THREAD_LOCAL = MyThreadLocalFactory.create();
  
  	// 两个线程安全的HashMap,一个保存具体的数值,另一个保存是否null保护
    private final ConcurrentMap<ScopeKey<?>, Object> values = new ConcurrentHashMap<>();
    private final ConcurrentMap<ScopeKey<?>, Boolean> enableNullProtections = new ConcurrentHashMap<>();
}
复制代码

确保Scope范围的方法

本质上就是在新的Scope上下文环境中执行runnable或者supplier方法

执行前保存旧的scope,执行结束重设scope

public static <X extends Throwable> void runWithExistScope(@Nullable Scope scope,
                                                           ThrowableRunnable<X> runnable) throws X {
  supplyWithExistScope(scope, () -> {
    runnable.run();
    return null;
  });
}
public static <T, X extends Throwable> T supplyWithExistScope(
  @Nullable Scope scope, ThrowableSupplier<T, X> supplier
) throws X {
  Scope oldScope = SCOPE_THREAD_LOCAL.get();
  SCOPE_THREAD_LOCAL.set(scope);
  try {
    return supplier.get();
  } finally {
    if (oldScope != null) {
      SCOPE_THREAD_LOCAL.set(oldScope);
    } else {
      SCOPE_THREAD_LOCAL.remove();
    }
  }
}
复制代码

另一组控制范围方法

public static <X extends Throwable> void runWithNewScope(@Nonnull ThrowableRunnable<X> runnable)
  throws X {
  supplyWithNewScope(() -> {
    runnable.run();
    return null;
  });
}
public static <T, X extends Throwable> T
  supplyWithNewScope(@Nonnull ThrowableSupplier<T, X> supplier) throws X {
  beginScope();
  try {
    return supplier.get();
  } finally {
    endScope();
  }
}


// 依赖于以下两个方法
public static Scope beginScope() {
  Scope scope = SCOPE_THREAD_LOCAL.get();
  if (scope != null) {
    throw new IllegalStateException("start a scope in an exist scope.");
  }
  scope = new Scope();
  SCOPE_THREAD_LOCAL.set(scope);
  return scope;
}

// 这里要注意,该方法没有重设旧的scope,因此应该用于没有旧scope的场景
public static void endScope() {
  SCOPE_THREAD_LOCAL.remove();
}
复制代码

使用方法

private static final ScopeKey<T> scope = allocate();
复制代码

其内部的 get set 方法均会去尝试从 ThreadLocal 中获取 Scope,再将 ScopeKey 本身作为key从 ScopeMap 取得/设置 值

结束

image.png

おすすめ

転載: juejin.im/post/7068479373547929614