这篇文章是慕课网上一门免费课程《ThreadLocal》的观后总结。这门课将ThreadLocal
讲得非常清晰易懂,又深入底层原理和设计思想,是我看过的最好的ThreadLocal的资料,现在把用自己的话,把它整理成文字版本。
总共预计产出四篇文章,这是第一篇。
一致性问题
什么是一致性问题?
多线程充分利用了多核CPU的威力,为我们程序提供了很高的性能。但是有时候,我们需要多个线程互相协作,这里可能就会涉及到数据一致性的问题。
数据一致性指问题的是:发生在「多个主体」对「同一份」数据无法达成「共识」。这里的多个主体,可能是多线程,也可能是多个服务器节点。
当然了,这里的“多个主体”也可以指朋友之间,夫妻之间,所谓“道不同,不相为谋”,说的就是这个理。
数据一致性问题是我们使用分布式或者多线程带来的代价,使得程序或系统变得复杂,那我们有什么办法可以解决它呢?
如何解决一致性问题?
在我们解决一致性问题的时候,大概有这样几种思路。
第一种是「排队」,如果两个人对一个问题的看法不一致,那就排成一队,一个人一个人去修改它,这样后面一个人总是能够得到前面一个人修改后的值,数据也就总是一致的了。我们在操作系统中的锁、互斥量、管程、屏障等等概念,都是利用了排队的思想。
排队虽然能够很好的确保数据一致性,但性能非常低。
第二种是「投票」,投票的话,多个人可以同时去做一件决策,或者同时去修改数据,但最终谁修改成功,是用投票来决定的。这个方式很高效,但它也会产生很多问题,比如网络中断、欺诈等等。想要通过投票达到一致性非常复杂,往往需要严格的数学理论来证明,还需要中间有一些“信使”不断来来回回传递消息,这中间也会有一些性能的开销。
我们在分布式系统中常见的Paxos和Raft算法,就是使用投票来解决一致性问题的。感兴趣的同学可以去我的个人网站阅读我之前写的关于这两个算法的文章。
第三种是「避免」。既然保证数据一致性很难,那我能不能通过一些手段,去避免多个线程之间产生一致性问题呢?我们程序员熟悉的git就是这个实现,大家在本地分布式修改同一个文件,最后通过版本控制和“冲突解决”去解决这个问题。
而我们今天的正题,ThreadLocal,也是使用的“避免”这种方式。
❝我们不能避免所有的数据不一致问题,所以我们还是需要学习排队和投票,去解决不同场景的数据不一致问题。
❞
ThreadLocal是什么?
定义
首先我们上定义:ThreadLocal提供了「线程局部变量」,一个线程局部变量在多个线程中,分别有独立的值(副本)。
是不是有些看不懂?我刚开始了解ThreadLocal的时候,也有点不明白这句话是什么意思。最大的疑惑是:既然是每个线程独有的,那我干嘛不直接在调用线程的时候,在相应的方法里面声明和使用这个局部变量?
后来才明白,同一个线程可能会调用到很多不同的类和方法,你可能需要在不同的地方,用到这个变量。如果自己去实现这么一个功能,成本其实挺大的。
ThreadLocal是一个可以开箱即用、无额外开销、线程安全的工具类,可以完美解决这个问题。
ThreadLocal并不是Java语言独有的,在几乎所有提供多线程特征的语言里面,都会有ThreadLocal的实现。在Java中,ThreadLocal中用哈希表来实现的。
线程模型
这个图能够比较直观地解释ThreadLocal的线程模型。
其实并不复杂。左边的黑色大圆圈代表一个进程。进程里有一个线程表,红色波浪线代表一个个线程。
对于每一个线程来说,都有自己的独占数据。这些独占数据是进程来分配的,对于Java来说,独占数据很多都是在Thread类里面分配的,而每一个线程里面都有一个ThreadLocalMap的对象,它本身是一个哈希表,里面会放一些线程的局部变量(红色长方形)。ThreadLocal的核心也是这个ThreadLocalMap。
相关源码:
// Thread类里的变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap的定义:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ...
}
复制代码
基本API
基本API主要分成四个部分,分别是:
构造函数ThreadLocal() 初始化initialValue() 访问器get/set 回收remove
构造函数是一个泛型的,传入的类型是你要使用的局部变量变量的类型。初始化initialValue()
用于如果你没有调用set()
方法的时候,调用get()
方法返回的默认值。如果不重载初始化方法,会返回null
。
如果调用了set()方法,再调用get()方法,就不会调用initialValue()方法。
如果调用了set(),再调用remove(),再调用get(),是会调用initialValue()的。
❝JDK 8提供了静态方法withInitial来进行更好的初始化。
❞
示例代码:
public class ThreadLocalDemo {
public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
System.out.println("invoke initial value");
return "default value";
});
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
THREAD_LOCAL.set("first thread");
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
THREAD_LOCAL.set("second thread");
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
THREAD_LOCAL.set("third thread");
THREAD_LOCAL.remove();
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
System.out.println(THREAD_LOCAL.get());
}).start();
SECONDS.sleep(1L);
}
}
// 输出:
first thread
second thread
invoke initial value
default value
invoke initial value
default value
复制代码
4种核心场景
在实际项目中,ThreadLocal一般用来做什么呢?这里总结四种核心的应用场景。
资源持有
比如我们有三个不同的类。在一次Web请求中,会在不同的地方,不同的时候,调用这三个类的实例。但用户是同一个,用户数据可以保存在「一个线程」里。
这个时候,我们可以在程序1把用户数据放进ThreadLocalMap里,然后在程序2和程序3里面去用它。
这样做的优势在于:持有线程资源供线程的各个部分使用,全局获取,降低「编程难度」。
线程一致
这里以JDBC为例。我们经常会用到事务,它是怎么实现的呢?
原来,我们每次对数据库操作,都会走JDBC getConnection,JDBC保证只要你是同一个线程过来的请求,不管是在哪个part,都返回的是同一个连接。这个就是使用ThreadLocal来做的。
当一个part过来的时候,JDBC会去看ThreadLocal里是不是已经有这个线程的连接了,如果有,就直接返回;如果没有,就从连接池请求分配一个连接,然后放进ThreadLocal里。
这样就可以保证一个事务的所有part都在一个连接里。TheadLocal可以帮助它维护这种一致性,降低「编程难度」。
线程安全
假设我们一个线程的调用链路比较长。在中途中出现异常怎么做?我们可以在出错的时候,把错误信息放到ThreadLocal里面,然后在后续的链路去使用这个值。使用TheadLocal可以保证多个线程在处理这个场景的时候保证线程安全。
并发计算
如果我们有一个大的任务,可以把它拆分成很多小任务,分别计算,然后最终把结果汇总起来。如果是分布式计算,可能是先存储在自己的节点里。而如果是单机下的多线程计算,可以把每个线程的计算结果放进ThreadLocal里面,最后取出来汇总。
那么问题来了,怎么取出ThreadLocal的所有线程的值?且看下篇文章分析。
关于作者
我是Yasin,一个有颜有料又有趣的程序员。
微信公众号:编了个程
个人网站:http://yasinshaw.com
关注我的公众号,和我一起成长~