一、前置知识介绍
1、顺序一致性模型
一种计算机理论模型,要求程序必需按照顺序执行,多线程是保证所有线程(无论是否同步)能看到相同的执行顺序,每个操作具有原子性。如图所示:
两个线程的操作通过顺序一致性模型可能会按图中操作执行,总体执行顺序交叉,但对于每个线程自身的操作保持顺序一致,且每个线程都能看到相同的如图所示的执行顺序。
2、重排序
重排序是编译器、处理器对程序执行的优化,分为三种:
- 编译器重排序;
- 指令级并行重排序;
- 内存系统重排序;
经过重排序的程序与原程序的执行顺序或许会发生改变,如:
int a = 0;
int b = 1;
以上两行代码可能发生重排序。
3、数据依赖
数据依赖大致有三种:
- 读后写;
(int a = getA();a = 10;)
- 写后读;
(a = 10;int a = getA();)
- 写后写;
(a = 11;a = 10;)
以上三种情况被称为数据依赖。
4、as-is-serial
as-if-serial语义代表无论如何重排序,执行结果不变。发生数据依赖的操作明显不符合as-if-serial语义,所以不会发生重排序。
5、数据竞争
当两个线程同属读写一个未正确同步的变量时将会发生数据竞争。
6、线程通信
线程间通信的方式有两种:
1. 共享内存;
2. 线程间通信;
线程间的通信可以实现线程同步。
7、线程同步
线程同步是为了使多线程的操作发生相对顺序以保证程序正确执行的机制:
1、通过共享内存实现线程线程同步是一种隐式实现线程同步的机制:因为程序在操作共享变量时会对变量或代码段加互斥锁,这样可以使的多个线程的操作发生相对顺序,隐式完成了线程同步。
2、通过线程间通信是一种显示实现线程同步的机制:因为线程间通信需要先发送信息,再接收返回信息,所以显示完成了线程同步。
二、Java内存模型(Java Memory Model)即JMM
1、简介
Java内存模型是为了保证java多线程程序正确执行而设计的,java实现线程同步的方式为共享内存,注:线程操作共享变量的方式并不是直接操作主内存中的共享变量,而是每个线程有其自己的工作内存,操作共享变量时是通过将变量复制到自己的工作内存中执行完成的。大体流程如图所示:
2、JMM的具体体现:
1、原子性:
java中的基本数据类型除了64位的long和double外都能保证原子性,但对于64位的long与double,在读写时会被拆分为高32位和低32位,读写都需要分两步,因而不能保证其原子性(如一个线程读取long的高32位时,另一线程写入了低32位的数据,该线程此时再获取低32位数据,则获取的数据将不会是预期的数据),所以Java提供了volatile关键字,该关键字可保证单个变量读写的原子性,但对于i++
这样的复合操作,volatile并不保证其原子性。
为保证复合操作的原子性,java还提供了synchronized关键字及jdk1.5后提供的并发包api来实现复合操作的原子性。
2、可见性:
可见性主要体现在一个线程对共享变量的读写其他线程是否可见,如果不可见,往往导致各线程之间的数据错乱,因此Java通过synchronized以及concurrent并发包实现可见性(这两种方式同时实现了原子性和可见性),在这两种方式下,一个线程获取共享变量时会清除工作内存,从主内存重新复制共享变量到工作内存,同时获取共享变量的锁(当然,锁也能加在一个方法、一段代码上),防止其他线程在此期间操作共享变量,操作完成后,将工作内存中的数据更新到主内存,然后释放锁,这样保证了每个线程在获取共享变量时永远都是最新的值。
3、可排序:
编译器、处理器等会通过重排序对程序执行进行优化,但在并发编程中,这样的重排序往往会导致数据的错乱:如
volatile int a = 0;
volatile int b = 0;
//线程A //线程B
a = 30; print(a);
b = 40; print(b);
对于单个线程来说,A、B两个线程的重排序并不会影响运行结果,但在并发编程中,线程B将会产生诸多不同的结果,因此JMM通过添加读写屏障来禁止重排序。如对于获取锁、释放锁这样的操作不进行重排序,而这之间的操作可进行重排序。