ThreadLocal及InheritableThreadLocal的原理剖析

我们知道,线程的不安全问题,主要是由于多线程并发读取一个变量而引起的,那么有没有一种办法可以让一个变量是线程独有的呢,这样不就可以解决线程安全问题了么。其实JDK已经为我们提供了ThreadLocal这个东西。

ThreadLocal基本使用

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal 的主要方法有这么几个:

  • initialValue 初始化

  • set 赋值

  • get 取值

  • remove 清空

下面来看一个简单的使用代码示例:

public class ThreadLocalDemo {	
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {	
        @Override	
        public Integer initialValue() {	
            return 0;	
        }	
    };	
    static class ThreadDemo implements Runnable {	
        @Override	
        public void run() {	
            for (int i = 0; i < 1000; i++) {	
                threadLocal.set(threadLocal.get() + 1);	
            }	
            System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());	
        }	
    }	
    public static void main(String[] args) {	
        for (int i = 0; i < 10; i++) {	
            new Thread(new ThreadDemo()).start();	
        }	
    }	
}

上方代码使用了10个线程循环对一个threadLocal的值进行一千次的加法,如果我们不知道ThreadLocal的原理的话我们可能会觉得最后打印的值一定是1000、2000、3000。。10000或者是线程不安全的值。

但是如果你执行这段代码你会发现最后打印的都是1000。

ThreadLocal原理剖析

现在我们来看一下ThreadLocal是如何实现为每个线程单独维护一个变量的呢。

先来看一下初始化方法。

protected T initialValue() {	
        return null;	
    }

initialValue 默认是返回空的,所以为了避免空指针问题重写了这个方法设置了默认返回值为0,但是呢,虽然这个方法好像是设置默认值的,但是还没有生效,具体请接着往下看。

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

我们可以看到set方法首先会获取当前线程,然后通过一个getMap方法获取了ThreadLocalMap,接着来看一下这个map是怎么来的呢。

 ThreadLocalMap getMap(Thread t) {	
        return t.threadLocals;	
    }

这个map是在Thread类维护的一个map,下方是Thread类维护的此变量。默认这个map是空的。

ThreadLocal.ThreadLocalMap threadLocals = null;

接着往下看代码,如果获取的时候map不为空,则通过set方法把Thread类的threadLocals变量更新。如果是第一次创建的时候则初始化Thread的threadLocals变量。

下方是createMap的代码:

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

接下来看个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();	
    }

注意关注最后的一个return,看到调用的这个方法名我们就可以发现这个ThreadLocal的初始化原来是当第一调用get方法时如果还没有被set的时候才会去获取initialValue 方法的返回值。

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

使用ThreadLocal最应该注意的事项

首先来看一下线程退出的办法:

private void exit() {	
        if (group != null) {	
            group.threadTerminated(this);	
            group = null;	
        }	
        target = null;	
        threadLocals = null;	
        inheritableThreadLocals = null;	
        inheritedAccessControlContext = null;	
        blocker = null;	
        uncaughtExceptionHandler = null;	
    }

我们看到当线程结束的时候上方第7行会把ThreadLocal的值制为空,这个东西本身是没问题的。但是,如果你是使用的线程池,这个问题可就大了!!!

要知道线程池里的线程执行完一个任务之后紧接着下一个,这中间线程可不会结束,下一个任务获得Thread的值可是上一个任务的遗留数据。

下面是这个问题的示例代码:

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {	
        @Override	
        public Integer initialValue() {	
            return 0;	
        }	
    };	
    static class ThreadDemo implements Runnable {	
        @Override	
        public void run() {	
            for (int i = 0; i < 1000; i++) {	
                threadLocal.set(threadLocal.get() + 1);	
            }	
            System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());	
            //threadLocal.remove();	
        }	
    }	
    public static void main(String[] args) {	
        ExecutorService executorService= Executors.newFixedThreadPool(5);	
        for (int i = 0; i < 10; i++) {	
            executorService.submit(new Thread(new ThreadDemo()));	
        }	
    }

执行这段代码你就会发现同样的操作在线程池里已经得不到一样的结果了。想要解决这种问题也很简单,只需要把ThreadLocal的值在线程执行完清空就可以了。把第14行注释的代码放开再执行以下你就明白了。

InheritableThreadLocal

其实ThreadLocal还有一个比较强大的子类InheritableThreadLocal,它呢可以把父线程生成的变量传递给子线程。

下面来看一下代码示例:

public class InheritableThreadLocalDemo {	
    private static  InheritableThreadLocal<Integer> inheritableThreadLocal = new  InheritableThreadLocal<Integer>();	
    static class ThreadDemo implements Runnable {	
        @Override	
        public void run() {	
            for (int i = 0; i < 1000; i++) {	
                inheritableThreadLocal.set(inheritableThreadLocal.get() + 1);	
            }	
            System.out.println("thread :" + Thread.currentThread().getId() + " is" + inheritableThreadLocal.get());	
        }	
    }	
    public static void main(String[] args) {	
        inheritableThreadLocal.set(24);	
        for (int i = 0; i < 10; i++) {	
            new Thread(new ThreadDemo()).start();	
        }	
    }	
}

执行代码会发现程序输出全是1024,这就是因为InheritableThreadLocal吧在主线程设置的值24传递到了那10个子线程中。

InheritableThreadLocal原理剖析

接下来我们来看一下InheritableThreadLocal为什么可以实现这种功能呢。

InheritableThreadLocal是ThreadLocal的子类,

与ThreadLocal相同的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);	
    }

不同点是InheritableThreadLocal重写了createMap方法,将值赋值给了线程的inheritableThreadLocals变量。

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

再跟进去Thread类的源码看inheritableThreadLocals变量你会发现:我去,这不是跟Threadlocal一样么,同样初始值为null,线程退出的时候清空。没错,就是这样的。也就是说它其实也是一个线程私有的变量,ThreadLocal的功能它是都有的。

那么它又是怎么把父线程的变量传递到子线程的呢?

接着看Thread的构造方法

    public Thread() {	
        init(null, null, "Thread-" + nextThreadNum(), 0);	
    }	

一路追踪init方法你会看见这段代码:

private void init(ThreadGroup g, Runnable target, String name,	
                      long stackSize, AccessControlContext acc) {	
        if (name == null) {	
            throw new NullPointerException("name cannot be null");	
        }	
        this.name = name;	
        Thread parent = currentThread();	
        SecurityManager security = System.getSecurityManager();	
        if (g == null) {	
            if (security != null) {	
                g = security.getThreadGroup();	
            }	
            if (g == null) {	
                g = parent.getThreadGroup();	
            }	
        }	
        g.checkAccess();	
        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);	
        if (parent.inheritableThreadLocals != null)	
            this.inheritableThreadLocals =	
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);	
        this.stackSize = stackSize;	
        tid = nextThreadID();	
    }

仔细观察35-37行你就明白了。

 

点击下方“阅读原文”查看源码!!!

640?wx_fmt=gif

640?wx_fmt=jpeg

 收藏转发好看再走呗!

640?wx_fmt=gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值