`
frank-liu
  • 浏览: 1682341 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java concurrency: ThreadLocal及其实现机制

 
阅读更多

ThreadLocal概念

        从字面上来理解ThreadLocal,感觉就是相当于线程本地的。我们都知道,每个线程在jvm的虚拟机里都分配有自己独立的空间,线程之间对于本地的空间是相互隔离的。那么ThreadLocal就应该是该线程空间里本地可以访问的数据了。ThreadLocal变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。

        很多人看到这里会容易产生一种错误的印象,感觉是不是这个ThreadLocal对象建立了一个类似于全局的map,然后每个线程作为map的key来存取对应线程本地的value。你看,每个线程不一样,所以他们映射到map中的key应该也不一样。实际上,如果我们后面详细分析ThreadLocal的代码时,会发现不是这样的。它具体是怎么实现的呢?后面的详细实现分析部分会讲到这个部分。先别急,看看它是怎么用的吧。

 

应用和好处

        我们在多线程的开发中,经常会考虑到的策略是对一些需要公开访问的属性通过设置同步的方式来访问。这样每次能保证只有一个线程访问它,不会有冲突。但是这样做的结果会使得性能和对高并发的支持不够。在某些情况下,如果我们不一定非要对一个变量共享不可,而是给每个线程一个这样的资源副本,让他们可以独立都各自跑各自的,这样不是可以大幅度的提高并行度和性能了吗?

        还有的情况是有的数据本身不是线程安全的,或者说它只能被一个线程使用,不能被其他线程同时使用。如果等一个线程使用完了再给另外一个线程使用就根本不现实。这样的情况下,我们也可以考虑用ThreadLocal。一个典型的情况就是我们连接数据库的时候通常会用到连接池。而对数据库的连接不能有多个线程共享访问。这个时候就需要使用ThreadLocal了。一个典型的用法如下:

 

private static ThreadLocal<Connection> connectionHolder = 
    new ThreadLocal<Connection>() {
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };


pubic static Connection getConnection() {
    return connectionHolder.get();
}

        ThreadLocal类本身定义了有get(), set()和initialValue()三个方法。前面两个方法是public的,initialValue()是protected的,主要用于我们在定义ThreadLocal对象的时候根据需要来重写。这样我们初始化这么一个对象在里面设置它的初始值时就用到这个方法。

        ThreadLocal变量因为本身定位为要被多个线程来访问,它通常被定义为static变量。除了这个示例,在一些开源的j2ee容器以及spring框架中都有应用到。网上可以找到大量介绍的东西,这里就不在赘述。

 

 

具体实现细节分析

Thread和ThreadLocal的关系

        好吧,现在进入刨根究底时间。ThreadLocal它到底是怎么实现的呢?我们先看看Thread本身的定义。在Thread.java的声明代码中,我们可以看到有这么一部分代码:

 

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

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

 

        这就说明了其实每个Thread本身就包含了两个ThreadLocalMap对象的引用。这一点非常重要。以后每个thread要访问他们的local对象时,就是访问存在这个ThreadLocalMap里的value。

ThreadLocalMap

        那么这个ThreadLocalMap是个什么东西呢?从字面上可以猜出来,它是一个map。没错,一个map。在ThreadLocal.java中,它是一个内部类。它是以ThreadLocal为key,我们存储的对象为Value的map. 下面是它被删节后的部分定义代码:

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);
        }
}
 这里面牵涉到一个map的实现细节。这里面封装了一个Entry的列表,在Entry里存放的就是key和value。具体是如何从key映射到value的方法和通用的HashMap实现方法类似,在这里就不在赘述。主要知道有了这么一个map,我们给它一个ThreadLocal的对象,它就可以找到对应的value.

从get()入手

        我们看看get方法的实现以及它关联的方法:

public T get() {
        Thread t = Thread.currentThread(); // 获得当前的线程
        ThreadLocalMap map = getMap(t);  //取得当前线程关联的map
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

        在代码中间我增加了一些注释。这里比较有意思的一个地方就是getMap()方法。我们首先在获得当前线程的情况下,然后去取得当前线程的ThreadLocalMap。getMap方法做的就是取得ThreadLocalMap这个事。它的定义如下:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}
         看到这里,我想各位已经明白了。原来get方法就是获取到当前的线程,在找到这个线程本身关联的map来折腾。你想想,既然每个线程都有各自独立的map,我也只是针对线程本身的map来操作,肯定相互之间不会有干扰了。

        get()方法后面的map.getEntry()方法,无疑就是通过map来取这个对应的封装值了。Entry的实现里对这个要访问的值做了一点封装,所以后面返回的是e.value.map.getEntry()方法的实现如下:

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

它就是一个查找和映射的过程,具体的细节和HashMap差不多,这里就不做重点说了。

        我们再来看后面的return setInitialValue();这一句是在如果前面找到的map为空或者找到的映射实体为空的话,我们会来设置它的初始值。setInitialValue的定义如下:

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

        它调用initialValue方法获得初始值。然后判断情况,是我们映射的实体为空呢还是map为空,如果实体为空的话,我们就直接根据得到的初始值给它设上去,否则我们就新建一个map。createMap()的方法就比较简单,就是一个直接的new:

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

 

        从前面这两部分的代码,我们可以看到。ThreadLocal中的get方法在首先调用get()方法的时候,会去调用initialValue()方法获取一下初始值。这也就是为什么前面说到推荐我们覆写initialValue()方法来设置自己期望的值。另外,在这里也会为每个线程建立它本地的map对象。

 

再看set()

        把前面get()方法的流程理清之后,再来看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()的差不多,就不啰嗦了。

 

一个有意思的地方

        为什么要通常将ThreadLocal对象声明为static的呢?一方面是为了多个线程共享方便。另外,由于每个Thread都有这么个ThreadLocalMap对象的引用,每次在ThreadLocal中执行Get方法的时候,实际上就是根据当前线程来获取它的ThreadLocalMap对象。再将这个ThreadLocal对象为Key来查找对应的值。因为每个Thread各自的ThreadLocalMap,所以相当于每个对象对应这么一个同样的ThreadLocal对象key值,来放一份自己本身的拷贝。

        我们可能还有一个疑问就是既然如果我们声明一个ThreadLocal对象相当于每个关联访问的Thread有了一个该对象对应的key和value对,为什么每个对象要放这么一个Map呢?这是考虑到如果有多个ThreadLocal对象在被多个线程使用的情况。ThreadLocal类中间有这么一部分代码:

private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
 

ThreadLocal中的static变量nextHashCode相当于一个全局的私有变量,但是threadLocalHashCode是针对每个对象的实例成员,每次它被初始化的时候都要调用nextHashCode()方法。这个方法是static的,在成员初始化的时候也就运行一次。它就是为了将这个值增加一段,保证这个对象的threadLocalHashCode和其他ThreadLocal对象的不一样。因为最终将ThreadLocal对象映射到map中的值是用的threadLocalHashCode。所以,当我们多个线程要访问多个ThreadLocal变量的时候,每个变量映射到的就是ThreadLocalMap中不同的项。

总结

每个线程都有一个map,这个map里存的就是和该线程关联的本地数据。可能这个map是空的。在通过访问ThreadLocal的方法时,通过和ThreadLocal对象建立关联来映射到对应的本地对象。这个ThreadLocal对象相当于是map的key,放的本地变量的值相当于map里的value.ThreadLocal相当于一个帮助类,为每个访问的线程建立本地的拷贝数据。

 

参考资料:

java concurrency in practice

轻松使用线程: 不共享有时是最好的

openjdk

分享到:
评论

相关推荐

    Java Concurrency in Practice CHM版本

    3. **同步机制**:Java提供了多种同步机制,包括synchronized关键字、java.util.concurrent.locks包下的Lock接口及其实现,以及java.util.concurrent.atomic包中的原子变量类。这些机制有助于控制并发访问,确保线程...

    Java Concurrency in Practice Java并发编程

    - 理解线程的优先级及其在Java中的实现,以及线程调度策略对并发程序的影响。 《Java Concurrency in Practice》这本书通过大量实例和深入分析,帮助读者理解和掌握Java并发编程的核心概念和最佳实践,是Java...

    《Java Concurrency in Practice》源码

    2. **并发工具类**:Java Concurrency API提供了丰富的并发工具类,如ExecutorService、Semaphore、CountDownLatch、CyclicBarrier等,这些工具可以帮助我们更好地管理和协调线程,实现更高效的并发程序。...

    java_concurrency_in_practice:《Java并发编程实战》学习Demo

    同时,Java提供多种同步机制,如`synchronized`关键字、`volatile`变量、`Lock`接口及其实现。 2. **线程安全** - **线程不安全**:当多个线程访问同一资源时,如果没有正确同步,可能导致数据不一致或死锁等问题...

    java并发编程经典书籍(英文版)

    - **同步机制**:详细讲解了synchronized关键字,wait/notify机制,以及如何使用这些机制实现线程间的通信和协作。 - **并发工具类**:在JDK 1.5之前,书中介绍了Semaphore、Barrier等并发工具,它们用于控制对...

    JAVA程序员面试宝典 第4版(alin书签).pdf

    - 类加载机制:了解类加载过程,双亲委派模型和自定义类加载器的实现。 7. **设计模式** - 23种设计模式:逐一讲解单例、工厂、装饰、代理、观察者等经典设计模式及其应用场景。 8. **数据库与SQL** - JDBC:...

    java后端工程师面试题

    - 同步机制:掌握synchronized关键字、Lock接口及其实现类,如ReentrantLock,以及volatile关键字的作用。 - 线程池:了解ExecutorService、ThreadPoolExecutor和Future接口,以及线程池参数的调整。 4. **IO流**...

    java 并发编程实践

    本篇文章将深入探讨Java并发编程的相关知识点,主要基于提供的两个文件——"Java并发编程实战(中文版).pdf"和"Java Concurrency in Practice.pdf"。 1. **线程与并发** - **线程基础**:Java中的线程是并发执行...

    Java并发编程实践.pdf

    为了解决线程安全问题,Java提供了以下几种机制: - **synchronized关键字**:用于修饰方法或代码块,确保同一时刻只有一个线程可以执行被synchronized修饰的代码。 - **volatile关键字**:用于修饰变量,确保变量的...

    28个java常用的工具类源码

    以上知识点涵盖了Java开发中的常用工具类及其核心功能,通过学习和理解这些源码,开发者可以更好地掌握Java编程的精髓,提高代码质量和效率。同时,理解这些工具类的实现也有助于应对面试中的技术问题。

    concurrency.zip

    5. **阻塞队列** - `BlockingQueue` 接口及其实现如 `ArrayBlockingQueue`, `LinkedBlockingQueue` 等,用于线程间的通信和协作。它们在生产者-消费者模式中扮演重要角色,可以避免线程之间的直接交互,提高程序的...

    JUC并发编程与源码分析视频课.zip

    《JUC并发编程与源码分析视频课》是一门深入探讨Java并发编程的课程,主要聚焦于Java Util Concurrency(JUC)库的使用和源码解析。JUC是Java平台提供的一组高级并发工具包,它极大地简化了多线程编程,并提供了更...

    JavaWeb并发编程与高并发解决方案.docx

    - J.U.C(Java Util Concurrency):Java并发工具包,提供多种并发控制工具类。 - AQS(AbstractQueuedSynchronizer):抽象队列同步器,许多并发工具的基础。 - ThreadLocal:线程局部变量,用于解决线程间的...

    JUC

    Java并发编程库(Java Concurrency Utility,简称JUC)是Java平台中用于高效并发处理的重要组件,主要包含在`java.util.concurrent`包及其子包下。JUC为开发者提供了丰富的并发工具类,使得多线程编程更加安全、高效...

Global site tag (gtag.js) - Google Analytics