Java多线程 ThreadLocal原理解析

目录
  • 1、什么是ThreadLocal变量
  • 2、ThreadLocal实现原理
  • 3、内存泄漏问题
  • 4、使用场景
    • 1)存储用户Session
    • 2)解决线程安全的问题
    • 3)使用ThreadLocal重新设计一个上下文设计模式
    • 4)ThreadLocal注意事项
      • 脏数据
      • 内存泄漏
      • 父子线程共享线程变量

1、什么是ThreadLocal变量

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。

这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

2、ThreadLocal实现原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()set() 方法其实都是调用了这个ThreadLocalMap类对应的 get() set() 方法。

例如下面的 set 方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法:

    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();
    }

createMap方法:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocalMap是个静态的内部类:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
...
}

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

3、内存泄漏问题

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()get() remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get() set()remove() 方法的情况下。

4、使用场景

如上文所述,ThreadLocal 适用于如下两种场景

每个线程需要有自己单独的实例
实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

1)存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

2)解决线程安全的问题

比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

这里的DateUtil.formatDate()就是线程安全的了。(Java8里的 [java.time.format.DateTimeFormatter]

(http://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) 是线程安全的,Joda time里的DateTimeFormat也是线程安全的)。

public class Context {

    private String name;
    private String cardId;

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class ExecutionTask implements Runnable {

    private QueryFromDBAction queryAction = new QueryFromDBAction();

    private QueryFromHttpAction httpAction = new QueryFromHttpAction();

    @Override
    public void run() {

        final Context context = new Context();
        queryAction.execute(context);
        System.out.println("The name query successful");
        httpAction.execute(context);
        System.out.println("The cardId query successful");

        System.out.println("The Name is " + context.getName() + " and CardId " + context.getCardId());
    }
}
public class QueryFromDBAction {

    public void execute(Context context) {

        try {
            Thread.sleep(1000L);
            String name = "Jack " + Thread.currentThread().getName();
            context.setName(name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

    public void execute(Context context) {
        String name = context.getName();
        String cardId = getCardId(name);
        context.setCardId(cardId);
    }

    private String getCardId(String name) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "444555" + Thread.currentThread().getId();
    }
}
public class ContextTest {

    public static void main(String[] args) {

        IntStream.range(1, 5)
                .forEach(i ->
                        new Thread(new ExecutionTask()).start()
                );
    }
}

The name query successful
The name query successful
The name query successful
The name query successful
The cardId query successful
The Name is Jack Thread-0 and CardId 44455511
The cardId query successful
The Name is Jack Thread-1 and CardId 44455512
The cardId query successful
The Name is Jack Thread-2 and CardId 44455513
The cardId query successful
The Name is Jack Thread-3 and CardId 44455514

问题:需要在每个调用Context的方法中传入进去

public void execute(Context context) {
}

3)使用ThreadLocal重新设计一个上下文设计模式

public final class ActionContext {

    private static final ThreadLocal<Context> threadLocal = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new Context();
        }
    };

    public static ActionContext getActionContext() {
        return ContextHolder.actionContext;
    }

    public Context getContext() {

        return threadLocal.get();
    }

    private static class ContextHolder {
        private final static ActionContext actionContext = new ActionContext();

    }
}
public class Context {

    private String name;
    private String cardId;

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class ExecutionTask implements Runnable {

    private QueryFromDBAction queryAction = new QueryFromDBAction();

    private QueryFromHttpAction httpAction = new QueryFromHttpAction();

    @Override
    public void run() {

        queryAction.execute();
        System.out.println("The name query successful");
        httpAction.execute();
        System.out.println("The cardId query successful");

        final Context context = ActionContext.getActionContext().getContext();
        System.out.println("The Name is " + context.getName() + " and CardId " + context.getCardId());
    }
}
public class QueryFromDBAction {

    public void execute() {

        try {
            Thread.sleep(1000L);
            String name = "Jack " + Thread.currentThread().getName();
            ActionContext.getActionContext().getContext().setName(name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class QueryFromHttpAction {

    public void execute() {
        Context context = ActionContext.getActionContext().getContext();
        String name = context.getName();
        String cardId = getCardId(name);
        context.setCardId(cardId);

    }

    private String getCardId(String name) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "444555" + Thread.currentThread().getId();
    }
}
public class ContextTest {

    public static void main(String[] args) {

        IntStream.range(1, 5)
                .forEach(i ->
                        new Thread(new ExecutionTask()).start()
                );
    }
}

The name query successful
The name query successful
The name query successful
The name query successful
The cardId query successful
The Name is Jack Thread-3 and CardId 44455514
The cardId query successful
The cardId query successful
The Name is Jack Thread-0 and CardId 44455511
The cardId query successful
The Name is Jack Thread-2 and CardId 44455513
The Name is Jack Thread-1 and CardId 44455512

这样写 执行过程中不会看到context的定义和声明

注意:在使用之前记得将上个线程中context旧值清除调,否则会重复调用(比如线程池操作)

4)ThreadLocal注意事项

脏数据

线程复用会产生脏数据。由于结程池会重用Thread对象,那么与Thread绑定的类的静态属性ThreadLocal变量也会被重用。如果在实现的线程run()方法体中不显式地调用remove() 清理与线程相关的ThreadLocal信息,那么倘若下一个结程不调用set() 设置初始值,就可能get() 到重用的线程信息,包括 ThreadLocal所关联的线程对象的value值。

内存泄漏

通常我们会使用使用static关键字来修饰ThreadLocal(这也是在源码注释中所推荐的)。在此场景下,其生命周期就不会随着线程结束而结束,寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收EntryValue就不现实了。如果不进行remove() 操作,那么这个线程执行完成后,通过ThreadLocal对象持有的对象是不会被释放的。

以上两个问题的解决办法很简单,就是在每次用完ThreadLocal时, 必须要及时调用 remove()方法清理。

父子线程共享线程变量

很多场景下通过ThreadLocal来透传全局上下文,会发现子线程的value和主线程不一致。比如用ThreadLocal来存储监控系统的某个标记位,暂且命名为traceId。某次请求下所有的traceld都是一致的,以获得可以统一解析的日志文件。但在实际开发过程中,发现子线程里的traceld为null,跟主线程的并不一致。这就需要使用InheritableThreadLocal来解决父子线程之间共享线程变量的问题,使整个连接过程中的traceId一致。

到此这篇关于Java多线程 ThreadLocal原理解析的文章就介绍到这了,更多相关Java多线程 ThreadLocal内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java多线程编程之ThreadLocal线程范围内的共享变量

    模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * 线程范围内的共享变量 * * 三个模块共享数据,主线程模块和AB模块 * * @author Administrator * */ public class ThreadScopeS

  • Java多线程编程中ThreadLocal类的用法及深入

    ThreadLocal,直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名. 早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用.其实它还是挺有用的,不相信的话,我们一起

  • java多线程编程之InheritableThreadLocal

    InheritableThreadLocal的作用: 当我们需要在子线程中使用父线程中的值得时候我们就可以像使用ThreadLocal那样来使用InheritableThreadLocal了. 首先我们来看一下InheritableThreadLocal的jdk源码: package java.lang; import java.lang.ref.*; public class InheritableThreadLocal<T> extends ThreadLocal<T> { p

  • Java多线程 ThreadLocal原理解析

    目录 1.什么是ThreadLocal变量 2.ThreadLocal实现原理 3.内存泄漏问题 4.使用场景 1)存储用户Session 2)解决线程安全的问题 3)使用ThreadLocal重新设计一个上下文设计模式 4)ThreadLocal注意事项 脏数据 内存泄漏 父子线程共享线程变量 1.什么是ThreadLocal变量 ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本. 这里有几点需要注意: 因为每个 T

  • Java ThreadLocal原理解析以及应用场景分析案例详解

    目录 ThreadLocal的定义 ThreadLocal的应用场景 ThreadLocal的demo TheadLocal的源码解析 ThreadLocal的set方法 ThreadLocal的get方法 ThreadLocalMap的结构 ThreadLocalMap的set方法 ThreadLocalMap的getEntry方法 ThreadLocal的内存泄露 如何避免内存泄露呢 应用实例 实际应用二 总结 ThreadLocal的定义 JDK对ThreadLocal的定义如下: The

  • Java transient关键字原理解析

    这篇文章主要介绍了Java transient关键字原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Java 中的 transient 关键字被用来表示变量将不被序列化处理.那么在理解 transient 关键字之前,我们先了解下什么是序列化. 什么是序列化 序列化是对象进行持久化处理,也就是说,将对象转化成一个字节流进行存储(比如存储为一个字节文件)或传输(通过网络传输字节).同时,我们也可以从字节中反序列化一个对象出来.这是Java

  • java char数据类型原理解析

    这篇文章主要介绍了java char数据类型原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java中的 char 数据类型使用 Unicode 编码,占用两个字节内存. 因为Unicode 采用无符号编码,一共可以存储 0x0000 ~ 0xffff 共65536 个字符, 而 int 是有符号4个字节,刚好一半是2个字节,所以在 java 将 char 看作整数(0-65535),于是我做了一个测试: //unicode 无符号编码

  • Java容器ArrayList原理解析

    这篇文章主要介绍了Java容器ArrayList原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 List是collection接口的实现类 List: 特点:有序,可重复 它有两个常用的实现类: 一.ArrayList: 特点:以数组的形式进行存储,因此随机访问速度较快,所有它适用于查询. 缺点:不适用于插入和删除的操作 因为每次操作都需要移动数组中的元素. 根据源码我们能得出以下几点: 1.ArrayList 在初始化的时候如果我们没

  • Java多线程volatile原理及用法解析

    首先volatile有两大功能: 保证线程可见性 禁止指令重排序 1.保证线程可见性 首先我们来看这样一个程序,其中不加volatile关键字运行的结果截然不同,加上volatile程序能够正常结束,不加则程序进入死循环: package com.designmodal.design.juc01; import java.util.concurrent.TimeUnit; /** * @author D-L * @Classname T001_volatile * @Version 1.0 *

  • Java原子操作CAS原理解析

    一.CAS(Compare And Set) Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数--内存位置(V).预期原值(A).新值(B).如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值.否则,处理器不做任何操作.无论哪种情况,它都会在CAS指令之前返回该位置的值.CAS有效地说明了"我认为位置V应该包含值A:如果包含该值,则将B放到这个位置:否则,不要更改该位置,只

  • Java多线程 ReentrantReadWriteLock原理及实例详解

    读写锁ReentrantReadWriteLock概述 读写锁ReentrantReadWriteLock,使用它比ReentrantLock效率更高. 读写锁表示两个锁,一个是读操作相关的锁,称为共享锁:另一个是写操作相关的锁,称为排他锁. 1.读和读之间不互斥,因为读操作不会有线程安全问题 2.写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题 3.读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题 多个Thread可以同时进行读取操作,但是同一时刻只允许一个

  • Java Number类原理实例解析

    这篇文章主要介绍了Java Number类原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Number类 内置数据类型:byte.int.long.double等 包装类:Integer.Long.Byte.Double.Float.Short. ​ 这种由编译器特别支持的包装称为装箱,所以当内置数据类型被当作对象使用的时候,编译器会把内置类型装箱为包装类.相似的,编译器也可以把一个对象拆箱为内置类型.Number 类属于 java.l

  • Java多线程的用法详细介绍

    Java多线程的用法详细介绍 最全面的Java多线程用法解析,如果你对Java的多线程机制并没有深入的研究,那么本文可以帮助你更透彻地理解Java多线程的原理以及使用方法. 1.创建线程 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例.Thread构造函数: public Thread( ); publi

随机推荐