ThreadLocal

假设有一个静态变量,线程使用它的过程中可能会做一些修改,但是这些修改只想在这个线程内部生效,不想影响其他线程,那么就应该用ThreadLocal.

举个栗子

  1. public class ThreadLocal2 {
  2. // 创建一个ThreadLocal变量
  3. static ThreadLocal<Person> tl = new ThreadLocal<>();
  4. // 也可以直接初始化赋值
  5. //static ThreadLocal<Person> tl = ThreadLocal.withInitial(() -> new Person("xiaobai"));
  6. public static void main(String[] args) {
  7. new Thread(()->{
  8. try {
  9. TimeUnit.SECONDS.sleep(2);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. // 下面线程的ThreadLocal.set不会影响当前线程
  14. // 所以从ThreadLocal中取值,是null(对于当前线程来说,还没有set过值)
  15. System.out.println(tl.get());
  16. }).start();
  17. new Thread(()->{
  18. try {
  19. TimeUnit.SECONDS.sleep(1);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. // ThreadLocal放入值
  24. tl.set(new Person());
  25. // 处理处理
  26. // 用完后remove掉
  27. tl.remove();
  28. }).start();
  29. }
  30. static class Person {
  31. String name = "zhangsan";
  32. }
  33. }

源码解析

ThreadLocal.set()过程(get同理)
image.png
每个线程会维护一个ThreadLocal.ThreadLocalMap类型变量,ThreadLocalMap是hash map的一个定制化实现,这个ThreadLocalMap中的key是一个个的ThreadLocal的”地址”,值是各自维护的值.

可以这么理解:
ThreadLocal.set是把值set到了当前线程维护的一个Map中,key是弱引用,value是强引用.

如果一个方法内部使用一个变量指向ThreadLocal.get(),当方法结束时会回收这个变量的(强)引用,此时这个ThreadLocalMap中的key就只有弱引用指向了,一旦GC就会被回收.但是ThreadLocalMap对应的value还是强引用,如果key被回收了,那这个value就不能被访问也不能被回收了.
所以我们使用ThreadLocal时,如果用完该value了,就remove掉,避免内存泄漏
image.png

用途

  • (Spring)声明式事务

    强软弱虚引用

    强引用

    普通的引用如Object obj = new Object()就是一个强引用,只要有变量在指向这个对象,那么它就一定不会被GC回收

    求证小例子

    先写个这个类,为了观察垃圾回收
    1. public class M {
    2. // GC回收该对象时会调用该方法
    3. // 这个方法在工程中,永远都不建议重写!这里重写是为了更好的观察垃圾回收,当GC回收该对象时,会打印finalize
    4. @Override
    5. protected void finalize() throws Throwable {
    6. System.out.println("finalize");
    7. }
    8. }
    强引用(普通引用)小测试:
    1. public class T01_NormalReference {
    2. public static void main(String[] args) throws IOException {
    3. M m = new M();
    4. // 如果m不指向null,就不会打印finalize
    5. m = null;
    6. System.gc(); //DisableExplicitGC
    7. // 为了阻塞当前线程,使程序不立即停止
    8. System.in.read();
    9. }
    10. }

    软引用

    SoftReference m = new SoftReference<>(new Object)
    如果一个对象,只被一个软引用指向,那么当JVM内存不够用的时候会回收该对象.

对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。
如果这次回收还没有足够的内存,才会抛出内存溢出异常。

软引用非常适合缓存使用

求证小例子
这个例子运行时需要将堆内存设置成20M,设置JVM参数: -Xms20M -Xmx20M

  1. public class T02_SoftReference {
  2. public static void main(String[] args) {
  3. // 10M的byte数组对象
  4. SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
  5. //m = null;
  6. System.out.println(m.get());
  7. System.gc();
  8. try {
  9. Thread.sleep(500);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println(m.get());
  14. //再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
  15. // 15M的byte数组对象,如果改为5M的数组则m不会被回收
  16. byte[] b = new byte[1024*1024*15];
  17. // 如果m被回收了,则输出为null
  18. System.out.println(m.get());
  19. }
  20. }

弱引用

WeakReference m = new WeakReference<>(new M());
如果一个对象只被弱引用指向,那么只要遭遇到GC就会被回收.

一般用在容器里(典型应用:ThreadLocal)

如果弱引用a指向对象M,同时强引用b也指向对象M;那么当b不再指向M的时候,M就应该被回收.
WeakHashMap

求证小例子

  1. public class T03_WeakReference {
  2. public static void main(String[] args) {
  3. WeakReference<M> m = new WeakReference<>(new M());
  4. System.out.println(m.get());
  5. System.gc();
  6. System.out.println(m.get());
  7. ThreadLocal<M> tl = new ThreadLocal<>();
  8. tl.set(new M());
  9. tl.remove();
  10. }
  11. }

虚引用

首先需要一个队列
private static final ReferenceQueue QUEUE = new ReferenceQueue<>();
这是虚引用
PhantomReference phantomReference = new PhantomReference<>(new M(), QUEUE);
当虚引用的对象被回收时,会把改对象装到队列里

虚引用拿不到里面的值(有啥用?吹牛批时用呀!)
只要GC就被会回收

管理直接内存,即堆外内存(JVM,Netty)

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,
也无法通过虚引用来获取一个对象的实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用或者弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法可以获取对象,虚引用的get方法始终返回null,
弱引用可以选择是否使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作.
java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,
相应的用户线程会收到通知并对直接内存进行清理工作。

事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,
DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

求证小例子
设置JVM堆内存为20M

  1. public class T04_PhantomReference {
  2. private static final List<Object> LIST = new LinkedList<>();
  3. private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
  4. public static void main(String[] args) {
  5. PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
  6. new Thread(() -> {
  7. while (true) {
  8. LIST.add(new byte[1024 * 1024]);
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. Thread.currentThread().interrupt();
  14. }
  15. System.out.println(phantomReference.get());
  16. }
  17. }).start();
  18. new Thread(() -> {
  19. while (true) {
  20. Reference<? extends M> poll = QUEUE.poll();
  21. if (poll != null) {
  22. System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
  23. }
  24. }
  25. }).start();
  26. try {
  27. Thread.sleep(500);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }