深入理解Java虚拟机——第十二章——Java内存模型与线程

硬件效率与一致性

处理器需要与内存交互,但处理器运算速度与对内存的I/O操作速度相差几个数量级,因此现代操作系统不得不加入尽可能接近处理器运算速度的高速缓存来作为内存与处理器之前的缓冲。这样处理器就不用等待缓慢的内存读写。

Java内存模型

主存与工作内存

Java内存模型的主要目的是定义程序中各个变量的访问规则。这里的变量和Java编程中的变量有所区别,这里指的是实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数,因为是线程私有的,不会被共享,自然也不存在竞争问题。

Java内存模型规定所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存存储了被该线程使用到的变量的主内存副本拷贝。变量的所有操作(读取、赋值等)都必须在工作内存中运行, 而不能直接读写主内存中的变量。

内存间交互操作

lock、unlock、read、load、use、assign、store、write。

read和load、store和write的操作必须是顺序执行,但可以不用连续,比如read a、readb b、load b、load a。

 8种操作的规则:

  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程执行多次,多次lock后,只有执行相同次数的unlock,变量才会被解锁。
  • 对一个变量lock,那么会清空工作内存中该变量的值,需要重新load或assign这个值。
  • 对一个变量unlock之前,必须将变量同步回主内存

volatile变量的特殊规则

volatile可以说是最轻量级的同步机制。具备两种特性:

  1. 保证此变量对所有线程的可见性。可见性指一条线程修改了值后其它线程立即可知。普通线程不具备这个特性,必须得刷回主内存后值后才对其它线程可见。但这种特性并不能保证线程安全,具体来说无法保证变量的一致性。如多线程的++操作,volatile只能保证读变量时值是正确的,但是多个线程一起写变量,某个线程写的快,其它线程的变量就会都被废弃。在不符合下面两条规则的场景还是需要加锁(JUC或synchronized)来保证原子性:
    1. 运算结果并不依赖当前值,或者能够确保只有单一的线程能够修改变量的值
    2. 变量不需要与其它的状态变量共同参与不变约束。(不变约束就是比如说lower<upper,初始值分别为0和10,同一时刻,线程1调用setLower(8),线程2调用setUpper(2),执行上完全没问题,但是现在的lower和upper的值就变为了8和2这种无效的数据了
  2. 禁止指令重排序优化。加入内存屏障,使得CPU的Cache写入内存,该操作会使别的CPU无效化其Cache。从硬件来讲,指令重排序是将多条指令不按程序规定分开发送给各相应电路单元处理。

volatie的读和普通度的性能消耗没什么区别,但是写操作则可能会慢一些。

volatile对于操作的规则:

  • read、load、use操作必须连续出现,即保证每次都从主内存读取最新的值。
  • assign、store、write操作必须连续出现,即保证每次都把修改结果刷新到主内存

64位的数据允许分成两次32位的操作来执行,但目前虚拟机都把其作为原子操作来处理,因此使用的时候不需要专门设置为volatile。

volatile的基本数据类型能解决不同线程对get/set方法的执行,因为基本数据类型的操作是原子性的,即对其修改不依赖自身而是依赖传入的值,因此执行set方法不会说执行到一半就切到另一个线程去get。

原子性、可见性、有序性

原子性:即操作的原子性,上述8个操作中除了lock和unlock外都是

可见性:当一个线程修改了共享变量值后,其它线程能立即得知这个修改。除了volatile外synchronized和final也能实现可见性。synchronized可见性是由“在unlock之前,必须把变量同步回主内存”实现的。final是在构造器中一旦初始化完成,并且this引用没有逃逸,那么在其它线程中就能看见final字段的值。

有序性:线程内是有序的,线程之间是无序的。通过volatile和synchronized来提供线程之间有序。

Java与线程

线程的实现

Java在不同硬件和操作系统下对线程的操作统一处理

线程实现主要有3种方式:

  1. 内核实现:直接用操作系统内核支持的线程,这种线程由内核完成线程切换。一般不会直接去使用内核线程,而是使用内核线程的高级接口——轻量级进程,也就是通常意义上的线程。
  2. 使用用户线程实现:一个线程只要不是内核线程,就可以认为是用户线程,比如轻量级进程。但这种是建立在内核上的,许多操作都要系统调用,效率会受限制。因此用户线程建立在用户空间的线程库上,用户线程的建立、同步、销毁、调度完全在用户态中完成,不需要内核调度。
  3. 使用用户线程加轻量级进程混合实现。

Java线程调度

协同式线程调度和抢占式线程调度。

  • 协同式线程调度:线程的执行时间由线程本身控制,线程执行完后,主动通知系统切换到另一个线程上。
  • 抢占式线程调度:线程由系统来分配执行时间,线程的切换不由线程本身决定(Thread.yield()可以让出执行时间,但是线程本身无法获取执行时间)。Java一共有10个级别的线程优先级。Java线程是通过映射到系统的原生线程实现的,因此线程调度是取决于系统。

状态转换

Java定义了5种线程状态(等待有两种),在任意一个时间点,一个线程只能有一个状态。

  • 新建(new):创建后尚未启动的线程
  • 运行(runable):包括了操作系统中的running和ready,即处于此状态中的线程有可能正在运行,也有可能在等待CPU为其分配执行时间
  • 等待:
  • 无限期等待(waiting):处于这种状态的线程不会被CPU分配时间,要等待被其它线程唤醒。以下方法会让线程进入无限期等待:
    • 没有设置timeout的Object.wait()方法
    • 没有设置timeout的Thread.join()方法
    • LockSupport.park()方法
    限期等待(timed waiting):处于这种状态的线程不会被CPU分配时间,不过不是等待唤醒,而是在一定时间后会由系统自动唤醒。以下方法会让线程进入限期等待:
    • 设置timeout的Object.wait()方法
    • 设置timeout的Thread.join()方法
    • Thread.sleep()方法
    • LockSupport.parkNanos()方法
    • LockSupport.parkUntil()方法
  • 阻塞(blocked):线程被阻塞,与等待的区别是,阻塞是等待获取一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生。等待状态是等待一段时间或唤醒状态发生。在程序进入同步区域的时候,线程进入这个状态。
  • 结束:已终止线程的状态,线程结束执行。

 

posted @ 2019-07-29 17:24  大尾鲈鳗  阅读(143)  评论(0编辑  收藏  举报