Java进阶篇--并发容器之ThreadLocal

目录

ThreadLocal的简介

ThreadLocal的实现原理

ThreadLocalMap详解

Entry的数据结构

set()方法

getEntry()方法

remove()方法

ThreadLocal的应用场景 


ThreadLocal的简介

ThreadLocal可以被理解为线程的本地变量。它提供了一种将变量与线程关联起来的机制,使得每个线程都可以拥有自己独立的变量副本,互不干扰。

在多线程编程中通常解决线程安全的问题,我们会利用synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。并且共享变量的访问往往是一个潜在的线程安全问题。通过使用ThreadLocal,我们可以为每个线程创建一个独立的变量副本,使得每个线程都可以独立地操作该变量,而不会对其他线程产生影响。这种方式实际上是通过空间换时间的方式,通过增加内存占用来避免线程间的竞争和同步机制。

ThreadLocal类实际上是一个容器,内部维护了一个ThreadLocalMap,其中的键值对以线程作为键,变量副本作为值。每个线程访问ThreadLocal变量时,实际上是在操作自己的变量副本,线程之间互不干扰。

需要注意的是,使用ThreadLocal时要注意及时清理不再使用的变量副本,以避免内存泄漏。通常在使用完ThreadLocal后,应该调用其remove方法移除当前线程的变量副本。

总结起来,ThreadLocal提供了一种简便的方式,使得每个线程都可以拥有自己独立的变量副本,实现线程间的数据隔离,从而避免了一些多线程环境下的竞争和同步问题。这是一种以空间换时间的策略,通过增加内存占用来提高程序的并发性能和线程安全性。

ThreadLocal的实现原理

ThreadLocal的实现原理是通过每个线程维护一个ThreadLocalMap来实现。ThreadLocal是一个泛型类,通过调用set方法将值存放在当前线程的ThreadLocalMap中,以ThreadLocal实例为key,值为value进行存储。而get方法则是通过ThreadLocal实例作为key从ThreadLocalMap中获取对应的值。

具体实现原理如下:

1. ThreadLocal类中定义了一个静态内部类ThreadLocalMap,该类是ThreadLocal的一个成员变量,用于存储线程局部变量。

2. 在调用ThreadLocal的set方法时,会首先获取当前线程的ThreadLocalMap对象。如果ThreadLocalMap对象存在,则以当前ThreadLocal实例为key,将value存储在ThreadLocalMap中。如果ThreadLocalMap对象不存在,则创建一个新的ThreadLocalMap对象,并将其与当前线程关联。

3. 在调用ThreadLocal的get方法时,同样会先获取当前线程的ThreadLocalMap对象。然后通过ThreadLocal实例作为key,从ThreadLocalMap中取出对应的值。

4. 在调用ThreadLocal的remove方法时,会先获取当前线程的ThreadLocalMap对象,然后从ThreadLocalMap中删除以当前ThreadLocal实例为key的键值对。

总结一下,ThreadLocal的实现原理就是通过每个线程维护一个ThreadLocalMap,使用ThreadLocal实例作为key,将值存储在ThreadLocalMap中,实现了线程间的隔离和数据的独立访问。这样就保证了每个线程都可以独立地访问自己的ThreadLocal变量,而不会受到其他线程的干扰。

public class main {
    // 创建一个ThreadLocal对象,用于存储线程局部变量
    private static ThreadLocal threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 在主线程中设置ThreadLocal的值
        threadLocal.set("MainValue");

        // 创建两个子线程并启动
        Thread thread1 = new Thread(new MyRunnable("Thread1"));
        Thread thread2 = new Thread(new MyRunnable("Thread2"));
        thread1.start();
        thread2.start();

        try {
            // 等待两个子线程执行完毕
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 获取主线程中的ThreadLocal的值并打印
        System.out.println("主线程中的ThreadLocal的值:" + threadLocal.get());

        // 调用remove方法清除主线程中的ThreadLocal的值
        threadLocal.remove();

        // 再次获取主线程中的ThreadLocal的值并打印
        System.out.println("主线程中的ThreadLocal的值:" + threadLocal.get());
    }

    // 自定义Runnable类,用于在每个线程中访问ThreadLocal变量
    private static class MyRunnable implements Runnable {
        private String name;

        public MyRunnable(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            // 在当前线程中设置ThreadLocal的值
            threadLocal.set(name + "Value");

            // 在当前线程中获取ThreadLocal的值并打印
            System.out.println(name + "线程中的ThreadLocal的值:" + threadLocal.get());

            // 调用remove方法清除当前线程中的ThreadLocal的值
            threadLocal.remove();

            // 再次获取当前线程中的ThreadLocal的值并打印
            System.out.println(name + "线程中的ThreadLocal的值:" + threadLocal.get());
        }
    }
}

代码分析:

  1. 在ThreadLocalExample类中,创建一个ThreadLocal对象 threadLocal,用于存储字符串类型的线程局部变量。
  2. 在main方法中,首先在主线程中使用threadLocal.set("MainValue")设置threadLocal的值为"MainValue"。
  3. 创建两个子线程 thread1 和 thread2,并分别传入不同的名称参数。
  4. 在自定义的MyRunnable类中,构造方法接收一个名称参数,并将其保存在私有字段中。
  5. 在run方法中,首先在当前线程中使用threadLocal.set(name + "Value")设置threadLocal的值为当前线程名和"Value"拼接而成的字符串。
  6. 然后在当前线程中使用threadLocal.get()获取threadLocal的值,并将其打印出来。
  7. 在当前线程中调用threadLocal.remove()方法,清除当前线程中的threadLocal的值。
  8. 再次在当前线程中使用threadLocal.get()获取threadLocal的值,并将其打印出来。
  9. 在main方法中,启动两个子线程并等待它们执行完毕。
  10. 获取主线程中的threadLocal的值,并打印出来。
  11. 调用threadLocal.remove()方法,清除主线程中的threadLocal的值。
  12. 再次获取主线程中的threadLocal的值,并打印出来。

