TOC
核心理论
共享性
共享性指资源对多个线程操作同一份资源,是导致线程不安全的主要前提之一。若所有线程都只操作线程内的数据,则就不会产生线程安全问题。这也是为什么编程过程中更倾向于编写无状态的类、方法。但是在多线程编程中,总不可避免需要数据共享。最明显的例子就是数据库访问,为了保证数据一致性,不可避免的需要多个线程访问同一数据,再通过锁机制保证线程的安全。
互斥性
互斥性指同一时刻只允许一个线程访问共享资源。通常我们允许多个线程同时读,但同一时刻允许一个线程写,此时读也是不允许的。这就是通常讲的共享锁(读锁)和排它锁(写锁)。在多线程环境中,如果所有线程只读共享资源,不修改共享资源,此时线程也是安全的,当需要复写共享资源时,就需要使用互斥操作来保证线程安全。所以在编写代码时,共享变量总是优先使用静态常量类型,迫不得已才通过互斥操作保证线程安全。在Java中最简单方便的就是使用synchronized关键字实现互斥性。
原子性
原子性指一个操作是一个独立的,不可分割的整体。原子性操作就是指一个连续不可中断的操作。最简单的原子操作就是系统指令,若一个操作对应一条系统指令就可以保证原子性。然而,许多操作都需要多个系统指令组合而成。例如:自增(i++)操作,需要以下三个步骤:
- 获取i的值;
- 对i进行运算+1操作;
- 将结果写回主存中;
这就是为什么volatile只保证可见性和有序性,不保证原子性会导致i++操作仍然不安全。
可见性
要理解可见性,首先要了解JMM(Java内存模型),JMM与操作系统模型相似,如下图所示
每个共享变量都存在主存中,当一个线程访问共享变量时,会将主存中的共享变量复制一份到自己的工作内存,只有线程对共享变量修改后,才会将工作内存的值更新到主存中。这就可能导致一个线程已经修改了数据,而另一个线程依旧拿着旧的数据做操作。这就是不可见性。而volatile实现了可见性就是通过线程每次读取共享资源都从主存读取,这就保证了一个线程对共享资源修改更新至主存,立刻能够被其他线程感知。同样synchronized也保证了可见性。
注意指令重排序也会导致不可见,而voatile关键字底层通过内存屏障实现了防重排序,这使得volatile关键字保证可见性。
有序性
这里有序性指避免指令重排序导致的线程不安全问题。下面重点介绍什么是指令重排序。
指令重排序是编译器和处理器为了优化程序对指令序列的重新排序,也就是说指令重排序目的是为了提高程序运行性能。
重排序有三类:
- 编译器优化重排序:编译器在不改变单线程运行程序结果的前提下,对执行语句的重新排序。
- 指令级并行重排序:由于现代处理器采用指令级并行技术(ILP)将多条指令并行执行。所以当两个操作之间不存在数据依赖,处理器可以改变语句对应的机器指令的执行顺序
- 内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
数据依赖:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为以下三种类型
有关指令重排序的内容详细请看JVM章节的指令重排序笔记。
参考链接
- 由Java引起的指令重排序思考
- https://yq.aliyun.com/articles/348581
- http://www.cnblogs.com/paddix/p/5374810.html
- http://www.cnblogs.com/wewill/p/8086375.html