Java ThreadLocal 原理简介

使用场景

首先看以下的程序:

public class Main {
    public static class MyRunnable implements Runnable {
        int val = 0;
        @Override
        public void run() {
            val = (int) (Math.random()*100);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println(val);
        }

    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

/*
可能的输出:
65
65
*/

可知,变量var是被thread1和thread2两个线程共享的,当其中一个线程修改了var以后,另一个线程再修改var,就会造成var值被覆盖掉。

这是一个典型的多个线程跑同一份代码。但是,如果我们不想这些线程共享某些变量,或者说var在每个线程内都有一个独立备份,这时候该怎么办呢?TreadLocal就是用来解决这个问题的。

例子1:

public class Main {
    public static class MyRunnable implements Runnable {
        private ThreadLocal threadLocal = new ThreadLocal();

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println(threadLocal.get());
        }

    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

/*
可能的输出:
68
34
*/

如果我需要两个不共享的变量呢?很简单,再添加一个ThreadLocal对象用来存储数据就行了。也就是说一个ThreadLocal只代表一个变量的独立备份,多个变量需要独立备份,就要有多个ThreadLocal对象来存放独立备份。

例子2:

public class Main {
    public static class MyRunnable implements Runnable {
        private ThreadLocal threadLocal = new ThreadLocal();
        private ThreadLocal threadLocal2 = new ThreadLocal();

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            threadLocal2.set(Math.random() * 100D);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println(threadLocal.get());
            System.out.println(threadLocal2.get());
        }

    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}
/*
可能的输出:
28
34
56.049492341729
11.603018021238775
*/

 

实现方案

从上面的ThreadLocal的使用,可以知道ThreadLocal对象也是多个线程共享的,为什么不同的线程往里面放数据,还可以从里面获取每个线程自己存放的数据呢?

方案一:ThreadLocal维护线程与实例之间的映射

既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案就是 ThreadLocal 维护一个 Map,键是 Thread,代码大概如下:

public class ThreadLocal {
    private Map<Thread, Object> mMap = new HashMap<>();
    public void set(Object o) {
        mMap.put(Thread.currentThread(), o);
    }
    public Object get() {
        return mMap.get(Thread.currentThread());
    }
    public void remove() {
        mMap.remove(Thread.currentThread());
    }
}
//以下为泛型形式
public class ThreadLocal<T> {
    private Map<Thread, T> mMap = new HashMap<>();
    public void set(T o) {
        mMap.put(Thread.currentThread(), o);
    }
    public T get() {
        return mMap.get(Thread.currentThread());
    }
    public void remove() {
        mMap.remove(Thread.currentThread());
    }
}

该方案可满足上文提到的每个线程内一个独立备份的要求。每个新线程访问该 ThreadLocal 时,需要向 Map 中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:

  • 增加线程与减少线程均需要写 Map,故需保证该 Map线程安全。虽然从ConcurrentHashMap的演进看Java多线程核心技术一文介绍了几种实现线程安全 Map的方式,但它或多或少都需要锁来保证线程的安全性
  • 线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏

其中锁的问题,是 JDK 未采用该方案的一个原因。

方案二:Thread维护ThreadLocal与实例的映射

上述方案中,出现锁的问题,原因在于多线程访问同一个 Map。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。代码如下:

public class Main {

    public static class Mythread extends Thread {
        private HashMap<Object, Object> container = null;

        public Mythread(Runnable r) {
            super(r);
        }

        public void putData(Object key, Object data) {
            if (container == null) {
                container = new HashMap<>();
            }
            container.put(key, data);
        }

        public Object getData(Object key) {
            if (container == null) {
                return null;
            }
            return container.get(key);
        }
    }

    public static class MyRunnable implements Runnable {

        @Override
        public void run() {
            Mythread curr = (Mythread) Thread.currentThread();
            curr.putData("key1", (int) (Math.random() * 100D));
            curr.putData("key2", Math.random() * 100D);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println(curr.getData("key1"));
            System.out.println(curr.getData("key2"));
        }

    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Mythread(sharedRunnableInstance);
        Thread thread2 = new Mythread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

/*
可能的运行结果:
45
11
4.537484778554357
27.90239993353697

*/

JDK中的实现

JDK中的实现类似于第二种方案,在Thread类中,有一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,实现类似上面container的功能;其次,在ThreadLocal的get和set方法内部获取threadLocals来存放数据。threadLocals也不像上面的container,直接使用字符串做为键,而是使用ThreadLocal对象本身,不同的ThreadLocal对象刚好对应这不同的独立备份。

ThreadLocal如何防止内存泄漏

ThreadLocal中,获取到线程私有对象是通过Thread类持有的一个threadLocalMap(threadLocals),然后传入ThreadLocal当做key获取到对象的,这时候就有个问题,如果你在使用完ThreadLocal之后,将其置为null,这时候这个对象并不能被回收,因为他还有 ThreadLocalMap->entry->key的引用,直到该线程被销毁,但是这个线程很可能会被放到线程池中不会被销毁,这就产生了内存泄露,jdk是通过弱引用来解决的这个问题的,entry中对key的引用是弱引用,当你取消了ThreadLocal的强引用之后,他就只剩下一个弱引用了,所以也会被回收。

弱引用的使用,可参考这篇文章 http://www.cnblogs.com/dolphin0520/p/3784171.html

参考文章:

https://blog.csdn.net/u011983531/article/details/50833784

https://www.tuicool.com/articles/auE3A3e

猜你喜欢

转载自my.oschina.net/wuqingyi/blog/1801182