1.线程安全问题引入
首先看下面代码:
class Counter{
public int count = 0;
public void increase(){
count++;
}
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
我们会想,两个线程,针对同一个变量,进行循环自增,预期结果应该是10w,那么是不是呢?
显而易见,并不是10w,并且每次的运行结果都不同,这是为什么呢?
看这个操作:
这个操作本质上是三个步骤(CPU指令的角度):
而如果按照上述的操作,在两个线程,或者更多个线程并发执行的情况下,就可能出现问题:
虽然是自增两次,但是由于两个线程并发执行,就可能在一定的执行顺序下,导致运算的中间结果被覆盖了.
那么在这5w次的循环过程中,有多少次这两个线程执行是"串行的",有多少次会出现覆盖结果呢?
不确定!!!线程的调度是随机的,抢占式执行的过程
所以此处的结果就会出现问题,而且这个错误的结果一定小于10w
简单列举几种错误:
2.线程安全问题的原因
1) [根本原因]多个线程之间的调度是"随机的",操作系统使用"抢占式"策略来调度
(与单线程相比,多线程下,代码的执行顺序产生了更多的变化)
2) 多个线程同时修改同一个变量,容易产生线程安全问题(注意这三点)
3) 进行的修改,不是"原子的".如果修改操作,能够按照原子的方式来完成,此时就不会有线程安全问题
("count++"不是原子的,"="直接赋值,视为原子,"if="先判定,再赋值,也不是原子的)
4) 内存可见性
一个线程在读,另一个线程在改(类似于事务的不可重复读),也会出问题
5) 指令重排序
编译器可能觉得你写的代码不够高效,所以将代码的执行顺序进行了调整(保持逻辑不变的情况下),来加快程序的执行效率
3.synchronized锁
- 把一组操作,打包成一个"原子"的操作,让多个线程,同一时刻只有一个线程能使用这个变量
- synchronized修饰的是静态方法,和具体的对象无关,是和类有关的
3.1 代码加锁操作
如下,对上述代码进行修改:
class Counter{
public int count = 0;
/*synchronized public void increase(){
count++;
}*/ //法一
public void increase(){
synchronized (this){
count++;
}
} //法二
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
运行结果如下:
3.2 synchronized相关注意事项
- 注:Java中引入synchronized关键字进行加锁操作
- synchronized进行加锁解锁,是以"对象"为维度进行展开的
现在我们思考一下, 通过加锁操作把并发执行=>串行执行了,那么多线程还要存在的意义嘛?
- 这里的串行执行并不是针对整体,只针对了count++这件事,而for循环并没有加锁
- for循环中操作的变量i是栈上的一个局部变量,两个线程,有两个独立的栈空间,也就是完全不同的变量,即两个线程中的i不是同一个变量,两个线程修改两个不同的变量,没有线程安全问题,所以也就不需要给i加锁
- 不过,在这两个线程中,一部分代码是串行执行的,一部分是并发执行的,所以比存粹的串行执行效率要高
- 加锁是为了互斥使用资源(互斥的修改变量),synchronized每次加锁,也是针对每个特定的对象加锁!!!
- 具体针对哪个对象加锁不重要,重要的是,两个线程是不是针对同一个对象!!!
如下解释:
那如果是两个线程针对不同的对象加锁呢?
class Counter{
public int count = 0;
private Object locker = new Object();
public void increase(){
synchronized (this){
count++;
}
}
public void increase2(){
synchronized (locker){
count++;
}
}
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
可以知道,如果要是两个线程针对不同的对象加锁,此时,就不会有阻塞等待,也就不会让两个线程按照串行的方式进行count++,也就仍然会存在线程安全问题
此时仍然是针对同一个对象加锁,会解决线程安全问题
所以具体针对哪个对象加锁不重要,重要的是,两个线程是不是针对同一个对象!!!