《深入理解JVM》读书笔记(五)

学习总结

Posted by 溜大虾 on July 22, 2017

第五部分 高效并发

这一部分主要讲了Java内存模型与线程相关

Java内存模型

何为内存模型?首先我们知道,Java是跨平台的,怎么实现跨平台呢?是因为虚拟。虚拟机怎么就跨平台了呢?是因为Java虚拟机定义了一种内存模型,用来屏蔽掉各种硬件和操作系统的内存访问差异,这样就让Java程序在各个平台下都能达到一致的内存访问效果。

1.主存与工作内存

1.Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取变量这样的底层细节。(此处的变量不是单纯的指Java语言中定义的变量,塔包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数)

2.Java内存模型规定了所有变量都存储在主内存中,除此之外每条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。

3.这里所讲的主内存、工作内存和Java内存区域中所谓的堆、栈、方法区并不是一个层次的内容。

4.所谓的java内存模型,是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特性来建立的。

2.内存间交互操作

1.主内存与工作内存间的交互协议,有以下八种操作。这其中的每一种操作都是原子性的,不可再分的。

  • lock:作用于主存,把一个变量标识为线程独占状态
  • unlock:作用于主存,把一个被lock的变量释放出来
  • read:作用于主存,把一个变量的值从主存传输到工作内存
  • load:作用于工作内存,把read读到的变量值放入到工作内存的变量副本中
  • use:作用于工作内存,把变量的值传递给执行引擎
  • assign:作用于工作内存,把从执行引擎接收到的值赋给变量
  • store:作用于工作内存,把变量的值传送到主存中
  • write:作用于主存,把工作内存得到的变量的值放入到主存

2.针对这八中操作,有以下八种规则:

  • read和load,store和write操作之一不能单独出现
  • 不允许线程丢弃最近assign操作
  • 不允许线程无原因的把数据从线程工作内存同步到主存中
  • 一个变量只能在主存中诞生
  • 一个变量同一时刻只能有一个线程clock
  • 一个变量clock会清空工作内存中此变量的值
  • 没有clock就不能unclock
  • unclock之前一定要先把此变量同步到主存中

3.volatile特殊规则

1.当一个变量被volatile定义后,便有了以下两个特性:可见性和禁止指令重排

2.可见性是指:当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

3.volatile定义的变量并不是说不存在一致性。相反,在多线程环境下也可能有不一致的情况。volatile变量只能保持其可见性,在不符合以下条件时,仍然需要通过锁机制来保证一致性:

  • 运算结果不依赖变量的当前值
  • 只有单一线程操作
  • 变量不需要与其他状态变量共同参与不变约束

4.普通的变量只会保证执行的结果是正确的,而不在乎指令执行的顺序如何,因此在多线程环境下,极端情况会出现问题

5.从硬件架构上说,指令重排是指CPU采用了允许将多条指令不按程序规定而顺序分开发送给各相应的电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的处理结果。

6.volatile读操作的性能消耗和普通变量几乎没有差异,但是写操作会慢一些,因为他要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。尽管如此,在大多数情况下,volatile的总开销还是要比锁要低很多。

总结:对于volatile和锁,处于对性能的考虑,只有在volatile的语义无法满足时才应该选择锁。

4.原子性 可见性 有序性

  • 原子性:由Java内存模型来保证具有原子性的是基本数据类型(除long double因其长度特性因此不是原子性)外,还有用synchronized修饰的代码同步块。
  • 可见性:可见性是指一个变量的值被修改后,其他线程能立刻得知这一个修改及获得新的值。java通过volatile来确保可见性。
  • 有序性:简单概括为,在本线程内观察,所有线程都是有序的。在另一个线程中观察,所有操作都是乱的。

5.先行发生原则

1.先行发生规则很重要,他是判断数据是否存在竞争、线程是否安全的主要依据。

2.先行发生是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,也就是说在操作B之前发生,操作A产生的影响能被操作B观察到。这里的影响是包括内存中共享变量的值、发送了消息、调用了方法。

3.时间先后顺序和先行发生原则之间基本没有太大关系,所以我们衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准

Java线程

1.线程的实现

实现线程有以下三种方式:

  • 使用内核线程实现:直接由操作系统内核支持,由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责江线程的任务映射到各个处理器上。程序要使用的是操作系统内核提供的高级接口——轻量级进程。
  • 使用用户线程实现:从广义上讲,只要不是内核线程,就是用户线程。狭义上讲用户线程指完全建立在用户空间的线程库上,系统内核不能感知线程存在。这是一种1:N的关系
  • 用户线程和轻量级进程混合实现:用户线程完全建立在用户空间中,操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁

优缺点:

 
内核线程 一个进程的阻塞不会影响其他进程 所有操作都要系统调用,代价高,要消耗一定的内核资源
用户线程 不需要系统内核支持 所有线程操作均需要用户手动完成
混合 兼具两者有点 实现复杂

2.java线程的实现:在jdk1.2之前是由用户线程实现的。之后转为由操作系统原生线程模型实现。因此现在操作系统支持什么样的线程,很大程度上就决定了jvm的线城实现。

2.Java线程调度

1.线程调度是指系统为线程分配处理器使用权的过程,主要有两种:协同式和抢占式。

  • 协同:执行时间由线程本身来控制,实现简单,没有线程同步问题,但是时间不可控,会产生阻塞
  • 抢占:由系统分配时间,实现复杂,会产生同步问题,执行时间可控,不会产生阻塞

Java使用了抢占式调度

2.虽然是由系统调度执行,但也可以通过用户设置优先级的方式来做改动。

3.虽然可以通过设置优先级调度,但由于线程终究还是映射到系统原生线程上来实现,所以最终调度还是取决于系统,而系统线程优先级和Java又不一定一一对应,因此优先级还是不太靠谱的。

3.状态转换

Java定义了线程有五种状态:

  • 新建:创建后未运行
  • 运行:可能正在执行,也可能正在等待分配时间
  • 无限期等待:不会被分配时间,要等待直到被其他线程唤醒
  • 限期等待:也不会分配时间,不过不用等待其他线程唤醒,只要等到一定时间之后就会由系统自动唤醒
  • 阻塞:阻塞状态与等待状态的区别在于,阻塞是在等待着获得一个锁,这个锁由其他线程释放。等待指示由其他线程唤醒。
  • 结束:线程已经结束执行