多线程与一致性
为了提高我们程序的性能,很多时候我们都会使用多线程以解决各种场景,但随之而来的是多线程带来的数据一致性问题该如何解决。
如何解决一致性问题?
- 排队:如果多个线程操作‘同一份数据’,那就排个队吧,一个一个来,这样后面一个线程总能得到最新的修改值,例如操作系统中的锁,管程,屏障等都是这种排队机制。<ins style="box-sizing: border-box;">缺点是:性能低。</ins>
- 投票:投票的机制就是多个人同时决策一件事,这个就涉及到了算法,往往会产生很多其他问题,比如欺诈
- 避免:直观意思就是避免多个线程之间产生一致性问题,那该如何去做呢?例如git,ThreadLocal正是采用的这种避免的方式来完成多线程的执行
ThreadLocal定义:
定义:ThreadLocal提供了线程局部变量,一个线程局部变量在多个线程中分别由独立的值(副本)。
提问:既然是每个线程独有的,为什么不直接在调用线程的时候,在相应的线程方法里声明这个局部变量呢?
答:同一个线程可能会调用到很多不同的类和方法,这样就要在不同的地方用到这个变量,自己去实现,代价太大,用ThreadLocal更加方便,且线程安全。
线程模型
对应每个线程来说都有自己的独占数据,这些数据事进行来分配的,每个线程都有一个ThreadLocalMap对象,它本身是一个hash表,里面会放一些线程的局部变量,而ThreadLocal的核心也是这个ThreadLocalMap。
4种核心应用场景
1.资源持有:
例如有三个不同的类,在一次web请求中调用这三个类,但是用户是一个,那么用户数据就可以保存在一个线程里。 如图:
2.线程一致:
例如JDBC事务,我们每次对数据库操作都会走getConnection,jdbc保证只要你是同一个线程过来的请求,不管是哪一个part,都返回的是同一个连接,就是使用ThreadLocal来做的,达到维护一致性的目的。Mybatis使用SqlSessionManager保证了我们同一个线程取出来的连接总是同一个。它是如何做到的呢?其实很简单,就是内部使用了一个ThreadLocal。
3.线程安全:
如果一个线程的调用链路比较长,中间出现异常,那我们可以把出错信息放在ThreadLocal里,然后在后续的链路中使用这个值,可以达到多线程在处理这个场景的时候保证线程安全。
4.并发计算:
例如一个大的任务,拆分成多个小任务,分别计算,最后再进行结果汇总,那么我们可以把每个线程的计算结果放进ThreadLocal中,最后进行汇总计算。 实现案例:比如需要统计一段时间内某个接口的调用量
线程不安全实现:
@RestController
@RequestMapping("orders")
public class OrderController {
private Integer count = 0;
@GetMapping("/visit")
public Integer visit() throws InterruptedException {
count++;
Thread.sleep(100);
return 0;
}
@GetMapping("/stat")
public Integer stat() {
return count;
}
}
count++操作,首先我们是从内存里面读取原来的值,放在了线程本地内存里。然后进行 +1 操作,再写回到内存里。这个时候如果多个线程操作的话,有可能线程A这边还没来得及写,线程B那边读取的是原来的值。这样子的话就会造成数据不一致的问题。结果就会比预期的小。 结果明显是count的值与我们所期望的值不一致
如何解决?
当然方法很多,比如加锁,但今天我们要用ThreadLocal实现
@RestController
@RequestMapping("orders")
public class OrderController {
private static final ThreadLocal<Integer> TL = ThreadLocal.withInitial(() -> 0);
@GetMapping("/visit")
public Integer visit() throws InterruptedException {
Thread.sleep(100);
TL.set(TL.get() + 1);
return 0;
}
@GetMapping("/stat")
public Integer stat() {
return TL.get();
}
}
这样即可达到我们的计数目的。
还有很多方法可以实现,比如我们经常用的原子类Automatic或者synchronized等,他们的实现思想不同,加锁和原子类使用的是【排队】思想,而ThreadLocal使用的是【避免】思想,效率更高。
刮痧ThreadLocal源码
API: ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
1.set:
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。
先拿到当前的线程,然后通过它去拿到一个Map,如果这个Map存在,就把value塞进去,否则就创建一个新的。
2.get:
public T get() {
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}
private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}
先通过getMap方法拿到当前线程对应的Map,然后从里面取出value。如果没有value,就调用ThreadLocal提供的初始化方法,初始化一个值。
3.remove:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
我们在开发一个多线程的程序时,往往会使用线程池。而线程池的功能就是线程的复用。那如果线程池和ThreadLocal在一起就可能会造成问题,所以使用完ThreadLocal,显式调用一下remove方法。
ThreadLocal不支持继承性,ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
关注微信公众号:晏子哒哒
在刮痧技术路上,我们一同成长