一. 是什么?
一般而言,全局变量都是由线程共享的。引入ThreadLocal
,可以给多个线程提供同一个变量的多个拷贝(副本)。
二. 为什么要用?
突出该技术概念有什么优点是值得投入使用的
-
简单
不需要考虑多线程容易引起的数据一致性问题。
降低了编程复杂度,能够直接以单线程思维编程。 -
性能好
更少的同步等待,更少占用cpu时间片 -
安全
具有线程安全特性
三. 用在什么场景?
在进程中,默认变量都是由各个线程共享的。当多个线程对共享变量进行写时,可能会导致读写共享变量不一致的问题。
-
资源持有
当多个线程需要对同一个变量拥有独立的拷贝时,避免线程间对共享变量读写影响。 -
线程一致性
协助需要保持资源一致性的操作,如数据库事务变更数据后读取,降低编程复杂度。 -
线程安全
在进行多线程改造时,容易从单线程的实现角度转换为多线程实现,降低编程复杂度。 -
并发计算
多个线程分发计算时,方便每个线程累计一部分的计算结果值
四. 基本api的使用
在java中,内部原理使用是通过具有线程安全的Hashtable
来实现ThreadLocal
ThreadLocal<T>
一般搭配static一起使用,在全局中声明一个线程多副本存储的变量initialValue
实例化ThreadLocal时,用于重写的方法。当get方法为null时,返回默认值。get/set
获取/设置当前线程中对应的拷贝副本remove
移除当前线程中对应的拷贝副本
简单练习
/**
* @Title ThreadLocal概念学习
* @Description 简要api学习
* @Author Heavent
* @Date 2020-02-02
* @Version 1.0.0
**/
public class ThreadLocalDemo {
protected static ThreadLocal<String> threadId = new ThreadLocal(){
// 重写该方法,允许ThreadLocal实例通过get获取值时,返回一个默认值
protected String initialValue() {
return "initial: thread->" + Thread.currentThread().getId();
}
};
public static void main(String[] args) {
System.out.println("============== main thread =================");
// 设置当前线程中对应threadId的拷贝副本
threadId.set("main-thread->" + Thread.currentThread().getId());
// 设置当前线程中对应threadId的拷贝副本
System.out.println(threadId.get());
new Thread(() -> {
System.out.println("------------------ 1 thread ------------------");
threadId.set("rename-1 thread->" + Thread.currentThread().getId());
threadId.remove();
System.out.println("获取默认的initialValue定义的值:" + threadId.get());
}).start();
new Thread(() -> {
System.out.println("------------------ 2 thread ------------------");
System.out.println("获取默认的initialValue定义的值:" + threadId.get());
}).start();
}
}
五. 分布式计算简单案例模型
对于远程分布式汇总计算,可用ThreadLocal
减少synchronized
频次的使用,实现最终数据一致性
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
/**
* @Title 分布式计算案例
* @Description 通过10个线程计算100次后,将各线程的计算结果汇总
* @Author Heavent
* @Date 2020-02-02
* @Version 1.0.0
**/
public class DistributedComputingDemo {
/**
* 由于ThreadLocal没有提供遍历 多线程存储account变量副本 的api
* 声明map来引用每个线程中account变量副本的情况
* 需要主要的是要自定义对象引用变量,不能直接使用Double/Integer/Float
*/
static Map<Thread, RefVal<Double>> allThreadMap = new HashMap<>();
static ThreadLocal<RefVal<Double>> account = new ThreadLocal(){
@Override
protected RefVal<Double> initialValue() {
RefVal<Double> defVal = new RefVal<>();
defVal.setVal(0d);
/**
* 此处线程不安全,需要增加同步操作
* 此处同步属于低频次同步,多少线程加入,则有多少次同步
* 当使用ThreadLocal并不一定完全避免同步,但要尽量将同步减少到最低
*/
synchronized (DistributedComputingDemo.class) {
allThreadMap.put(Thread.currentThread(), defVal);
}
return defVal;
}
};
/**
* 累加运算 10次
*/
public void accumulation() throws InterruptedException {
for (int i=0; i<10; i++) {
Thread.sleep(50);
// 模拟远程获取的值为1,便于汇总验证
double res = 1;
RefVal<Double> refVal = account.get();
refVal.setVal(refVal.getVal() + res);
}
}
/**
* 获取最终的累加值
*/
public Double getAccountRes(){
// 使用map筛选出所有的value,再通过reduce进行累加
return allThreadMap.entrySet().stream().map(entry -> entry.getValue().getVal())
.reduce((x, y) -> x + y).get();
}
public static void main(String[] args) {
try {
// 线程数
int threadCore = 10;
/**
* CountDownLatch用于协调多个线程之间的通信
* 允许一个线程暂时挂起,等待其它线程执行完毕后,再继续执行
*/
CountDownLatch downLatch = new CountDownLatch(threadCore);
DistributedComputingDemo demo = new DistributedComputingDemo();
// 开启10个线程累加计算, 每个线程将累计10次, 每次累计加1,汇总值应为100
for (int i=0; i<threadCore; i++) {
new Thread(() -> {
try {
demo.accumulation();
System.out.printf("thread-%s was run...\r\n" , Thread.currentThread().getId());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 减少一次线程计数
downLatch.countDown();
}
}).start();
}
// 多线程完成后继续执行main线程
downLatch.await();
System.out.println("================= 获取多线程计算后的汇总值 =================");
System.out.println(demo.getAccountRes());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 引用每一个线程的副本对象,
* 因此需要声明一个引用对象, 便于全局获取
* @param <T>
*/
class RefVal<T> {
T val;
public T getVal() {
return val;
}
public void setVal(T val) {
this.val = val;
}
}