线程安全
JMM-Java内存模型
JMM(java内存模型Java Memory Model)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁必须是同一把锁
JMM规定的线程安全三大特性:可见性、原子性、有序性
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,简要过程如下图:
happens-before先行发生原则
happens-before主要用于强调两个有冲突的动作之间的顺序,以及数据争用的发生时机。在具体的虚拟机实现中,必须保证以下原则:
- 某个线程中的每个动作都happens-before 该线程中该动作后面的动作。
- 某个管程上的unlock 动作happens-before 同一个管程上后续的lock 动作
- 对某个volatile 字段的写操作happens-before 每个后续对该volatile 字段的读操作
- 在某个线程对象上调用start() 方法happens-before 该启动了的线程中的任意动作
- 某个线程中的所有动作happens-before 任意其它线程成功从该线程对象上的join() 中返回
- 如果某个动作a happens-before 动作b , 且b happens-before 动作c , 则有a happens-before c.
当程序包含两个没有被happens-before关系排序的冲突访问时,就称存在数据争用。遵守了happens-before原则,也就意味着有些代码不能进行重排序,有些数据不能缓存。
volatile关键字
- 保证可见性
- 不保证原子性
- 禁止指令重排序
可见性问题:让一个线程对共享变量的修改,能够及时被其他线程看见。
根据JMM中规定的happen before 和同步原则:
- 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作。
- 对volatile变量v的写入,与所有其他线程后续对v的读同步。
要满足这些条件,volatile关键字就有这些功能:
- 禁止缓存。(volatile变量的访问控制符会加个ACC VOLATILE)
- 对volatile变量相关的指令不进行重排序。
指令重排序
在执行程序是为了提高性能,编译器和处理器常常会对指令做重排序。重排序氛围三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
内存屏障
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能做指令重排序。如果在指令之间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强调刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版。
竞态条件与临界区
public class Test {
public int i = 0;
public void incr(){
i++;
}
}
多个线程访问了相同的资源,向这些资源做了写操作时,对执行顺序有要求。
- 临界区:incr方法内部就是临界区域,关键部分代码的多线程并发执行,会对执行结果产生影响。
- 竞态条件:可能发生在临界区内的特殊条件(使线程不安全的条件)。多线程执行incr方法内部的i++关键代码时,产生了竞态条件。