线程安全性
如果一个类在单线程环境下能运行正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运行正常,那么这个类具有线程安全性。
竞态的模式:read-modify-write(读-改-写)和check-then-act(检测而后行动)。
原子性
原子性:访问(读、写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,即其他线程不会看到该操作执行了部分的中间效果。
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
public class AtomicityDemo { /** * person初始name = 李四,age = 30 * 线程1将person更新为name = 张三,age = 20,线程2得到person * 1、线程1执行updatePerson方法的person.setName(name); name = 张三 * 2、线程2执行getPerson方法的person.getName();name = 张三 * 3、线程2执行getPerson方法的person.getAge();age = 30 * 4、线程1执行updatePerson方法的person.setAge(age); age = 20 * 此时线程2得到的是name = 张三,age = 30,和预期结果不一致。 * 所以updatePerson需要保证原子性,保证setName、setAge直接不能插入任何操作,它们应该为一个原子,不可分割。 */ private Person person; public void updatePerson(String name , int age){ person.setName(name); person.setAge(age); } public void getPerson(){ String name = person.getName(); int age = person.getAge(); } static class Person{ private String name; private int age; //get、set方法 }
保证原子性的方法:使用锁和CAS指令。它们能够保障一个共享变量在任意一个时刻只能够被一个线程访问。
Java语言规范规定(Java内存模型要求)long和double类型以外的任何变量的写操作都是原子性的。
Java语言规范规定对于long/double类型的变量加上volatile关键字之后,对long/double变量的写操作是原子性。
volatile关键字仅能保证写操作原子性,不能保证竞态模式下的原子性。
注:long/double类型变量会占用64位的存储空间,而32位Java虚拟机对这种变量的写操作可能会被分解为两个步骤来实施,比如先写低32位,再写高32位,在多线程共享这个环境变量的情况下,可能会出现一个线程在写高32位的时候,另外一个线程正在写低32位的情形。
可见性
可见性:多线程并发读写变量,能及时的感知。
硬件级别可见性的问题:
1、每个处理器都有自己的寄存器,所以多个处理器各自运行一个线程的时候,可能某个变量给放到寄存器里去,各个线程没法看到其他处理器寄存器里的变量的值修改了。
2、一个处理器运行的线程对变量的写操作都是针对写缓冲来的并不是直接更新主内存,所以可能导致一个线程更新了变量,但是仅仅是在写缓冲区里罢了,没有更新到主内存里去。其他处理器的线程是没法读到它的写缓冲区的变量值。
3、一个处理器的线程更新了写缓冲区之后,将更新同步到了自己的高速缓存里(或者是主内存),然后还把这个更新通知给了其他的处理器,但是其他处理器可能就是把这个更新放到无效队列里去,没有更新他的高速缓存,此时其他处理器的线程从高速缓存里读数据的时候,读到的还是过时的旧值。

MESI协议实现可见性:
flush处理器缓存:把自己更新的数据强制刷新到高速缓存(或者是主内存),确保数据不停留在写缓冲器里面。
发送一个消息到总线(bus),通知其他处理器,某个变量的数据被他给修改了。
refresh处理器缓存:处理器中的线程在读取一个变量的数据的时候,如果发现其他处理器的线程更新了变量的数据,必须从其他处理器的高速缓存(或者是主内存)里,读取这个最新的数据,更新到自己的高速缓存中。

有序性
有序性:代码指令重排序。
//线程1: prepare(); // 准备资源 flag = true; //线程2: while(!flag){ Thread.sleep(1000); } execute(); // 基于准备好的资源执行操作
线程1重排序之后,让flag = true先执行了,会导致线程2直接跳过while等待,执行某段代码,结果prepare()方法还没执行,资源还没准备好,此时就会导致执行操作出现异常。
指令重排序:

处理器重排序:
在现代处理器里面都是走的指令的乱序执行机制,把编译好的指令一条一条读取到处理器里,但是哪个指令先就绪可以执行,就先执行,不是按照代码顺序来的。每个指令的结果放到一个重排序处理器中,重排序处理器把各个指令的结果按照代码顺序应用到主内存或者写缓冲器里。

内存重排序:
处理器会将数据写入写缓冲器,这个过程是store;从高速缓存里读数据,这个过程是load。写缓冲器和高速缓存执行load和store的过程,都是按照处理器指示的顺序来的,处理器的重排处理器也是按照程序顺序来load和store的。
1、LoadLoad重排序:一个处理器先执行一个L1读操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再L1。
2、StoreStore重排序:一个处理器先执行一个W1写操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再W1。
3、LoadStore重排序:一个处理器先执行一个L1读操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再L1。
4、StoreLoad重排序:一个处理器先执行一个W1写操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再W1。
假设处理器Processor0和处理器Processor1上的两个线程如下表所示执行各自的代码,其中data、ready是这两个线程的共享变量,初始值分为为0和false。
Processor0 | Processor1 |
---|---|
data = 1; //S1 | |
ready = true; //S2 | |
while(!ready){ L4 } //L3 | |
System.out.println(data); //L4 |
正常情况下:Processor0执行完操作,S1操作在S2之前。Processor1的L3操作一直循环,当读到ready = true时,打印data的值。因为S1在S2之前,所以在读取S2的结果也会读取到data = 1。
如果S1被重排序到S2之后,这就导致Processor1的线程读取到ready的值为true,而S1的操作结果仍然停留在Processor0的写缓存器之中,写缓存器对不同的处理器不可见,因此,L4的执行结果输出0。