线程安全问题
在多线程中各种随机调度顺序,代码非常容易出现bug,系统的随机调度也就是抢占式执行就是线程不安全的万恶之源。
一个经典的线程不安全案例
class Counter{
public int count = 0;
public void count(){
count++;
}
}
public class Demo14 {
//一个经典的线程不安全案例
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.count();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.count();
}
});
t1.start();
t2.start();
//让主线程等待
t1.join();
t2.join();
System.out.println("count: " + counter.count);
}
}
我们预期结果是经过两个线程的并发执行把count累加到十万,但是运行结果永远都是小于十万,这个是为什么。
在CPU中,CPU执行指令都是以一个指令为单位进行执行,一个指令就相当于CPU中的最小单位了,所以每次都是完成一个指令后再去执行下一个指令不能执行到一半就去执行另一条指令,而修改操作在CPU其实是三条指令:
- load 加载内存中的值到CPU
- add 在CPU中进行加一运算
- save 写回内存
由于是两个线程修改同一个变量,每次修改也是三条指令,加上系统中线程的调度顺序随机,所以修改操作的调度顺序有很多种执行顺序。
线程不安全的原因
- 抢占式执行(内核)
- 多个线程修改同一个变量(不可避免)
- 修改操作不是原子的(解决线程不安全问题,就从这个方面入手最常用的方法就是把多个操作打包成一个原子操作)
- 内存可见性问题(jvm优化引入的bug)
- 指令重排序
解决线程不安全问题
java中使用synchronized关键字,把多个操作上锁变成一个操作,来解决线程不安全问题,上锁后也就是把并发执行转为了串行,为了线程安全降低了一定的运行效率,但是根据代码的实现来看还是比纯串行的效率更高,因为你只给一部分的代码逻辑上了锁。
synchronized能给所有对象加锁(在java中任意对象都能作为锁对象),在使用锁时一定要清楚是给哪个对象加锁,写多线程代码时,我们不关心这个锁对象是谁,有什么形态,只是关心两个线程是否锁同一个对象。
class Counter{
public int count = 0;
// public Object locker = new Object();
public static Object locker = new Object();
// public synchronized void count(){
// 直接给方法加锁,锁对象就是this
// count++;
// }
public void count(){
// count++;
//加锁把修改操作变为一个原子操作就解决了线程不安全问题
synchronized(locker){
count++;
}
// synchronized (this){
//针对当前对象加锁,谁调用谁就是锁对象
// count++;
// }
}
}
public class Demo14 {
//一个经典的线程不安全案例
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.count();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.count();
}
});
t1.start();
t2.start();
//让主线程等待
t1.join();
t2.join();
System.out.println("count: " + counter.count);
}
}