ThreadLocal在多线程下保证数据安全

1)ThreadLocal是干什么的

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问变量时的线程安全的方法;如果每个线程对变量的访问都是基于线程自己的变量这样就不会存在线程不安全问题。

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下其实还可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝在一个线程中对变量的任何操作都不会影响到其它线程的变量。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,同时还能保证程序的性能。

2)ThreadLocal的实现原理

Java中的ThreadLocal是用哈希表实现的,每个线程里都有一个ThreadLocalMap属性,里面就以Map的形式存储了多个ThreadLocal对象。当在线程中调用ThreadLocal操作方法时,都会通过当前Thread线程对象拿到线程里的ThreadLocalMap,再通过ThreadLocal对象从ThreadLocalMap中锁定数据实体(ThreadLocalMap.Entry

3)ThreadLocal中的基本方法

ThreadLocal暴露了5个基本的操作和构造方法,主要的功能有:构造方法、设值方法、取值方法和资源回收;上面我们已经简单阐述了ThreadLocal的实现原理,这里我们再通过解析其中的代码来详细说说它是怎么做到线程隔离的。

3.1)ThreadLocal构造方法

ThreadLocal是一个泛型类,只提供了一个构造方法,通过泛型可以指定要存储的值的类型;这个构造方法通常可以单独使用,也可以配合protected T initialValue()方法 从而在实例化对象时提供一个初始值,因为这是一个protected方法,所以我们需要在实例化ThreadLocal对象时覆盖该方法:

private static ThreadLocal threadLocal = new ThreadLocal() {
        @Override
        protected Integer initialValue() {
            return 5; // 这里设置期望的初始值
        }
    };

当然这样未免太过繁琐了,代码也比较冗余!官方自然也考虑到了这一点,所以提供了一个静态的设置初始值的方法withInitial

public static  ThreadLocal withInitial(Supplier supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

Supplier供给接口,这意味着我们可以使用如下方式设置初始值:

private static ThreadLocal threadLocal1 = ThreadLocal.withInitial(() -> 1L);

这样的形式明显要优雅很多,但SuppliedThreadLocal又是个什么东西?其实它只是ThreadLocal类中一个简单的静态内部类罢了:

static final class SuppliedThreadLocal extends ThreadLocal {

        private final Supplier supplier;

        SuppliedThreadLocal(Supplier supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override // 这里通过覆盖ThreadLocal的initialValue方法设置初始值
        protected T initialValue() { return supplier.get(); }
    }

3.2)设值方法set

要保存的数据通过set方法设置,多次调用set方法并不会保存多个数据,而是发生覆盖,一个ThreadLocal正常只能保存一个数据:

public void set(T value) {
        Thread t = Thread.currentThread(); // 获取当前线程
        ThreadLocalMap map = getMap(t); // 拿到当前线程中的ThreadLocalMap
        if (map != null) {
            map.set(this, value); // 线程中存在ThreadLocalMap,设值
        } else {
            createMap(t, value); // 线程中不存在ThreadLocalMap,创建后再设值
        }
    }

3.3)取值方法get

在没有使用set方法设值之前,调用get方法获取的将是initialValue方法设置的值(没有覆盖该方法返回就是null),否则返回的就是set方法设置的值。我们通过代码来解析其中的原理:

public T get() {
        Thread t = Thread.currentThread(); // 获取当前线程
        ThreadLocalMap map = getMap(t); // 拿到当前线程保存的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this); // 这里传入的this就是当前的ThreadLocal对象,拿到ThreadLocal对应的值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; // 拿到ThreadLocal的值
                return result;
            }
        }
        return setInitialValue(); // 调用setInitialValue方法返回初始值
    }

代码中的getMap(t)方法返回当前线程的ThreadLocalMap类型的变量:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; // 返回线程中的ThreadLocalMap
    }

map.getEntry(this)这里通过传入当前的ThreadLocal对象(线程Thread中有一个ThreadLocalMap类型属性,存储了多个ThreadLocal)拿到了ThreadLocalMap.Entry,它是ThreadLocal的静态内部类ThreadLocalMap的静态内部类,代码如下:

static class ThreadLocalMap {