注意:使用了多线程来访问和设置ThreadLocal变量。由于多线程的执行顺序和调度是不确定的,因此每次运行的结果可能会不同。

因为在多线程环境下,每个线程都有自己的副本,称为线程局部变量。而ThreadLocal对象就是用来存储线程局部变量的。在代码中,主线程设置了threadLocal的值为"MainValue",而两个子线程分别设置了threadLocal的值为"Thread1Value"和"Thread2Value"。

由于线程调度的不确定性,不同的线程可能会以不同的顺序运行,这导致了每次运行的结果可能会不同。例如,在某一次运行中,主线程先执行,然后是Thread1线程,最后是Thread2线程;而在另一次运行中,可能是Thread2线程先执行,然后是Thread1线程,最后是主线程。

因此,每次运行的输出结果取决于各个线程的执行顺序和调度情况,所以会出现不一样的结果。

ThreadLocalMap详解

ThreadLocalMap是ThreadLocal的核心实现类,用于存储线程局部变量。ThreadLocalMap是一个自定义的哈希表,它使用了开放地址法来解决哈希冲突。每个Thread对象都有自己的ThreadLocalMap对象,用于存储线程局部变量。

ThreadLocalMap中的每个元素都是一个Entry对象,Entry包含了线程局部变量的键值对,其中键是ThreadLocal对象,值是线程局部变量的值。Entry内部还包含了一些相关的操作方法,例如get()方法获取线程局部变量的值,set()方法设置线程局部变量的值等。下面分别介绍Entry的数据结构、set()方法、getEntry()方法和remove()方法。

Entry的数据结构

static class Entry extends WeakReference> {
    /** 与此 ThreadLocal 关联的值。*/
    Object value;

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

可以看到,Entry类继承了WeakReference>,通过继承WeakReference来实现对ThreadLocal对象的弱引用,以便在不需要时可以被垃圾回收。value字段存储线程局部变量的值。 

set()方法

public void set(ThreadLocal key, Object value) {
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(Thread.currentThread());
    if (map != null)
        // 如果ThreadLocalMap不为null,则将key和value放入map中
        map.set(key, value);
    else
        // 如果ThreadLocalMap为null,则创建一个新的ThreadLocalMap对象,并将key和value放入其中
        createMap(key, value);
}

set()方法用于设置线程局部变量的值,首先通过getMap(Thread.currentThread())方法获取当前线程的ThreadLocalMap对象,如果该对象不为null,则将键值对放入ThreadLocalMap中;否则,调用createMap(key, value)方法创建一个新的ThreadLocalMap对象,并将键值对放入其中。 

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

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

getEntry()方法用于获取与指定ThreadLocal对象关联的Entry对象。首先通过key.threadLocalHashCode & (table.length - 1)计算哈希值,然后在table数组中查找对应位置上的Entry对象。如果找到的Entry对象不为null且其引用的ThreadLocal对象与指定ThreadLocal对象相同,则返回该Entry对象;否则,调用getEntryAfterMiss(key, i, e)方法在table数组中的其他位置继续查找。

如果在查找的过程中发现Entry对象的引用为null,则需要调用expungeStaleEntry(i)方法将table数组中对应位置的Entry对象置为null。如果最终仍然没有找到与指定ThreadLocal对象关联的Entry对象,则返回null。

remove()方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

remove()方法用于清除当前线程中指定ThreadLocal对象的值。首先通过getMap(Thread.currentThread())方法获取当前线程的ThreadLocalMap对象,如果该对象不为null,则调用m.remove(this)方法将指定ThreadLocal对象的值清除掉;否则,不做任何操作。

ThreadLocal的应用场景 

ThreadLocal的主要作用是提供线程局部变量,即每个线程都拥有自己的变量副本,互相之间不会相互干扰。 ThreadLocal 的使用场景如下:

  • 并发控制:在多线程环境下,可以使用ThreadLocal来存储每个线程独有的变量,避免线程间数据的共享和竞争条件。
  • 上下文信息传递:在跨多个方法或模块的代码中,可以使用ThreadLocal传递上下文信息,而不必显式地传递参数。这在某些框架和工具,如Web应用程序中的请求上下文或用户信息等方面特别有用。
  • 数据库连接管理:在多线程环境下,使用ThreadLocal可以为每个线程维护一个独立的数据库连接,确保线程安全和高效的数据库操作。
  • 事务管理:在基于线程的事务管理中,可以使用ThreadLocal存储事务上下文,使得不同的线程能够独立地进行事务处理,而不会冲突。
  • 线程安全的日期格式化:SimpleDateFormat是非线程安全的,但可以通过ThreadLocal在每个线程中创建一个独立的SimpleDateFormat实例,以便进行日期的格式化和解析。
  • 缓存管理:可以使用ThreadLocal来缓存一些需要在同一线程中共享的数据,避免频繁的计算或查询操作。

需要注意的是,在使用ThreadLocal时,要避免内存泄漏问题。因为ThreadLocal弱引用的特性,如果没有及时清理线程的引用,可能会导致对象无法回收,造成内存泄漏。因此,在不再使用ThreadLocal时,要调用remove()方法主动清除数据,或者使用ThreadLocal的initialValue()方法设置初始值,以确保资源能够正确释放。

你可能感兴趣的:(Java进阶篇,java,开发语言)