线程安全问题的原因和解决方案
1.示例代码
class Counter {
public int count = 0;
public void add(){
count++;
}
}
public class Demo {
public static void main(String[] args) {
Counter counter = new Counter();
//两个线程,两个线程分别调用counter来进行5W次的add方法
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
}
代码的原理是两个线程分别调用counter来进行5W次的add方法,按照理想情况来说,最后输出的count值应该为10W,但是实际上运行上面的代码,会发现每次输出count的值可能都不一样,而这就是典型的线程安全问题。
2. 原因
1.根本原因: 是抢占式执行,随机调度
2.代码结构: 多个线程同时修改同一个变量
解决方法: 可以通过调整代码结构来规避这个问题,但是这种调整,并不一定都可以使用,值得注意的是 String是不可变对象,不可变对象,是天然是线程安全的
3.原子性
如果修改操作是非原子的,就可能出现线程安全问题,比如上面的代码中count的值不一样,其原因是修改操作是非原子性的,因为count++的++操作本质上分成三步:
1.先把内存中的值,读取到CPU的寄存器中 load
2.把CPU寄存器里的数值进行+1运算 add
3.把得到的结果写在内存中 save
如果两个线程并发执行count++,此时相当于两组load add save进行执行
那么线程调度顺序的不一致,就可能产生结果上的差异。
解决方法: 可以通过把这个非原子的操作变成原子的操作
4.内存可见性问题
是指一个线程读变量,一个线程改变量,从而产生线程安全问题
5.指令重排序问题
是指编译器对所写的代码进行优化,从而产生线程安全问题
3.线程安全问题的解决方案
从原子性入手,利用加锁,把非原子的改成原子的,即利用synchronized进行加锁
4.synchronized的介绍
1.synchronized使用方法
1.修饰方法
1)修饰普通方法
//修饰普通方法
synchronized public void print() {
System.out.println("A");
}
2)修饰静态方法
// 修饰静态方法
synchronized public static void print(){
System.out.println("A");
}
2.修饰代码块
//修饰代码块
public static void print(){
synchronized (this){
System.out.println("A");
}
}
2.synchronized的特性
1) 互斥
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待
1.进入synchronized修饰的代码块,就相当于加锁
2.退出synchronized修饰的代码块,就相当于解锁
synchronized用的锁是存在Java对象里头的
2)可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
理解"把自己锁死":即一个线程没有释放锁,然后又开始尝试再加锁
//第一次加锁,加锁成功
lock();
//第二次加锁,锁已经被占用,阻塞等待
lock();
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才可以获得第二个锁,但是释放第一个锁也是由该线程来完成的,所以就无法进行解锁操作,从而产生死锁,这种锁就叫做不可重入锁。
Java当中的synchronized是可重入锁,因此没有上面的问题。