如何保证线程安全

  1. 控制资源的访问、使用(即保证在同一时刻只能有一个线程对资源访问、使用成功);我们常使用加锁或者通过更复杂的CAS来控制资源的访问来达到线程安全;
  2. 除了控制资源的访问,我们还可以通过增加资源来保证所有对象的线程安全。我们只要给每一个线程都分配不同的资源对象,那么从线程安全的角度来看也是满足的。
  3. java中的ThreadLocal便是通过上述第二种思路来保证线程安全的。

简单用法

@SpringBootTest
@RunWith(SpringRunner.class)
public class ParseDateTest {

    @Test
    public void testPareDate() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new ParseDate(i));
        }
        while (!executorService.isShutdown()){

        }
    }
}

class ParseDate implements Runnable {
    static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal();
    int i = 0;

    public ParseDate(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            if (threadLocal.get() == null) {
                threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            }
            SimpleDateFormat simpleDateFormat = threadLocal.get();
            Date t = simpleDateFormat.parse("2020-12-01 23:30:" + i % 60);
            System.out.println(i + ":" + t+"threadLocal="+simpleDateFormat+"threadName="+Thread.currentThread().getName());
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}
//因为SimpleDateFormat并不是线程安全的,
//所以上述代码使用了ThreadLocal给每一个线程各自分配一个SimpleDateFormat。
//当然也可以多个线程共用一个SimpleDateFormat实例(需要加锁保证线程安全)

ThreadLocal简述

  1. ThreadLocal是一个线程的局部变量,也就是说只有当前线程可以访问,那自然是线程安全的。
  2. ThreadLocal只是起到了简单的容器作用,为每一个线程分配不同的对象,需要在应用层面保证。比如上述代码:
if (threadLocal.get() == null) {
        threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

当然你也可以在定义ThreadLocal的时候复写initialValue()方法来指定默认实现方式:

static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

ThreadLocal实现原理

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  1. set()方法中,先获取当前线程实例对象,然后通过当前线程这个对象获取ThreadLocalMap;
  2. ThreadLocalMap是Thread类中的成员变量(每一个线程都有自己的ThreadLocalMap),ThreadLocalMap可以简化理解成是一个HashMap。
  3. map.set(this, value); key(this)指的就是当前ThreadLocal这个对象,value就是我们需要设置的值。可以知道ThreadLocalMap中保存的就是当前这个线程的所有“局部变量”,也就是一个ThreadLocal变量的集合;
  4. 每一个线程都有自己的Thread类,该类中有一个成员变量ThreadLocalMap。该成员变量维护着这个线程的所有局部变量(map),其中key是某个局部变量ThreadLocal的实例,value就是这个ThreadLocal具体包装的值;
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();
    }
  1. get()方法中,也是先获取当前线程实例对象,然后获取这个线程实例对象中的ThreadLocalMap成员变量。
  2. 接着通过map.getEntry(this),传入ThreadLocal实例这个key在ThreadLocalMap容器中获取到具体包装的值;
  3. 如果ThreadLocalMap为空的话,就会调用上述在定义ThreadLocal的时候复写的initialValue()方法,来达到初始化值;

总结

  1. 从上述分析可以知道,这些变量都是维护在Thread类内部的,也就意味着只要线程不退出,对象的引用将一直存在,当线程退出时,Thread类中的ThreadLocalMap才会进行清理。
  2. 但是当我们使用线程池时,意味着当前线程未必会退出(比如使用固定大小的线程池,线程总是存在)
  3. 所以一些大的对象设置到ThreadLocal后,如果在使用完毕后,而线程还没有这么块退出,最好使用ThreadLocal.remove()方法将这个变量移除;