Andorid的好东西

第二章_Java 并发机制的底层实现原理

2018-11-12  本文已影响9人  沐小晨曦

volatile 的应用

概述

volatile 是轻量级的 synchronized,如果使用恰当的话,它比 synchronized 的使用和执行成本更低,因为它不会引起线程的上下文切换和调度问题。

volatile 的实现原理

我们知道,volatile 可以保证 “可见性”,那它是如何保证的呢?

对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在的缓存行数据会写到系统内存。但是,就算回写到内存,如果其他处理器缓存的值还是旧的,在执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改时,就会从系统内存从新读取这个数据。

简单可总结为以下两条:

  1. Lock 前缀的指令会引起处理器缓存回写到主内存
  2. 一个处理器的缓存回写到主内存会导致其他处理器的缓存失效
volatile 优化

JDK 7 的并发包中新增了一个队列集合类 LinkedTransferQueue,它在使用 volatile 变量时,用一种追加字节的方式来优化队列出队和入对的性能。

为什么追加字节能优化性能呢?

因为大多数处理器的高速缓存行是 64 字节,不支持部分填充缓存行,这意味,如果队列的头节点和尾节点都不足 64 字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点。而使用追加字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载在同一个缓存行,使得头节点、尾节点在修改时不会相互锁定。

那是不是在使用 volatile 变量的时候都应该追加到 64 字节呢?并不是,有以下两种情况不建议这种方式:

  1. 缓存行非 64 字节宽的处理器

  2. 共享变量不会被频繁的写

    如果共享变量不被频繁写的话,锁的几率非常小,就没必要通过追加字节的方式来避免相互锁定。

synchronized 的实现原理与应用

Java 中的每一个对象都可以作为锁,具体表现为以下三种形式:

  1. 对于普通方法:锁是当前实例对象
  2. 对于静态方法:锁是当前类的 Class 对象
  3. 对于代码块:锁是代码块中配置的对象

当一个对象试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面存储什么信息呢?

Java 对象头

synchronized 用的锁是存在 Java 对象头里的。

内容 说明
Mark Word 存储对象的 hashCode、分代年龄和锁标记位
Class Metadata Address 存储对象数据类型的指针
Array Length 数组的长度(如果当前对象时数组)

在运行期间,Mark Word 里存储的数据会随着锁标记位的变化而变化。

锁的升级与对比

Java SE 1.6 为了减少获取锁和释放锁带来的性能消耗,引入了 “ 偏向锁 ” 和 “ 轻量级锁 ” ,在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这种策略目的是为了提高获取锁和释放锁的效率。

锁的优缺点对比:

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗 CPU 追求响应时间,同步块执行速度非常快的场景
重量级锁 线程竞争不使用自旋,不会消耗 CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长的场景

原子操作的实现原理

处理器如何实现原子操作
  1. 通过总线锁
  2. 通过缓存锁定
Java 如何实现原子操作
  1. 循环 CAS
上一篇下一篇

猜你喜欢

热点阅读