引发线程安全问题的几个原因:
- 抢占式执行,随机调度 线程中的代码执行到任意一行,都随时可能被切换出去
- 多个线程同时修改同一个变量
- 修改操作不是原子的
- 内存可见性 volatile
- 一个线程频繁读,一个线程修改
- 指令重排序
java并发的三大基本特性:
- 原子性
- 可见性
- 有序性
原子性:
原子性就是每一步操作都不能中途暂停去执行其他线程再回来继续执行剩余操作. 即使是一行代码,有的操作都需要分几步执行. 比如一个变量自增的操作需要先从主内存读取数据到寄存器中,cpu计算过程中,会从寄存器中读取数据,并将计算结果写回寄存器,最终写回工作内存. 假如有两个线程同时对一个值进行自增修改,线程a刚完成+1操作.突然调度到另一个线程b,这个线程b完成自增操作修改了主内存的值.又回到线程a,执行剩余操作.重点是虽然进行了两次自增,线程a修改的值是在线程b之前获取的,又在线程b执行完后再将计算好的数据放回主内存.(线程a拿到了0 - 线程a自增变为1- 线程b拿到了0 - 线程b自增变为1 - 线程b将数据1放回主内存 - 线程a将数据1放回主内存 )两次自增数据只加了1.这就是为什么多线程要保证原子性来避免安全问题.
如何解决此类问题:
使用Synchronized(同步)代码块,效果就类似于有一个厕所,线程a进去了就获取到了锁并上锁,在线程a没有结束前,其他线程都不能进这个厕所,因为他们拿不到锁.所以只能进行阻塞等待.这样就能保证一段代码的原子性了.
可见性:
可见性指,一个线程对共享的变量值的修改,能够及时地被其他线程看到.
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
如果解决此类问题:
使用Volatile关键字,被volatile修饰的变量,每次读取,都会从主内存中重新读取.
有序性:
编译器对于指令重排序的前提是"保持逻辑不发生变化". 即在不影响最终结果的情况下,执行步骤可能会被打乱.在多线程情况下会出现问题
使用volatile关键字可以禁止指令重排序
再介绍一下synchronized
synchronized的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以synchronized也能保证内存可见性.