并发编程这三大特性就是为了在多个线程交替执行任务的过程中保证线程安全性。
接下来我们从这三个特性切入来介绍线程不安全的原因。
一组操作要么全部执行,要么全部不执行,执行过程中不能被中断。
Java
并发编程中必然存在多个线程的交替执行,因此不论采取何种线程调度算法,都会涉及到线程的切换,而在线程切换的过程中,如果对某个共享变量的操作不是原子的,就可能会导致脏读等各种数据混乱的问题,造成线程不安全,因此我们必须保证对共享变量操作的原子性防止数据混乱以保证线程安全。
如何保证原子性:
通过 synchronized 关键字保证原子性。
通过 Lock保证原子性。
通过 CAS保证原子性。
一个线程修改了某个共享变量,其他线程立即可以“感知到”。
从对Java
内存模型的了解我们可以知道,Java
中每个线程对共享数据的修改都是在其工作内存中进行的,而每个线程在其工作内存中对共享数据的修改并不会立即同步到主内存,因此其他线程并不能立即“感知到”某个线程对共享数据的修改,这样就会导致每个线程工作内存中同一个共享变量的值不一定相等,即缓存不一致,导致线程不安全。因此我们必须保证可见性以保证线程安全。
如何保证可见性:
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized 关键字保证可见性。
通过Lock保证可见性。
通过 final 关键字保证可见性
如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。
为了提高性能,编译器和处理器可能会在满足数据依赖性的条件下对操作进行重新排序,即在单线程环境中,对指令的重排序并不影响执行结果,在单线程环境下,这种重排序不会有什么问题,因为执行结果总是正确的,但是在多线程环境下就会出现问题。
如 a+=1;a*=2
这两个操作不能交换顺序,一旦交换会影响程序的执行结果。
如何保证有序性:
通过 volatile 关键字保证有序性。
通过 内存屏障保证有序性。
通过 synchronized关键字保证有序性。
通过Lock保证有序性。
之所以会出现并发编程的三大特性,就是因为在提升程序性能的同时需要保证安全性,而原子性、可见性、有序性这三大特性可以认为是线程安全的等价概念,我们需要通过一些机制来保证这三大特性,也就是保证线程安全。
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
有序性:JVM
对指令的优化会让指令执行顺序改变,有序性是禁止指令重排