!线程安全是整个多线程最关键的要点!
tips:本节莫得笑话,全是干货,还请坐好扶稳,我们出发喽~
一、线程安全
前面我们已经学习了如何创建线程、启动线程以及Thread类中比较重要的属性和方法等,现在我们来看一段代码:
package thread;
public class Demo11 {
private static long count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start();
t2.start();
///main阻塞等待t1和t2
t1.join();
t2.join();
System.out.println(count);
}
}
相信这段代码对于大家来说都不复杂,凭直觉来说代码的运行结果是10000,静态变量、两个线程、两个循环一个5000次、加在一起10000!ok皆大欢喜~~
但是,当我们真正把程序运行起来就会发现,哪里好像不太对,,,,
8938、5382、7465……好像结果除了10000什么都有可能出现,为什么呢?
我们再来详细分析一下这段代码,明确几个问题
- 一共有几个线程?三个,t1、t2和main线程
- 线程执行顺序?随机调度
- 线程结束顺序?t1和t2先结束,main线程最后结束
emmm,似乎和我们理解的也没什么出入,不存在由于main线程先结束导致count打印结果有误的问题
so,why?答案是:count++操作
还记得我们之前谈到过,编程语言是程序员理解使用的语言,对于计算机来说,要将其翻译成它能理解的语言才能进一步执行
在计算机中,count++这行语句其实对应了三行命令
- load:将内存中count变量的值读取到CPU寄存器
- add:把指定寄存器中的值进行+ 1操作
- save:将修改后的寄存器的值写回到内存中
如果是单线程程序,我们自然不用担心,这三行命令一定是按顺序依次执行的,可是在多线程程序中就要考虑一个问题:如果一个线程的这三行命令执行了一半,CPU资源就分给了另一个线程,这时候会发生什么事情呢?
我们先来模拟一下两个线程由于线程的随机调度可能出现的count+结果(仅一次,count初始值为0)
第一种
t1线程 | t2线程 |
---|---|
load(t1线程加载count的值为0) | |
add(t1线程对count的值进行++) | |
save(t1线程将count的值写回内存,此时count=1) | |
load(t2线程加载count的值为1) | |
add(t2线程对count的值进行++) | |
save(t2线程将count的值写回内存,此时count=2) |
结果:count=2
第二种
t1线程 | t2线程 |
---|---|
load(t1线程加载count的值为0) | |
load(t2线程加载count的值为0) | |
add(t2线程对count的值进行++) | |
save(t2线程将count的值写回内存,此时count=1) | |
add(t1线程对count的值进行++) | |
save(t1线程将count的值写回内存,此时count=1) |
结果:count=2
第三种
t1线程 | t2线程 |
---|---|
load(t1线程加载count的值为0) | |
load(t2线程加载count的值为0) | |
add(t1线程对count的值进行++) | |
add(t2线程对count的值进行++) | |
save(t1线程将count的值写回内存,此时count=1) | |
save(t2线程将count的值写回内存,此时count=1) |
结果:count=1
当然,随机调度的结果还有很多,这里就不一一列举
由此可以看出,两个线程分别对count进行仅仅一次++操作,就有可能出现两种结果,那两个线程分别对count进行5000次++,出现若干种结果也就不算出人意料了
这种程序运行结果和预期不匹配的效果当然是我们不愿意见到的,也就是我们常说的bug,在这里属于一种线程安全问题
1.1线程安全问题产生的原因
线程安全问题产生原因主要是以下几点:
- 根本原因:操作系统对于线程的调度是随机的,不同线程抢占式执行
- 多个线程修改了同一个变量
- 修改操作不是原子的(不是不可再分的)
- 内存可见性问题
- 指令重排序
在不同的代码中我们可能涉及到不同的线程安全问题,这点需要我们在实际编程中认真体会~
1.2如何解决线程安全问题
开头的例子我们可以稍作修改,将最后几条语句改为
t1.start();
t1.join();
t2.start();
t2.join();
这样先启动t1线程,main线程阻塞等待直到t1线程运行结束;再启动t2线程,main线程阻塞等待直到t2线程运行结束
这样调正代码结构,将并行执行的两个线程改为串行执行,规避了线程不安全的代码
这可以是一种解决线程安全问题的方案,但是缺点也显而易见,这种方法不够通用,改为串行也降低了程序的运行效率
我们一般使用另一种更加通用的方案——加锁
加锁——通过加锁操作将不是原子的操作打包成一个原子的操作,避免其他线程的操作在当前线程执行过程中插队
Java中使用synchronized这样的关键字搭配代码块来实现加锁操作
synchronized(锁对象){ //进入代码块,就相当于加锁
//执行一些要保护的逻辑
} //出了代码块,就相当于解锁
欲知后事如何,且听下回分解~