目录
ThreadLocal:用来解决多线程程序下并发问题,通过为每一个线程创建一份共享变量的副本保证线程之间的变量的访问和修改互不影响。
案例练习
1.三个销售卖小米SU7,求他们的总销售。使用CountDownLatch维护三个线程
package threadlocalTest;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
//资源类,本门店有三个销售(三个线程)卖小米SU7
class SU7 {
private int saleTotal;
public synchronized void saleTotal(){
saleTotal++;
}
public int getSaleTotal() {
return saleTotal;
}
public void setSaleTotal(int saleTotal) {
this.saleTotal = saleTotal;
}
}
//需求一:求三个销售的总体销售额
public class ThreadLocalDemo1 {
public static void main(String[] args) throws InterruptedException {
SU7 su7 = new SU7();
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 1; i <= 3 ; i++) {
new Thread(()->{
try {
for (int j = 1; j <= new Random().nextInt(3)+1; j++) {
//本店销售总和统计全部
su7.saleTotal();
}
}finally {
//计数值减一
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
//主线程等待计数器减为0,再执行后面代码
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t"+"销售总额 "+su7.getSaleTotal());
}
}
输出结果1:main 销售总额 7
输出结果2:main 销售总额 5
输出结果3:main 销售总额 6
2.现在想知道每个销售各自的销售量,使用ThreadLocal
初始销售量可写为:
ThreadLocal<Integer> salePersonal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
//初始值
return 0;
}
};
可以使用Lamda更加简洁的初始化:
ThreadLocal<Integer> salePersonal = ThreadLocal.withInitial(() -> 0);
完整程序:
//资源类,本门店有三个销售(三个线程)卖小米SU7
class SU7 {
private int saleTotal;
public synchronized void saleTotal() {
saleTotal++;
}
使用ThreadL 求单个销售的各自销售量
//ThreadLocal<Integer> salePersonal = new ThreadLocal<Integer>(){
// @Override
// protected Integer initialValue() {
// //初始值
// return 0;
// }
//};
ThreadLocal<Integer> salePersonal = ThreadLocal.withInitial(() -> 0);
public void salePersonal() {
salePersonal.set(1 + salePersonal.get());
}
public int getSaleTotal() {
return saleTotal;
}
public void setSaleTotal(int saleTotal) {
this.saleTotal = saleTotal;
}
}
//需求一:求三个销售的总体销售额
public class ThreadLocalDemo1 {
public static void main(String[] args) throws InterruptedException {
SU7 su7 = new SU7();
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= new Random().nextInt(3) + 1; j++) {
//本店销售总和统计全部
su7.saleTotal();
//各个销售独立的销售额
su7.salePersonal();
}
System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出: " + su7.salePersonal.get());
} finally {
//计数值减一
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
//主线程等待计数器减为0,再执行后面代码
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + "销售总额 " + su7.getSaleTotal());
}
}
输出结果1:
1 号销售卖出: 3
2 号销售卖出: 2
3 号销售卖出: 3
main 销售总额 8
输出结果2:
1 号销售卖出: 1
3 号销售卖出: 1
2 号销售卖出: 3
main 销售总额 5
3.在线程池复用情况下,如果不清理自定义的ThreadLocal变量,可能会影响后序业务逻辑和造成内存泄漏的问题。
例如:模拟三个线程办理业务,每个顾客未办理状态为0,办理后为1
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyData {
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
MyData myData = new MyData();
//模拟一个线程有3个办理业务
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
//10个顾客(请求线程),池子里面有3个受理线程,负责处理业务
for (int i = 1; i <= 10; i++) {
int finalI = i;
threadPool.submit(() -> {
try {
Integer beforeInt = myData.threadLocalField.get();
myData.add();
Integer afterInt = myData.threadLocalField.get();
System.out.println(Thread.currentThread().getName()+"\t"+"工作窗口\t"+"受理第:"+ finalI + "个顾客业务"+
"\t beforeInt:"+beforeInt+"\t afterInt:"+afterInt);
}finally {
myData.threadLocalField.remove();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
这里使用ThreadLocal的remove()方法就是确保线程的复用导致结果出错
如果注释掉remove方法则输出结果为:
加上正确结果为:
应用场景
用户身份信息存储:
在做登录鉴权时,一旦鉴权通过之后,就可以吧用户信息存储在ThreadLocal中,这样在后续的所有流程中,需要获取信息的,直接从ThreadLocal中获取。详细案例:http://t.csdnimg.cn/gHWEl
线程安全:ThreadLocal可以用来定义一些需要并发安全处理的成员变量,例如SimpleDateFormat,由于SimpleDateFormat不是线程安全的,可以使用ThreadLocal为每个线程创建一个独立的SimpleDateFormat实例,从而避免线程安全问题。
日志上线文存储:在Log4j等日志框架中,经常使用ThreadLocal来存储与当前线程相关的日志上下文。这允许开发者在日志消息中包含特定与线程的信息,如用户ID或事务ID,对于调试和监控是非常有用。
traceId存储:和上面存储日志上下文类似,在分布式链路追踪中,需要存储本次请求的traceId,通常也都是基于ThreadLocal存储的。
数据库Session:很多ORM框架(对象关系映射),如Hibernate、Mybatis,都是使用ThreadLocal来存储和管理数据库会话的。这样可以确保每个线程都有自己的会话实例,避免了在多线程环境中出现的线程安全问题。
PageHelper分页:PageHelper是MyBatis中提供的分页插件,主要是用来做物理分页的。我们在代码中设置分页参数信息,页码和分页大小等信息都会存储在ThreadLocal中,方便在执行分页时读取数据。