精通高并发与多线程,却不会用ThreadLocal?
之前我们有在并发系列中提到 ThreadLocal 类和基本使用方法,那我们就来看下 ThreadLocal 究竟是如何使用的!
ThreadLocal 简介
概念
ThreadLocal 类是用来提供线程内部的局部变量。这种变量在多线程环境下访问(get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。 ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和上下文。
作用
- 传递数据
提供线程内部的局部变量。可以通过 ThreadLocal 在同一线程,不同组件中传递公共变量。
- 线程并发
适用于多线程并发情况下。
- 线程隔离
每个线程的变量都是独立的,不会相互影响。
ThreadLocal 实战
1. 常见方法
- ThreadLocal ()
构造方法,创建一个 ThreadLocal 对象
- void set (T value)
设置当前线程绑定的局部变量
- T get ()
获取当前线程绑定的局部变量
- void remove ()
移除当前线程绑定的局部变量
2. 为什么要使用 ThreadLocal
首先我们先看一组并发条件下的代码场景:
@Data
public class ThreadLocalTest {
private String name;
public static void main(String[] args) {
ThreadLocalTest tmp = new ThreadLocalTest();
for (int i = 0; i < 4; i++) {
Thread thread = new Thread(() -> {
tmp.setName(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() +
" 拿到数据:" + tmp.getName());
});
thread.setName("Thread-" + i);
thread.start();
}
}
}
复制代码
我们理想中的代码输出结果应该是这样的:
/** OUTPUT **/
Thread-0 拿到数据:Thread-0
Thread-1 拿到数据:Thread-1
Thread-2 拿到数据:Thread-2
Thread-3 拿到数据:Thread-3
复制代码
但是实际上输出的结果却是这样的:
/** OUTPUT **/
Thread-0 拿到数据:Thread-1
Thread-3 拿到数据:Thread-3
Thread-1 拿到数据:Thread-1
Thread-2 拿到数据:Thread-2
复制代码
顺序乱了没有关系,但是我们可以看到 Thread-0 这个线程拿到的值却是 Thread-1
从结果中我们可以看出多个线程在访问同一个变量的时候会出现异常,这是因为线程间的数据没有隔离!
并发线程出现的问题?那加锁不就完事了!这个时候你三下五除二的写下了以下代码:
@Data
public class ThreadLocalTest {
private String name;
public static void main(String[] args) {
ThreadLocalTest tmp = new ThreadLocalTest();
for (int i = 0; i < 4; i++) {
Thread thread = new Thread(() -> {
synchronized (tmp) {
tmp.setName(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()
+ " " + tmp.getName());
}
});
thread.setName("Thread-" + i);
thread.start();
}
}
}
/** OUTPUT **/
Thread-2 Thread-2
Thread-3 Thread-3
Thread-1 Thread-1
Thread-0 Thread-0
复制代码
从结果上看,加锁好像是解决了上述问题,但是 synchronized 常用于多线程数据共享的问题,而非多线程数据隔离的问题。这里使用 synchronized 虽然解决了问题,但是多少有些不合适,并且 synchronized 属于重量级锁,为了实现多线程数据隔离贸然的加上 synchronized,也会影响到性能。
加锁的方法也被否定了,那么该如何解决?不如用 ThreadLocal 牛刀小试一番:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public String getName() {
return threadLocal.get();
}
public void setName(String name) {
threadLocal.set(name);
}
public static void main(String[] args) {
ThreadLocalTest tmp = new ThreadLocalTest();
for (int i = 0; i < 4; i++) {
Thread thread = new Thread(() -> {
tmp.setName(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() +
" 拿到数据:" + tmp.getName());
});
thread.setName("Thread-" + i);
thread.start();
}
}
}
复制代码
在查看输出结果之前,我们先来看看代码发生了哪些变化
首先多了一个 private static 修饰的 ThreadLocal ,然后在 setName 的时候,我们实际上是往 ThreadLocal 里面存数据,在 getName 的时候,我们是在 ThreadLocal 里面取数据。感觉操作上也是挺简单的,但是这样真的能做到线程间的数据隔离吗,我们再来看一看结果:
/** OUTPUT **/
Thread-1 拿到数据:Thread-1
Thread-2 拿到数据:Thread-2
Thread-0 拿到数据:Thread-0
Thread-3 拿到数据:Thread-3
复制代码
从结果上可以看到每个线程都能取到对应的数据。ThreadLocal 也已经解决了多线程之间数据隔离的问题。
那么我们来小结一下,为什么需要使用ThreadLocal,与 synchronized 的区别是什么
- synchronized
原理: 同步机制采用 “以时间换空间” 的方式,只提供了一份变量,让不同线程排队访问
侧重点: 多个线程之间同步访问资源
- ThreadLocal
原理: ThreadLocal 采用 “以空间换时间” 的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而互不干扰
侧重点: 多线程中让每个线程之间的数据相互隔离
3. 内部结构
从上面的案例中我们可以看到 ThreadLocal 的两个主要方法分别是 set() 和 get()
那我们不妨猜想一下,如果让我们来设计 ThreadLocal ,我们该如何设计,是否会有这样的想法:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value ,这样就能达到各个线程的局部变量隔离的效果。