深入理解Java虚拟机8-chap12-13-斗者5星

时间:2023-03-09 08:46:06
深入理解Java虚拟机8-chap12-13-斗者5星

一、操作系统与内存

  • 通过在处理器与内存之间添加一层访问及更新速度更快的高速缓存,可以一定程度解决处理器与内存速度的矛盾
  • 引入新问题:缓存一致性,即每个处理器只与自己的缓存交互,如果操作的是内存中的同一块内存,会出现数据不一致的现象

  深入理解Java虚拟机8-chap12-13-斗者5星

二、Java内存模型

  1.Java内存模型的主要目的:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

  2.变量包括:实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数,因为这是线程私有的,不会存在竞争问题。

  3.Java线程、工作内存与主线程之间的交互关系

  • 工作内存中存放了变量的主内存副本拷贝,线程对变量的操作必须在工作内存中进行,线程间变量值传递均需要通过主内存
  • 主内存对应Java堆中对象实例数据部分,工作内存对应虚拟机栈的部分区域

  深入理解Java虚拟机8-chap12-13-斗者5星

  4.内存间的交互操作,均是原子操作,不可再分(double与long类型除外)

  • lock(锁定):作用于主内存变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存变量,它把一个处理锁定的状态的变量释放出来,释放后的变量才可以被其它线程锁定,unlock之前必须将变量值同步回主内存。
  • read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存变量,它把read操作从主内存中得到的值放入工作内存的变量副本中。
  • use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存变量,它把一个从执行引擎接到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的值放入主内存的变量中。

  5.操作连续性保证,没有保证连续执行

  • 如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作
  • 如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作

  6.执行上述8条基础操作时必须满足一下规则(部分):

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变(为工作内存变量赋值)了之后必须把该变化同步回主内存。
  • 一个新变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load和assign)的变量,换话说就是一个变量在实施use和store操作之前,必须先执行过了assign和load操作。
  • 如果一个变量事先没有被load操作锁定,则不允许对它执行unlock操作:也不允许去unlock一个被其它线程锁定的变量。
  • 对一个变量执行unloack之前,必须把此变量同步回主内存中(执行store和write操作)

  7.volatile变量:最轻量级的同步机制

  1)定义volatile变量后,保证了两个特性

  • 保证变量对所有线程的可见性,指当一个线程修改了变量的值,新值对于其他线程是立即可知的,当然需要通过主存来传递

    a.不代表并发下是安全的,因为Java里的运算并非原子操作,里类变量race++,反编译后是4条字节码指令如下,当getstatic把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd时,race的值可能已经改变,所以栈顶的数据过期了,造成结果错误.

  深入理解Java虚拟机8-chap12-13-斗者5星

    b.volatile变量只保证可见性,当可能存在多线程竞争,需要通过加锁保证原子性

  • 禁止指令重排序优化:保证变量赋值操作与程序代码中的执行顺序一致,普通变量只保证赋值动作正确,不保证顺序

    a.实现原理:添加内存屏障,重排序不能把后面的指令重排序到内存屏障前

  2)volatile变量性能

  • 读操作与普通变量几乎没什么差别
  • 写操作可能慢一点,因为需要添加内存屏障来保证处理器不发生乱序执行,但仍然比锁低。

  3)volatile与锁的选择:volatile是否能满足使用场景需求

  8.Java内存模型特征

  • 原子性:基本数据访问read、load等读写具备原子性,更大场景通过lock、unlock,或者synchronized,同步块之间的操作也保证原子性
  • 可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,volatile、synchronized、final等关键字
  • 有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

  9.先行发生原则:操作A先于操作B,则A产生的影响能被B观察到,影响包括修改了内存*享变量的值、发送了消息、调用了方法

  • 程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

  结论:时间上先发生不地标操作会是先发生,因为会有指令重排序,但是指令重排序不会违背先行发生原则。

三、线程

  1.线程实现的3种方式

  • 内核线程实现:需要在用户态与内核态切换,代价较高

  深入理解Java虚拟机8-chap12-13-斗者5星

  • 用户线程实现:需要考虑创建、切换、调度等各种问题

  深入理解Java虚拟机8-chap12-13-斗者5星

  • 用户线程加轻量级进程混合实现:Java采用的模式,抢占式调度,操作系统提供支持的轻量级进程作为用户线程和内核线程的桥梁

  2.状态转换

  深入理解Java虚拟机8-chap12-13-斗者5星

四、线程安全

  1.定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象时线程安全的。

  2.Java语言中的线程安全

  • 不可变:一定线程安全,通常是把对象中带有状态的变量声明为final,如String、Number的部分子类、BigInteger等(不包括AtomicInteger)
  • 绝对线程安全:Java API声明自己为线程安全的类,大多数都不是绝对线程安全,需要在使用时进行同步
  • 相对线程安全:如容器类等
  • 线程兼容:对象本身不是线程安全的,但是在调用段正确使用同步手段可以在并发环境下安全使用
  • 线程对立:无论调用端是否采用同步措施,都无法在并发环境下正确使用,较少。

3.线程安全的实现方法

  1)互斥同步:临界区、互斥量、信号量等,Java中最基本的同步手段是synchronized,经过编译在同步块前后形成加解锁指令,如果指明对象参数,则锁定对象引用,否则锁定对象实例

  a.synchronized两层语义:

  • synchronized同步块对于同一条线程是可重入的
  • 同步块在已进入的线程执行完,会阻塞后面其他线程的进入

  b.由于synchronized会带来线程阻塞和唤醒等用户态与内核态切换的动作,耗费处理器时间,所以是一个重量级操作,可以通过自旋优化

  c.java.util.concurrent中的重入锁

  • 用法与synchronized类似
  • 添加了高级功能:等待可中断、可实现公平锁、锁绑定多个条件
  • synchronized经过优化,性能基本与重入锁持平,虚拟机未来性能改进肯定偏向原生的synchronized

  2)非阻塞同步:互斥同步是悲观的并发策略,认为只要不去做正确的同步措施,就一定出问题

  • 可以选择基于冲突检测的乐观并发策略(先进行操作,如果没有其他线程争用数据,则操作成功,如果有争用,则失败,采用其他补救(如不断重试,直到成功)),不需要挂起线程,所以称为非阻塞同步
  • 依赖指令集:需要操作和冲突检测者两个步骤具备原子性,如CAS(很多Atomic操作)(比较并交换cas(v,a,b),如果v内存内容=a,则在v中填入b),CAS存在ABA问题

  3)无同步方案:本来不涉及共享数据

  • 可重入代码:可重入一定线程安全,反之不是,特征:不依赖存储在堆上的数据和公用的系统资源等,判断原则:返回结果可预测
  • 线程本地存储:web中一个请求对应一个服务器线程,都会使用线程本地存储来解决线程安全问题

五、锁优化

  1.自旋锁与自适应自旋

  2.锁消除

  3.锁粗化

  4.轻量级锁

  5.偏向锁