目录
在上篇博客中,我们学习了进程和线程的概念,以及线程的一些内容,这篇博客我们继续学习线程中的一个重点内容—线程安全,这也是线程里面最复杂的一部分。
1.概念
如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
接下来,我们用代码直观感受一下线程不安全。(答案与预期不符合)
public class Demo13 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
//对count 进行自增
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
//对count 进行自增
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//预期结果是10w
System.out.println(count); //但结果不唯一
}
}
我们可以直观看到答案与我们预期结果10w不相符,这就是一个典型的线程不安全。
经过这样更改了之后,虽然达到了预期结果10w,但也不是我们想要的,因为这样虽然是写在两个线程中了,但并不是“同时执行”,即在线程t1执行的时候,t2并不会启动。
2.产生原因
1.操作系统中,线程的调度顺序是随机的(抢占式执行),罪魁之首,万恶之源(系统内核里实现的,没办法进行修改)
2.两个线程,针对同一个变量,进行修改 (有些情况下可以通过调整代码结构,规避上述问题)
3.修改操作,不是原子的(此时的count++,就是非原子的操作,先读,再修改)(想办法让count++这里的三步走,成为原子的 - 加锁)
4.内存可见性问题
可⻅性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到。
5.指令重排序问题
是编译器的一种优化模式。
3.解决办法
加锁就可以解决,那么如何进行加锁呢?
其中最常通过使用synchronized关键字
synchronized修饰的是一个代码块,同时会指定一个“锁对象” -> 有且只有一个,当两个线程同时尝试对一个对象加锁,此时就会出现“锁冲突”/“锁竞争” 一旦竞争出现,一个线程能够拿到锁,继续执行代码;一个线程拿不到锁,就会阻塞等待,等待前一个线程释放锁之后,它才有机会拿到锁,
继续执行 -> 本质上把“并发执行”改成了“串行执行”