目录
1.对于第二种和第三种和第四种的解决方案都可以通过加锁来解决。而第一种线程的抢占式执行是无法改变的,只能通过加锁来使他变为串行。
2.对于内存可见性问题还有一个解决方案那就是利用volatile,来使该关键词修饰的变量只读内存而不去读寄存器,这样就避免了修改了内存而寄存器还没有修改的情况。
一.线程不安全的原因
1.线程是抢占式执行,线程之间充满随机性。
例如:有两个线程对同一个变量进行修改,而两个线程的抢占式执行就导致了线程的不安全。
class Counter{
public int count=0;
public void increase(){
count++;
}
}
public class TextDemo1 {
public static void main(String[] args) throws InterruptedException {
final Counter counter=new Counter();
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
输出:
7771
Process finished with exit code 0
按理论来说运算结果应该是5000+5000=10000,但由于线程抢占式执行的原因就使结果处在5000-10000之间。
2. 针对变量的操作不是原子的。
什么是原子性:一种操作对应一条机器指令。
若如上面所说的count.increase,一个加加操作对应了两条指令,就不是原子的。
3.内存可见性导致线程不安全
比如:针对同一变量,一个线程t1 进行读操作,一个线程t2 进行写操作。t1频繁的读取t2的写出来的数据,读着读着t1,开始偷懒,没读到 t2突然修改的数值
4.指令重排序导致的不安全
指令重排序是编译器自行进行优化而进行的操作,当我们运行程序代码时,编译器在不改变逻辑的前提下,对代码执行的顺序进行了改变以提升效率,但在多线程要比单线程复杂得多,此时编译器的指令重排序就可能会导致线程的不安全。
二.解决方案
1.对于第二种和第三种和第四种的解决方案都可以通过加锁来解决。而第一种线程的抢占式执行是无法改变的,只能通过加锁来使他变为串行。
我们这儿利用到的加锁操作是synchronized,synchronized的具体用法可以参考其他文章。
2.对于内存可见性问题还有一个解决方案那就是利用volatile,来使该关键词修饰的变量只读内存而不去读寄存器,这样就避免了修改了内存而寄存器还没有修改的情况。
class Counter3{
public volatile int flag=0; //保证内存的可见性
}
public class TextDemo3 {
public static void main(String[] args) {
Counter3 counter3=new Counter3();
Thread t1=new Thread(()->{
while (counter3.flag==0){
}
System.out.println("循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("输入一个整数");
counter3.flag=scanner.nextInt();
});
t1.start();
t2.start();
}
}