1.1 硬件效率与一致性
多任务处理是在现在计算机操作系统中几乎已经是一项必备的功能了。让计算机同时处理几项任务不仅因为计算机的运算能力强大,还因为计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、或网络通信或数据库访问上,这样就导致处理器大部分时间都处在等待其他资源的状态。
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机都加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存和处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存中,这样处理器就无需等待缓慢的内存读写了。
基于高速缓存的储存交互很好的解决了处理器与内存的速度矛盾,但也引入了一个新的问题:缓存一致性。再多处理器中,每一个处理器都有自己的高速缓存,而他们由共享同一主内存,当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自缓存数据不一致。为了解决这个问题,需要各个处理器访问内存时遵循一定得额协议(缓存一致性协议)。
除了增加高速缓存外,为了使处理器内部的运算单元尽量被充分利用,处理器可能对输入的代码进行乱序执行优化,处理器会在计算之后对结果进行重组,保证最终结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个任务依赖于另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序保证。预处理器的乱序执行优化类似,虚拟机的即时编译器中也有类似的指令重排序优化。
1.2 Java 内存模型
Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存中和从内存中取出变量这样的底层细节。该处变量指的是:实例字段、静态字段、和构成数据组对象的元素,但是不包括局部变量,因为局部变量是线程私有的,不被共享,也就不会存在竞争问题。
Java 内存模型规定了所有的变量都存在主内存(类似与前面提到的物理硬件主内存)中,而每个线程都有自己的独立内存(类似于高速缓存),线程的工作内存保存了被该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写在主内存中的变量。不同线程之间也无法直接访问对方的工作内存,线程之间的变量值的传递均需要通过主内存来完成。
1.2.1一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存,由以下8中操作来完成,他们都是原子的,不可再分的。
Lock(锁定):作用于主内存变量,他把一个变量标为内存独占的状态。
Unlock(解锁):作用于主内存,它把一个处于锁定状态的变量解放出来,这样该变量才可以被其他线程访问。
Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中
Load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量的值放入工作内存的变量副本中。
Use(使用):作用于工作内存的变量,它把变量值传递给执行引擎。
Assign(赋值):作用于工作内存的变量,它把一个执行引擎接收到的值赋给工作内存的变量。
Store(存储):作用于工作内存的变量,它把一个变量的值传送到主内存中,以便后面的write操作使用。
Write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
1.2.2 Volatile 型变量
关键字volatile可以说是最轻量级的同步机制,JMM对volatile专门定义了一些特殊的访问规则 。当一个变量被volatile修饰后,它将具备两种特性:第一是保证此变量对所有线程的可见性,即指当一条线程修改了这个变量后,新值对于其它线程是立即可见的。虽然volatile变量在各个线程中是一致的,但基于volatile变量的运算却不是线程安全的。因为java里的运算并不是原子的,所以volatile变量的运算在并发下一样是不安全的。(volatile只是保证可见性,并不保证原子性)。第二是禁止指令重排序优化。
1.2.3原子性、可见性、有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特征来建立的。
原子性:JMM来直接保证的原子性操作包括read,load,assign,use,store,write,所以基本数据类型的访问读写是具备原子性的;lock和unlock可以满足一个更大范围的原子性保证,虽然用户不能直接使用这两个操作,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到java代码中就是同步快---synchronized关键字,因此在synchronized块之间的操作也具有原子性。
可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。除了volatile之外,还有两个关键字也可以保证可见性,即synchronized和final。
有序性;如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的。Volatile和synchronized关键字保证线程之间操作的有序性。
1.2.4 先行发生原则(happens - before)
先行发生原则是java内存模型中定义的两项操作之间的偏序关系,如果说操作A 先行发生于操作B,其实就是说子啊发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。
一些天然的先行发生原则:
程序次序原则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;
管程锁定规则:一个unlock先行发生于后面的对同一个锁的lock操作。后面指的是时间上的先后顺序。
Volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。后面指的是时间上的先后顺序。
线程启动规则:Thread 对象的start()方法先行发生于此线程的每一个动作。
线程终止规则:线程的所有操作都先行发生于对此线程的终止检测
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
传递性:操作A先行发生于操作B,操作B先于C,那么A先于C。
注意:一个操作“时间上的先发生”不代表这个操作是“先行发生”。反之也不成立。即:时间先后顺序与先行发生原则之间基本没有太大的关系,所以衡量并发安全问题的时候一切必须以先行发生原则为准。
1.3 Java 与线程
线程是比进程更轻量级的调度执行单位,各个线程可以共享进程资源,又可以独立调度。线程是CPU调度的基本单位。
线程调度是指系统为线程分配处理器使用权的过程,主要有两种方式:协同式和抢占式,java采用抢占式。
状态转换:
新建(New):创建后尚未启动的线程处于该状态。
运行(Runnable):Runnable包括操作系统线程状态中的Running 和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待CPU为它分配执行时间。
等待(Waiting):等待分为无限期等待(Waiting)和有限期等待(Timed Waiting),处于此状态的线程不会被分配CPU执行时间,她们要等到被其他线程显式唤醒或满足等待时间。
阻塞(Blocked):在线程等待进入同步区域的时候,线程将进入该状态。“阻塞状态”和“等待状态”的区别是:“阻塞状态在等待获取一个排他锁”,这个事件将在另一个线程放弃这个所得时候发生;而“等待状态”则是在等待一段时间或唤醒动作发生。
结束(Terminated):线程终止执行。
状态转换图: