如何保证线程安全
- 控制资源的访问、使用(即保证在同一时刻只能有一个线程对资源访问、使用成功);我们常使用加锁或者通过更复杂的CAS来控制资源的访问来达到线程安全;
- 除了控制资源的访问,我们还可以通过增加资源来保证所有对象的线程安全。我们只要给每一个线程都分配不同的资源对象,那么从线程安全的角度来看也是满足的。
- 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简述
- ThreadLocal是一个线程的局部变量,也就是说只有当前线程可以访问,那自然是线程安全的。
- 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);
}
- set()方法中,先获取当前线程实例对象,然后通过当前线程这个对象获取ThreadLocalMap;
- ThreadLocalMap是Thread类中的成员变量(每一个线程都有自己的ThreadLocalMap),ThreadLocalMap可以简化理解成是一个HashMap。
- map.set(this, value); key(this)指的就是当前ThreadLocal这个对象,value就是我们需要设置的值。可以知道ThreadLocalMap中保存的就是当前这个线程的所有“局部变量”,也就是一个ThreadLocal变量的集合;
- 每一个线程都有自己的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();
}
- get()方法中,也是先获取当前线程实例对象,然后获取这个线程实例对象中的ThreadLocalMap成员变量。
- 接着通过map.getEntry(this),传入ThreadLocal实例这个key在ThreadLocalMap容器中获取到具体包装的值;
- 如果ThreadLocalMap为空的话,就会调用上述在定义ThreadLocal的时候复写的initialValue()方法,来达到初始化值;
总结
- 从上述分析可以知道,这些变量都是维护在Thread类内部的,也就意味着只要线程不退出,对象的引用将一直存在,当线程退出时,Thread类中的ThreadLocalMap才会进行清理。
- 但是当我们使用线程池时,意味着当前线程未必会退出(比如使用固定大小的线程池,线程总是存在)
- 所以一些大的对象设置到ThreadLocal后,如果在使用完毕后,而线程还没有这么块退出,最好使用ThreadLocal.remove()方法将这个变量移除;