1.关于线程的抢占式执行(是线程不安全的罪魁祸首)
多个线程执行的时候是随机调度的,是没有规律的,所以在写多个线程代码的时候,需要考虑在任意一种调度的情况下,都可以正确的运行出正确的结果
2.多个线程修改同一个变量 (线程不安全)
如果是一个线程修改一个变量 那没事
如果是多个线程读一个变量 那也没事
如果是多个线程修改不同变量 那还没事
3.修改操作不是原子的
原子:表示不可分割的最小单位
比如设定一个计数器count++ 它本质上是三个指令(load add save) 三个指令执行完成后才会进行加加,然而CPU执行指令都是以“一个指令”为单位的,一个指令就相当于CPU上的“最小单位”,不能说当load执行一半后就把线程调度走了。但如果多个线程修改count这个计数器的时候,由于线程调度是随机的,可能一个线程先执行了load再执行add的时候被另一个线程调度走了,从另一个线程又开始执行load,会导致最终的结果不和预期的一样
4.内存的可见性问题(由JVM的代码优化引入的bug)
因为程序猿写代码,写好的代码编译过后,在机器上运行,由于水平参差不齐,大佬写的代码非常的高效,而我写的代码效率就比较低,所以写编译器的大佬们就想了一个办法,让编译器具有一定的“优化能力”,比如我写了一些逻辑,然后编译,编译器就会把我写的代码等价转换成另一种执行的逻辑,等价转换过后,逻辑不变 但是效率变高了(现在主流的编程语言都具有优化策略,优化效果做的很好,提升很大)
5.指令重排序(volatile):也是编译器优化过后造成的问题
解决了内存可见性问题 没有保证原子性
public class asd {
static class Counter{
public int counter = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
while (counter.counter == 0){
//做一些事情
}
});
t1.start();
Thread t2 = new Thread(()->{
System.out.println("请输入一个数");
Scanner sc = new Scanner(System.in);
counter.counter = sc.nextInt();
});
t2.start();
}
}

原本计算机要读取数据然后进行判断是否是你认定的那个值的,但是读取数据比较慢,比较数据是否相等会比较快,所以Java编译器就会进行优化,就只读取一次数据,然后进行判断是否相等,但是当我修改了数据的时候,由于编译器不再进行读取数据了,就会出现一定的问题,因为编译器在优化时在多线程的情况下,会出现误判的操作。所以得在public int counter = 0;之前加上volatile最终的代码为volatile public int counter = 0;
volatile禁止了编译器优化,避免了直接读取CPU寄存器中缓存中的数据,而是每次都重新读取内存
站在JMM的角度看待volatile:正常程序执行的过程中,会把主内存的数据先加载到工作内存中,然后再进行计算,编译器优化可能会导致不是每次都是真正的读取到主内存,而是直接取了工作内存中的缓存数据(就可能会导致内存可见性的问题),volatile起到的效果就是保证每次读取内存都是真正的从主内存中获取的数据
以上原因不是全部,具体的代码需要具体的分析
控制多线程之间的执行先后顺序可以使用wait/notify
1.都需要搭配synchronized来进行使用
2.wait和notify都得使用同一个对象才是有效的
3.用来加锁的对象和wait/notify的对象也得是一样的
4.即使当前没有任何的线程在wait 那么使用notify也是不会有副作用的
解决线程不安全的手段:
加锁!!!使用synchronized
加完锁之后,别的线程想使用只能堵塞等待,等到解锁过后才能使用
线程安全,不是加了锁就一定安全了,而是通过加锁,让并发修改同一个变量 改为 串行修改同一个变量 才是安全的。不正确的加锁,不一定能解决线程安全问题。
写多线程的代码时候,不需要关心这个锁对象是谁,只关心两个线程是否是锁同一个对象,如果是就会造成锁竞争
在Java中,任意的对象都可以作为锁对象