目录
什么是线程安全?
在操作系统中,调度线程的时候,是随机的 (线程在系统中是抢占式执行)
正是因为这样的随机性,就可能导致程序的执行出现一些bug
因此我们认为如果程序在多线程的情况下,因为调度的随机性,引入了bug , 导致结果与我们的预料的结果相差甚远, 则认为代码是线程不安全的 !
相反如果这样的调度的随机性 , 没有带来bug, 则我们认为代码是线程安全的 !
引入一个例子: 在多线程的情况下 , 让count遍历自增10W次.
public class Demo19 {
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
//让 main线程等待t1与t2线程执行完
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
多次运行结果:
通过多次重复运行 我们看到count的是并不是我们预期的10W, 而貌似是一个随机数.
上述就是我们说到的在多线程下, 由于线程调度的随机性, 导致了代码出现了bug
问题如下 :
下面的过程是我们以为(期望)的执行顺序 :
但多线程它是抢占式执行 , 大多数的执行顺序如下 :
由于多线程的抢占式执行 , 上面的只是一部分交错执行顺序
例如 : 还有以下
等等...还有许多交错执行顺序. 但是符合我们预期的也就只有两种情况 (要么是t1先执行完,
要么是t2先执行完)
synchronized 关键字
要解决以上的问题我们就要引出 synchronized 关键字!!!!!
加锁
class Count {
private int m = 0;
//进行加锁操作
synchronized public void add() {
m++;
}
public int getM() {
return this.m;
}
}
public class Demo1 {
public static void main(String[] args) {
Count count = new Count();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.getM());
}
}
输出结果 :
这就属于线程安全的一种 , 多个线程对同一个变量进行操作, 我们给它加上synchronized关键字就可以保证 add()这个方法在运行的时候只能同时被一个线程调用 , 同时也就保证了count++ 的原子性(原子性 (不可分割的最小单位称为原子)---> 原子性就是在cpu中只有一个指令).
对于加锁操作 , 如果有了锁竞争 , 那就肯定有线程处于线程阻塞状态
总结 : 对于多线程来说并发性越高, 速度越快,但是同时可能就会出现一些问题 (bug)
加了锁之后, 并发程度就降低了, 此时数据就更靠谱了, 同时速度就慢了.
线程不安全问题
1) 抢占式执行
线程的抢占式执行 , 线程间的调度充满随机性 . (这个原因也就是线程不安全的核心)
2) 多线程修改
多线程对同一个变量进行修改操作 . (如果多个线程针对不同的变量进行修改, 那样没事,
如果多个线程对同一个变量进行读 , 那也没事).
3) 变量不是原子
例如上面的count++ 它就是3条cpu指令, 不是原子性, 就导致了多线程抢占式执行引起的bug
通过加锁操作就是把好几个指令给打包成为了一个原子的操作.
4) 内存可见性
举个栗子 :
public class Demo2 {
private static boolean flag = false;
public static void main(String[] args) {
Thread t = new Thread(()-> {
while (!flag) {
//空
}
});
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("main线程结束");
}
}
我们预期的效果就是通过将flag = true , 从而终止 t 线程的循环.
我们发现在main 线程中改变flag , 貌似对 t 线程中的while循环好像不起作用.
这就是内存可见性
解释:
对此我们给flag加上volatile关键字, 以禁止编译器进行优化,让编译器每次都从内存中取值.
5) 指令重排序
指令重排序,也是编译器优化的策略 , 调整了代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变 .
谈到优化,都得保证,调整之后的结果和之前是不变的 , 单线程下容易保证. 但如果是多线程的话,就不好说了
解决指令重排序的 , 也是加volatile 关键字, 禁止指令重排序.
synchronized 补充 :
综上 : volatile 不保证原子性, 但是可以禁止内存可见性, 禁止指令重排序
volatile 适用的场景,是一个线程读,一个线程写的情况~
synchronized 适用于多个写 锁竞争