1.线程不安全的原因
1.系统随机调度,抢占式执行:多线程并发执行
例子:两个线程对同一个变量进行并发的自增。意思就是一个对象在两个线程里同时发生自增操作
经典的load到save操作:(1)从内存读取数据到CPU load
(2)在CPU寄存器中,完成加法运算 add
(3)把寄存器的数据写回到内存里 save (可以简单理解成赋值操作)
以单线程为例(以右边操作为例)
如果是单线程像上面一样的步骤,得到的结果固然没有问题, 但操作系统对于线程的调度过程是随机的,简单来说就是两个线程作用于同一对象时操作的方法步骤没有固定的顺序,操作会相互进行穿插,相当于命令语句会进行排列组合,这时就会造成线程不安全,我再举一个上面这个例子的特殊情况,多线程比较:
我们在程序里用两个线程对同一个对象自增了两次,结果内存的值仍然是1
这是因为我们线程1第一次读取时先从内存中读到了0;然后线程2过来截胡,他把自己的读增写操作全部写完了,然后再执行线程1的增写操作时内存就已经改变了,线程1从0变1再写回内存的值还是1,表面上看操作实际运行了,但实际结果没有改变。
这种原因造成的线程不安全操作是造成线程不安全的根本原因。就是多线程情况下操作系统随机调度(上面我们说线程是系统调度的最小单位) ,我们无法避免这样的问题。
2.多个线程同时尝试修改同一个变量:
跟我们上面那种例子一样,多个线程修改同一个变量,就会造成线程不安全
这时我们可以把对象分成两部分,分别让两个线程去操作,而且还得加锁。(两个线程锁上同一把锁,保证线程之间安全,同时锁上,同时解锁)
最后的操作结果再求总和才可以
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
3.修改操作不是原子的:
原子:不可拆分的最小单位
例如:通过=来修改,=只对应一条机器指令,视为是原子的;
通过++来修改,++对应三条机器指令,则不是原子的。
解决方法:通过加锁操作,把一些不是原子的操作打包成一个原子操作
4.内存可见性引起的问题:
一个线程修改,一个线程读,就特别容易因为内存可见性引发问题
例子:线程1反复的读和判断
如果是正常的读写的情况下,线程1在读和判断,线程2突然写了一下,这时线程1就能立即读到内存中修改的数据,但是在程序运行过程中,会进行一个优化操作(系统自动)。因为从内存中读数据的速度比cpu自己在运算的过程慢太多了,所以这时系统会进行自动优化,把重复的读操作去掉,直接一开始读一下,然后一直在cpu中复用内存器读到寄存器的数据。在单线程中固然没有问题,在多线程中优化后会出现线程1一直在操作寄存器(一直在cpu 进行test操作),这时线程2把他的数据存进内存里,线程1由于没有读操作,无法感知到线程2的修改
5.指令重排序引起的问题
语句重排序:调整了代码的执行顺序,使得更高效率的运行
如果是单线程,则改变语句的执行顺序不会影响结果
Test t = new Text();
执行上述语句一般是三个步骤:
1.在堆上创建内存空间
2.内存空间里构造一个对象
3.把这个内存的引用赋值给t
如果是多线程,当把这个引用赋值给t时,会有另一个线程尝试读取t的引用.如果是按照2,3,第二个线程读到t为非null的时候t就是一个有效对象,如果是3,2顺序,t为非null时由于引用已经在另一个线程抢先读取内存的的引用赋值了,这时t再执行3,t就有可能是一个无效对象
对于这种因为系统优化出现的线程不安全问题,采用加关键字volatile解决