线程安全
线程不安全原因1:线程的抢占式执行
1.线程是抢占式执行,线程间的调度充满随机性.[线程不安全的万恶之源!]
class Counter {
public int count;
public void increase() {
count++;
}
}
public class Demo15 {
private 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.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
// 必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();
// 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
System.out.println(counter.count);
}
}
上面这一段代码的执行结果大概率并不是10 0000 ,只要多个线程操作的是同一个变量,就有问题
count++ 在cpu角度来看是3个指令:
1.load(把内存中的值加载到,cpu寄存器中)
2.add(将寄存器中的值加一)
3.save(将寄存器中的值写回到内存中)
因为抢占式执行,就导致两个线程同时执行这3个指令时,顺序上充满随机性…
情况1:
情况2:
情况3:…
如果类似,情况2出现多次,就会出现线程不安全
极端情况下:
如果所有的操作都是串行的, 此时结果就是10w(可能出现的,但是小概率事件)
如果所有的操作都是交错的,此时结果就是5w(可能出现的,也是小概率事件)
解决原因1的办法
将多条指令打包成一个原子操作
即:
给线程加锁,使用synchronized关键字
class Counter {
public int count;
synchronized public void increase() {
count++;
}
}
public class Demo15 {
private 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.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
// 必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();
// 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
System.out.println(counter.count);
}
}
给方法直接加上 synchronized关键字.当一个线程进入方法后,就会自动加锁,离开方法后,就会自动解锁.
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BLOCKED状态)
阻塞会一直持续到,占用锁的线程把锁释放为止
线程不安全原因2:多个线程对同一个变量进行修改操作,3:针对变量的操作不是原子的~
线程不安全原因4:内存的可见性
public class Demo16 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("循环结束! t 线程退出!");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个 isQuit 的值: ");
isQuit = scanner.nextInt();
System.out.println("main 线程执行完毕!");
}
}
上述代码在运行一段时间后,输入1赋值给isQuit ,但 t 线程并没有结束运行
原因:
t 这个线程在循环读取这个变量,是先从内存中读取后,加载到寄存器,最后打印到控制台,但是从内存中读取,相比于直接从寄存器中读取是非常低效的.
因此在t1中频繁的读取这里的内存的值,就会非常低效!!
而且如果t2线程迟迟不修改, t1线程读到的值又始终是一样的值!!
因此, t1就有了一个大胆的想法:就会不再从内存读数据了,而是直接从寄存器里读(不执行load 了)一旦t1做出了这种大胆的假设, 此时万一t2修改了count值, t1就不能感知到了
这是java编译器进行优化的效果
解决原因4的办法:使用synchronized或volatile关键字
1.使用synchronized关键字:
synchronized 不光能保证指令的原子性,同时也能保证内存可见性,被synchronized包裹起来的代码,编译器就不敢轻易的做出上述假设,相当于手动禁用了编译器的优化
使用volatile关键字:
volatile和原子性无关,但是能够保证内存可见性.禁止编译器做出上述优化.编译器每次执行判定相等,都会重新从内存读取isQuit的值.
public class Demo16 {
private volatile static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("循环结束! t 线程退出!");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个 isQuit 的值: ");
isQuit = scanner.nextInt();
System.out.println("main 线程执行完毕!");
}
}
线程不安全原因5:指令重排序
指令重排序,也是编译器优化中的一种操作
咱们写的很多代码,彼此的顺序,谁在前谁在后无所谓,编译器就会智能的调整这里代码的前后顺序从而提高程序的效率,保证逻辑不变的前提,再去调整顺序
如果代码是单线程的程序,编译器的判定一般都是很准,但是如果代码是多线程的,编译器也可能产生误判
解决原因5的办法:synchronized
synchronized:不光能保证原子性,同时还能保证内存可见性,同时还能禁止指令重排序