《深入理解java虚拟机》—内存模型与线程(12)

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】

一、概述

随着现在硬件资源的提升,不希望处理器大部分时间都是处于等待其他资源的状态,可以让计算机同时处理几项任务。对于java语言中也是一样,需要做一些并发应用来满足市场的需求,所以连接并发的内幕是一个高级程序员不可缺少的课程。

二、硬件的效率与一致性

其实硬件由于处理器和内存之间读取的速度差距很大,严重影响了处理器的工作效率,为了解决这个问题,我们又在他们中间增加了好几层的高速缓存,但是这样在处理多个任务的时候,怎样保证缓存一致性也是一个大大的问题,为了解决这个问题,需要各个处理器访问缓存的时候遵循一些协议,读写时候根据协议来进行操作。对于java虚拟机来说基本上是一个道理,当然除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入的代码进行乱序执行优化,将乱序执行的结果重组,保证结果一致性,但是并不保证代码执行顺序的一致性,这个就跟java虚拟机中的即时编译器中的指令重排序优化差不多。

2019120001541\_1.png

三、java内存模型

主要是为了实现java程序在各种平台下都能达到一致的内存访问效果

1.主内存与工作内存

主内存指的是存储所有共享变量的位置,工作内存是每条线程自己的需要用到的内存,存放自己需要用到的私有变量,这些变量都是从主内存中拷贝过来的,也就是说每个线程都是与主内存的交互,相互之间是不存在交互的

2019120001541\_2.png

2.内存间交互操作

主内存与工作内存之间的交互都是保证原子性的(long和double类型的load、store、read、write操作例外),基本操作有以下几种

  • lock(锁定):作用主内存,把一个变量标识为一条线程的独占
  • unlock(解锁):作用主内存,把处于锁状态的变量释放出来
  • read(读取):作用主内存,把变量的值从主内存传输到线程工作内存
  • load(载入):作用工作内存,把从主内存获得变量放入到工作内存中
  • use(使用):作用工作内存,把工作内存中的一个变量的值传递到执行引擎
  • assign(赋值):作用工作内存,把从工作引擎获得值赋值给工作内存的变量
  • store(存储):作用工作内存,把工作内存中的一个变量传输到主内存
  • write(写入):作用主内存,把工作内存中接收到的变量的值放入主内存的变量中

其中read、load和store、write的操作顺序必须固定,但是中间可以插入其他的操作,此外这些操作还必须符合下面的规则

  • read和load、store和write操作必须成对出现
  • 不允许一个线程丢弃他最近的assign操作
  • 不允许线程在无assign操作时发生store和write操作
  • 一个变量同一时刻只能被一条线程lock,但是可以重复多次的lock
  • 当一个变量被lock操作,会清空工作内存中这个变量的值,执行引擎使用前需要重新执行load和assign操作初始化变量的值
  • 不能unlock一个没有被lock的变量,也不能是被其他线程锁定的变量
  • 对一个变量unlock之前,必须先把此变量同步回主内存中

3.对于volatile型变量的特殊规则

volatile关键字可以说是java虚拟机中提供的最轻量级的同步机制,其实在现实的开发过程中还是建议使用synchronize关键字来保证线程的安全性,那么volatile存在意义只能说明volatile的开销更小,使代码的运行效率更高,其实volatile关键字并不能完全保证代码的安全性,主要是因为不能保证原子性,但是能够保证可见性,还能够防止指令重排序带来的安全问题,所以在运用这个关键字的时候需要考虑以下的场景

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量
  • 变量不需要与其他的状态变量共同参与不变约束

4.对于long和double型变量的特殊规则

对于long和double类型的load、store、read、write操作,可以不要求原子性,叫做非原子性协定,但是现在的商用虚拟机基本上都是会保证原子性操作的,所以出现问题的几率非常非常小。

5.原子性、可见性、有序性

  • 原子性:可以认为基本数据类型的访问读写是原子性的(long和double除外),还有就是在synchronize块之间的操作也是具备原子性的
  • 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,volatile、synchronize和final关键字都是能够保证可见性的
  • 有序性:如果本线程内观察,所有操作都是有序的,如果在一个线程中观察另外一个线程,所有操作都是无序的,volatile和synchronize能够保证有序性

6.先行发生原则

先行发生原则指的是两个操作之间的偏序关系,如果符合下面的规则,就会先行发生关系,无需任何同步器协助

  • 程序次序规则:在一个线程内,书写在前面的操作优先发生于后面的操作
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成先行于他的finalize()方法的开始
  • 传递性:如果A先行发生于B,B先行发生于C,那么A先行发生于C

四、Java与线程

1.线程的实现

  • 使用内核线程实现:由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核。缺点是内核资源有限。
  • 使用用户线程实现:优势是不需要系统内核的支援,但是所有的线程操作都是用户自己处理,而且一旦出现问题很难解决,现在这种实现方式基本已经放弃。
  • 使用用户线程加轻量级进程混合实现:完美结合,效果好
  • java线程的实现:通过参数设置指定使用哪种线程模型

2.java线程调度

线程调度是指系统为线程分配处理器使用权的过程,方式有协同式线程调度和抢占式线程调度。前者实现简单,但是容易进程阻塞或者其他问题出现,后者解决进程阻塞问题,目前java就是使用抢占式线程调度。后来增加了线程优先级的支持,java中是10个级别,但是这种支持不一定就能准确实现,主要是java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

3.状态转换

java语言定义了线程的5中状态,任意一个点只能有一种状态

  • 新建:创建尚未启动的线程
  • 运行:包括Running和Ready,可能正在执行或者是等待CPU分配执行时间
  • 无限期等待:不会被CPU分配执行时间,需要等待其他线程显式唤醒
  • 限期等待:不会被CPU分配执行时间,无需要等待其他线程显式唤醒,一定时间后会由系统自动唤醒,如sleep()方法
  • 阻塞:等待着获取到一个排它锁
  • 结束:已终止的线程状态
赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » 《深入理解java虚拟机》—内存模型与线程(12)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