写在最前边
所有的线程安全问题都是对共享资源的操作问题,而解决线程安全问题最简单的方法就是不使用共享资源!!!
曾经有一段时间,一直以为前后端交互并不是多线程执行的。
有这个想法是因为陷入了一个误区:从入门到现在一直认为只要是多线程就需要考虑线程安全问题!而对于前后端的交互,我一直在写,一直没有考虑,一直没有问题!!!
因此,在后来学习多线程时还对此有很大的误解:既然前后端交互是并发执行,那么我controller,service调来调去,参数传来传去,没有线程安全问题的么?还是Spring已经强大、智能到帮我们解决了线程安全问题?
这里突然想起来有人说面试被问到SpringBean是否是线程安全的?
答案是确实没有并发问题!!!
因为使用的全部都是局部变量。每个前端请求过来以后,都是使用新的参数,两次请求没有任何需要交互的地方(以查询购物车为例,A,B,C,D请求即使同时调用查询方法,那么也是自己查询自己的数据,不会出现A的数据需要交给B,需要等待C,D的情况,哪怕是同时调用service、dao也是传递各自的参数,执行各自的逻辑,当然数据库层面会做同步操作,这里不需要我们考虑)。因此在全程都没有共享资源的情况下当然不会出现并发问题。
相信这些东西,一说可能都清楚,但是使用过程中,依然无法保证没有线程安全问题。学完后似乎仅仅只是在面试时候,能说一些似乎很对的话。真让自己实战写一写代码,都不敢保证没有线程安全问题?那么原因呢?缺少练习,缺少实战,缺少思考,缺少对共享资源的定位和理解,总会遗漏一些,常年单线程CRUD忽略的细节问题出现。
示例一 (关于共享变量)
@RestController
public class TestController{
@Resource
private TestService testService;
private int count;
@GetMapping("selectTest")
public String selectTest(){
count++;
return "这是第"+count+"次访问,访问结果为:"+testService.selectTest();
}
}
-
上述代码中存在几个共享变量?testService是共享变量么?
-
上述代码中是否存在线程安全问题?
-
存在哪些线程安全问题?原子性?可见性?有序性?
-
应该如何来去保证线程安全?
第一题答: 存在一个1个被操作的共享资源:count。testService,没有对其进行操作(新增、修改、删除),只是调用了方法,因此不存在安全问题。
第二题答: 存在线程安全问题,有对共享变量count进行++操作。
第三题答: 首先存在原子性问题:count++是非原子操作的,包含了三个指令。 其次存在可见性问题:多线程(多核CPU)存在高速缓存,并非每次都是直接操作内存。
第四题解答:
@RestController
public class TestController{
@Resource
private TestService testService;
// 增加volatile关键字 通过LOCK指令禁用CPU高速缓存 解决可见性问题
private volatile int count;
@GetMapping("selectTest")
public String selectTest(){
// this 表示当前示例,而且spring中默认都是单例的,锁定当前实例即可
synchronized(this){
count++;
}
return "这是第"+count+"次访问,访问结果为:"+testService.selectTest();
}
}
示例二:(关于共享变量)
@RestController
public class TestController{
@Resource
private TestService testService;
@GetMapping("selectTest")
public String selectTest(User user , int days){ // 纯手写伪代码 不要在意参数、注解
List<Result> result = new ArrayList();
// 查询用户最近N天的记录,数据较多以一天为单位分批次查询,利用user对象中的days条件
for(int i = 0;i<days;i++){
Thread thread = new Thread(()->{
user.days = i;
result.add(testService.selectUserTest(user));
});
thread.start();
}
// 伪代码,此处无需考虑安全,假设是等待执行完毕再返回。
Thread.sleep(1000)
return result;
}
}
- 上述代码中存在几个被操作的共享资源?result是共享变量么?
- 上述代码中是哪几个点存在线程安全问题?分别是什么问题原子性?可见性?有序性?
- 应该如何来去保证线程安全?
第一题解答: 存在2个被操作的共享资源:result、user。user中的days每个线程都会去设置,没有进行同步很容易出现线程安全问题,result.add在线程中执行的是添加操作,且并没有地方执行读取操作,因此不会造成线程安全问题。
回归本质,解决多线程问题最好的方式就是不使用(操作)共享资源。
Thread thread = new Thread(()->{
// 由于此处操作共享变量user,且是并发处理
// 因此testService.selectUserTest(user),user中的days不一定是本线程中设置的值,有可能是其他线程设置的值。
// 此处可以转换为局部变量来使用 ,自己实现深克隆,或者使用其他工具提供的 深克隆方式,生成局部变量
// 那么此时 无需任何锁,即可实现 并行执行。
User newUser = user.deepClone();
newUser.days = i;
result.add(testService.selectUserTest(newUser));
});
示例三 (关于ThreadLocal)
线程内部共享变量存入ThreadLoacl就一定安全了么?
ThreadLocal作为线程内部共享变量,在某些情况下会出现获取到意料之外的结果。
1.1 场景
大量请求,在并发请求接口(线程),接口方法开始时,向ThreadLocal中存储一个int值,表示执行阶段,每执行一个阶段的任务,值+1,当执行到某一阶段后直接结束。 在该场景中,运行一段时间后会出现开始即最终的状态。
1.2 原因分析
由于服务器tomcat中默认线程池的关系,核心线程直接是复用的。线程1在执行完A任务后,会去执行B任务,而ThreadLocal是同一线程内部共享,所以B任务能够获取到A任务设置的值。因此在使用线程池时,一定要注意最后清理掉设置的ThreadLoacl值。
1.3 代码示例
private static final ThreadLocal<Integer> local = new ThreadLocal();
@RequestMapping("/local")
public Map loaclThest(@RequestParam("userId") Integer userId){
// 设置之前先打印 线程池中结果
String before = Thread.currentThread().getName()+":" + local.get();
local.set(userId);
String after = Thread.currentThread().getName()+":" + local.get();
Map<String,String> result = new HashMap<>();
result.put("before",before);
result.put("after",after);
return result;
}
线程安全类
使用了线程安全类,就一定是线程安全的嘛?
诸如ConcurrentHashmap等线程安全容器使用时千万不要陷入一个误区,我使用了线程安全类那就一定是线程安全的。对于此类容器来说,线程安全体现在容器内部的原子性、一致性和可见性,是内部CRUD操作的线程安全,对于外部而言,依然需要考虑如何保证线程安全。
2.1 场景
加入现在有一个苹果篮子,我们计划要装满100个苹果,用十个线程操作,每个线程开始执行前判断一下篮子中还需要装多少个苹果(一次最多装20个),如果缺口大于20则只放20个,如果小于则放差额数量。(篮子我们使用ConcurrentHashmap),最后执行结果会出乎意料,无法保证最后篮子中的一定是100个。
2.2 原因分析
首先,我们要分析在本示例中,有几个点需要保证线程安全。 一、ConcurrentHashmap一定是线程安全的,对于我们启动的20个线程而言,添加、读取肯定可以保证每次都是最准确的结果。 二、线程之间的操作也要保证线程安全。如果20个线程在同一时间启动并查看篮子中的数量,那么可能每个线程得到的结果都是我要往里边放20个苹果。最终可能就导致放了400个。(有兴趣的话,可以尝试一下20个线程每次只放一个进去,最终结果是否是100个) 虽然例子有点极端,但意思已经到位了,我们要保证整个业务的线程安全,肯定能紧靠使用线程安全类。在必要的情况下,需要对业务进行加锁(千万要注意锁的粒度)。