java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java ThreadLocal原理

深入浅出解析Java ThreadLocal原理

作者:ATFWUS

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,感兴趣的朋友快来看看吧

分享一下最近看的ThreadLocal的源码的一些体会。

1.了解ThreadLocal

简介

在这里插入图片描述

使用

/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/8 21:23
 * @description
 */
@Slf4j
public class ThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for(int i = 0; i < 1000; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    localStr.set(Thread.currentThread().getName() + " localStr");
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(localStr.get());
                }
            }, "t" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }
}

在这里插入图片描述

而对于普通变量来说,很明显是存在线程安全问题的:

/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/8 21:23
 * @description
 */
@Slf4j
public class ThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();
    static String shareStr;

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for(int i = 0; i < 1000; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    shareStr = Thread.currentThread().getName() + "  shareStr";
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(shareStr);
                }
            }, "t" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }
}

在这里插入图片描述

2.源码解析 – 探究实现思路

threadLocals变量与ThreadLocalMap

在这里插入图片描述

在这里插入图片描述

set(T value) 方法

    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() 方法

  1. 进入get()方法后,首先获取当前线程,然后进入getMap(Thread t)中获取ThreadLocalMap对象,直接返回t.threadLocals。
  2. 如果map不为空,直接返回map中当前ThreadLocal作为键对应的值。
  3. 如果map为空,需要先进行初始化。调用setInitialValue()方法进行初始化。
    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();
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    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;
    }
    protected T initialValue() {
        return null;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

remove() 方法

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

实现思路总结

在这里插入图片描述

3.InheritableThreadLocal与继承性

InheritableThreadLocal英语翻译一下就是可继承的ThreadLocal,让我们看下它和ThreadLocal的继承性体现在哪。

这里的继承性指的是:子线程是否能访问父线程的变量。

ThreadLocal的不可继承性

threadLocals是当前线程的成员变量,在子线程中不可见

/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/9 14:29
 * @description
 */
@Slf4j
public class InheritableThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();

    public static void main(String[] args) {
        localStr.set("main线程为其设置的值");
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("访问localStr : " + localStr.get());
            }
        }).start();
        System.out.println(localStr.get());
    }
}

在这里插入图片描述

InheritableThreadLocal实现继承性的源码剖析

看一下InheritableThreadLocal的源码:

在这里插入图片描述

源码非常简短,下面简单分析一下:

从Thread的构造方法看,发现所有的构造方法都会调用init()方法进行初始化,init()方法有两个重载形式。

在这里插入图片描述

我们进入参数较多的init方法查看一下:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
				// 新线程还未创建出来,当前线程就是即将要创建线程的父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        // 如果父线程的inheritThreadLocals 不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        		 // 设置子线程中的inheritableThreadLocals设置为父线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

我们重点看一下和inheritThreadLocals相关的地方(含注释的地方)

再看下ThreadLocal.createInheritedMap方法:

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

看下这个构造方法:

        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        // 调用inheritThreadLocals重写的childValue方法
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

如何理解这个继承性

通过上面的源码分析,可以发现,InheritableThreadLocal的继承性主要体现在:创建子线程时,会将父线程的inheritThreadLocals里面所有entry拷贝一份给子进程。

那么当子进程被创建出来之后,父进程又修改了inheritThreadLocals里面的值,这个操作是否对子线程可见,通过上面的源码可知,这个操作明显是不可见的,下面有个demo可以证实。

/**
 * @author ATFWUS
 * @version 1.0
 * @date 2021/11/9 14:29
 * @description
 */
@Slf4j
public class InheritableThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();
    static InheritableThreadLocal<String> inheritableLocalStr = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        inheritableLocalStr.set("main线程第一次为inheritableLocalStr设置的值");
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("子线程第一次访问inheritableLocalStr : " + inheritableLocalStr.get());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("子线程第二次访问inheritableLocalStr : " + inheritableLocalStr.get());
            }
        }).start();
        Thread.sleep(500);
        inheritableLocalStr.set("main线程第二次为inheritableLocalStr设置的值");
        log.debug("main线程第二次为inheritableLocalStr赋值");
        Thread.sleep(1000);
    }
}

看下输出:

在这里插入图片描述

可以发现,子线程创建出来后,对父线程中inheritThreadLocals的修改操作,对子线程不可见。

总结

4.存在的内存泄露问题

要充分理解ThreadLocal中存在的内存泄露问题,需要有以下JVM对内存管理的前置知识(这里篇幅问题就不补充了):

在分析上述ThreadLocalMap源码的时候,注意到有一个小细节,ThreadLocalMap的Entry继承了WeakReference<ThreadLocal<?>>,也就是说Entry的key是一个对ThreadLocal<?>的弱引用。问题来了,为什么这里要使用弱引用呢?

在这里插入图片描述

使用强引用会如何?

现在假设Entry的key是一个对ThreadLocal的强引用,当ThreadLocal对象使用完后,外部的强引用不存在,但是因为当前线程对象中的threadLocals还持有ThreadLocal的强引用,而threadLocals的生命周期是和线程一致的,这个时候,如果没有手动删除,整个Entry就发生了内存泄露。

使用弱引用会如何?

现在假设Entry的key是一个对ThreadLocal的弱引用,当ThreadLocal对象使用完后,外部的强引用不存在,此时ThreadLocal对象只存在Entry中key对它的弱引用,在下次GC的时候,这个ThreadLocal对象就会被回收,导致key为null,此时value的强引用还存在,但是value已经不会被使用了,如果没有手动删除,那么这个Entry中的key就会发生内存泄露。

使用弱引用还有一些好处,那就是,当key为null时, ThreadLocalMap中最多存在一个key为null,并且当调用set(),get(),remove()这些方法的时候,是会清除掉key为null的entry的。

set()、get()、remove() 方法中相关实现

通过这个方法的源码可以看出,key为null的那个entry实际上迟早会被替换成新的entry。

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }
							 // 发现key为空
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

同理,可以看到在get方法中也存在:

        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;
                // 替换过时的entry
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

remove() 方法中也是一样:

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 清除过时的key
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

总结

5.ThreadLocal应用

ThreadLocal把数据存放到线程本地,解决了线程安全问题,没有使用锁,直接访问线程本地变量,效率较高(空间换时间。)
同时threadLocals的生命周期是和线程一致的,可以解决很多参数传递问题。

ATFWUS 2021-11-11

以上就是深入浅出解析Java ThreadLocal原理的详细内容,更多关于Java ThreadLocal原理的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文