Java J.U.C并发包(7) —— ThreadLocal类

时间:2021-07-03 20:51:43

这里会记录 《Java Concurrency in Practice》(java并发编程实战)的所有知识点哟~

因为ThreadLocal类比较重要,所以单独写一个博文来介绍这个类

1. 什么是ThreadLocal

ThreadLocal是一个关于创建线程局部变量的类

一般情况下,我们创建的变量时可以被任何一个线程访问并且修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改这个变量。因此ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作。尽管有两个线程执行同一段相同的代买,而且这段代码又有指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量域

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而同一个线程在任何时候访问这个本地变量的结果都是一致的。当此线程结束生命周期时,所有的线程本地实例都会被GC。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。ThreadLocal通常定义为private static类型。

2. 创建一个ThreadLocal对象

private ThreadLocal myThreadLocal = new ThreadLocal();

上述代码的功能是实例化一个ThreadLocal对象。每个线程仅需要实例化一次。虽然不同的线程执行同一段代码时,访问同一个ThreadLocal变量,但是每个线程只能看到私有的ThreadLocal实例。因此,不同线程在给ThreadLocal对象设置不同的值时,线程间也看不到彼此的修改。

3. 访问ThreadLocal对象 set()get()方法

一旦创建了一个ThreadLocal对象,就可以通过以下方式来存储次对象的值

myThreadLocal.set("A thread local value");

也可以直接读取一个ThreadLocal对象的值

String threadLocalValue = (String)myThreadLocal.get();

get()方法会返回一个Object对象,而set()方法则依赖一个Object对象参数

4. ThreadLocal泛型

为了使get()方法返回值不用做强制类型转换,通常可以创建一个泛型化的ThreadLocal对象。

private ThreadLocal myThread = new ThreadLocal<String>();

执行上述代码之后,就可以存储一个字符串到ThreadLocal实例中,并且当从此ThreadLocal实例中获取值的时候,就不必要做强制类型转换。

myThread.set("hello");
String s = myThread.get();

5. 初始化

因为ThreadLocal对象的set()方法设置的值只对当前线程可见,如果现在想让每一个线程的初始值都一样,就需要每个线程都要赋初值,那么重复的代码会变得很多。为此我们可以通过ThreadLocal子类来实现为所有的线程赋初值

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @override 
    protected String initialValue() {
        return "This is the initial value";
    }
}

此时,在set()方法调用之前,当调用get()方法的时候,所有线程都可以看到同一个初始化值。

InheritableThreadLocal

InheritableThreadLocal类是ThreadLocal的子类。为了解决ThreadLocal实例内部每个线程都只能看到自己的私有制,所以InheritableThreadLocal允许一个线程创建的所有子线程访问其父线程的值

一个完整的ThreadLocal示例

package thread;

public class ThreadLocalTest {

    public static class MyRunnable implements Runnable {

        private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
        private int a;
        @Override
        public void run() {
            threadLocal.set((int)(Math.random() * 100D));
            a = (int) (Math.random() * 100D);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            System.out.println("threadLocal.get():" + threadLocal.get());
            System.out.println("a:" + a);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);

        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

    }
}

上述代码的执行结果是:

threadLocal.get():36
a:14
threadLocal.get():3
a:14

可以看到threadLocal对象为两个进程设置了不同的值,虽然两个进程都调用了threadLocal.set((int)(Math.random() * 100D));方法,但是一个在设置其值的时候,对另一个进程没有修改。反过来看整型变量a,两个线程的变量a的值相同,说明最后执行的那个线程将a的值修改后覆盖了前面的线程对a的修改。

同时,如果调用set方法的时候使用synchronized关键字进行同步,而不是使用ThreadLocal对象实例,那么第二个线程将会覆盖第一个线程所设置的值。但是因为是ThreadLocal对象,所以两个线程无法看到彼此的值。所以两个线程可以得到不同的threadLocal的值

ThreadLocal是如何实现功能的

谈到ThreadLocal是如何实现它的功能的,那么首先应该想到看其源码,在这里只看部分ThreadLocal的源码,之后的文章中,会主要讲解J.U.C包中的常用类的源码。

1. 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);
}
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

上面的代码获取的是Thread对象的threadLocals变量

class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null; 
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

源码解析:

  • 首先获取当前线程
  • 利用当前线程作为句柄获取一个ThreadLocalMap的对象
  • 如果得到的ThreadLocalMap对象非空,则通过调用带有两个参数的set()方法修改其值,如果为空,说明没有这个对象,那就创建这个ThreadLocalMap对象并设置值

因此,从远吗可以看出,ThreadLocal的值是放在当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程没办法访问。

ThreadLocal变量存放的位置

首先要知道JVM将内存分为几个区域,其中包括两个比较重要的区域:栈和堆。

栈内存是线程私有区域,每个线程都会有有一个栈内存,其存储的变量只能自其所属的线程可见。
堆内存是线程共享区域,堆内存中的对象可对所有线程可见,堆内存中对象可以被所有线程访问。

那么根据以上分析,ThreadLocal变量是存放在栈上的吗?其实ThreadLocal变量不是存放在栈上的,因为ThreadLocal实例实际上也是被其创建的类私有,而ThreadLocal的值也是被线程实例持有,它们都位于堆上,只是通过一些技巧将可见性修改成线程可见

一些需要知道的ThreadLocal应用问题

1. ThreadLocal与连接池connection问题

问题1:有一个用户请求就会启动一个线程。而如果ThreadLocal用的是变量副本,那我们把connection放在ThreadLocal里的话,那么我们的程序只需要一个connection连接数据库就行了,每个线程都是用的connection的一个副本,那为什么还有必要要数据库连接池呢?

ThreadLocal使得各县城能够保持各自独立的对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的map中,每个线程都有一个这样的map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象。ThreadLocal实例是作为map的key来使用的。

问题2:既然ThreadLocal当当前线程中没有时去创建一个新的,有的话就用当前线程中的,那数据库连接池已经有了这个功能,还要ThreadLocal干什么?

由于请求中的一个事务涉及多个DAO操作,而这些DAO中的Connection不能从连接池中获得,如果是从连接池获得的话,两个DAO就用到了两个Connection,就没有办法完成一个事务。DAO中的Connection如果是从ThreadLocal中获得Connection的话那么这些DAO就会被纳入到同一个Connection下。但是,DAO中不能把connection关闭,如果关闭,下一个使用者就不能使用了。

ThreadLocal与同步机制

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal则从另一个角度来解决多线程的并发问题。ThreadLocal回味每个线程提供一个独立的变量副本(每个线程创建一个,不是对象的拷贝),从而隔离了多个线程对数据的访问冲突。因为每个线程都拥有自己的变量副本,从而没有必要对该线程进行同步。ThreadLocal提供线程安全的共享对象,在编写多线程代码的时候,可以把不安全的变量封装进ThreadLocal。

对于多线程资源共享问题:
- 同步机制采用了“以时间换空间”的方法,提供一份变量,让不同的线程排队访问
- ThreadLocal采用了“以空间换时间”的方式,为每个线程都提供了一份变量,因此可以同时访问而互不影响

感谢

[1] http://www.sczyh30.com/posts/Java/java-concurrent-threadlocal/

[2] https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/

[3] 并发编程网http://ifeve.com/java-concurrency-thread-directory/

[4] https://blog.csdn.net/sup_heaven/article/details/30094187