        static class Entry extends WeakReference> {
            Object value;

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

代码很简单,其实ThreadLocal就是通过这个类以弱引用的方式与value绑定在了一起,通过ThreadLocal对象就能获取到对应ThreadLocal中存储的值。

在我们还没有使用set方法为ThreadLocal设置值的情况下,get方法会返回setInitialValue方法的值,可以看看这个方法的具体实现:

private T setInitialValue() {
        T value = initialValue(); // 这里获取初始值,如果我们有重写了initialValue方法的话就会返回设置的初始值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value); // 存在当前Thread对应的ThreadLocalMap,直接设值
        } else {
            createMap(t, value); // 不存在当前Thread对应的ThreadLocalMap,为当前线程创建一个ThreadLocalMap并设值
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal) this);
        }
        return value;
    }

上面最主要的是createMap这个方法,它的作用是给传递的线程创建一个对应的ThreadLocalMap并把值存进去,可以看到新创建的ThreadLocalMap被赋值给了线程中的threadLocals变量,这也说明对应的数据都是存储在各个线程中的,所以每个线程对数据的操作自然不会影响其它线程的数据:

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue); // this就是操作的ThreadLocal对象,firstValue就是要保存的值
    }

3.4)资源回收remove

当我们不再需要保存的数据时,应该通过remove方法将当前线程中保存的值移除掉使对象得到GC(调用remove方法将把ThreadLocal对象从当前线程的ThreadLocalMap移除):

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread()); // 拿到当前线程中的ThreadLocalMap
         if (m != null) {
             m.remove(this); // 从ThreadLocalMap移除key为当前ThreadLocal对象的记录
         }
     }

调用remove方法会清空使用set方法设置的值,此时如果再次调用了get方法,由于ThreadLocal对应的记录已经不存在,所以将会执行return setInitialValue();这段代码,这里将会调用initialValue方法从而返回初始值。

4)ThreadLocal的使用

最后我们再结合一个小栗子来解释ThreadLocal在多线程下的表现:

public class ThreadLocalDemo {
    private static ThreadLocal threadLocal = new ThreadLocal() {
        // 复写initialValue方法为ThreadLocal设置一个初始值,并获取调用了threadLocal的线程id
        @Override
        protected Integer initialValue() {
            System.out.println("当前的线程id:" + Thread.currentThread().getId());
            return 10;
        }
    };
    public static void main(String[] args) {
        // main方法就对应一个线程了,我们在主线程中对threadLocal的值进行修改
        System.out.println("~~~~~~~~~~~~主线程~~~~~~~~~~~~~");
        System.out.println("在主线程中获取threadLocal的值:" + threadLocal.get());
        threadLocal.set(100); // 改变threadLocal的值
        System.out.println("在主线程中再次获取threadLocal的值:" + threadLocal.get());

        System.out.println("~~~~~~~~~~~~新线程~~~~~~~~~~~~~");
        // 新创一个线程,并获取threadLocal的值
        new Thread(() -> System.out.println("在新的线程中获取threadLocal的值:" + threadLocal.get())).start();
    }
}

上面我们有一个静态的threadLocal变量,通过在new的时候覆盖initialValue方法(延迟加载,不会立即调用)为它设置了一个初始值10,并顺便在方法中输出使用threadLocal变量的线程的id,接着在获取了threadLocal的初始值后重新设置了一个数值100;在改变了threadLocal值的那个线程中确实看到了改变后的结果,然而在新线程中却有了“意料之外”的结果:

ThreadLocal在多线程下保证数据安全_第1张图片

之所以会有这样的结果其实因为ThreadLocal是线程隔离的,我们看到的是在操作同一个变量,但是Java会为每一个线程都创建一个threadLocal的副本变量,每个线程操作的其实都是属于它的那个副本变量,而不是公共的那个threadLocal;每个线程对threadLocal的任何操作都不会影响到其它线程的threadLocal

上面我们通过覆盖initialValue方法为threadLocal设置了一个默认值,如果不设置初始值,那么获取到的值就是null,这是ThreadLocal的初始化方法决定的;

5)总结

其实ThreadLocal并没有那么的神秘莫测,但它在多线程编程中的地位却是毋庸置疑的,用好了ThreadLocal能够帮助你写出优雅简洁的多线程代码。在使用synchronized同步代码的时候,如果没法保证同步代码的(细)粒度,则会使得程序的性能下降,而使用ThreadLocal时完全不用考虑性能问题,因为没有了多线程的竞争,也就不用额外的同步判断等开销。总而言之,当遇到多线程操作同一个共享变量需要保证线程安全的时候,你应该首先考虑使用ThreadLocal而不是synchronized

你可能感兴趣的:(ThreadLocal在多线程下保证数据安全)