logo头像

Always believe youself.

JVM-7内存模型

内存模型

【java 内存模型】 是 Java Memory Mdel (JMM)的意思。 JMM 定义了一套在多线程读写共享数据时(成员变量,数组)时,对数据的可见性,有序性和原子性的规则和保障。

原子性

多线程,会因为指令交错而产生错误的结果。主要因为 更新丢失 (某次指令计算后未完成更新)被其他线程更新。

用 synchronized 进行保证。monitor 是进行监控的,第一个线程过来后进行加锁(owner ),其他线程就执行不了(会阻塞,在entryList 排队),等待第一个线程执行结束(通知monotor),释放锁(monitorexit),其他的再争抢锁。

可见性

image.png

volatile 是可以保证可见性的。

synchronized 语句块是即可以保证代码块的原子性,也同时可以保证代码块内变量的可见性,但缺点是 synchronized 是属于重量级操作,性能相对更低。

有序性

在指令运行时会有 【指令重排】 时JIT 编译器在运行时的一些优化。部分代码的执行顺序的调整。

volation 修饰的变量,可以禁止指令重排。

image.png

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance 才使用 synchronized 加锁,后续使用时无需加锁

在多线程环境下,上面代码是有问题的。

image.png

happens-before 规定了哪些写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结。

CAS 与 原子类

乐观锁 和 悲观锁

  • cas 是基于乐观锁的思想: 最乐观的估计,不怕别的线程来修改共享的变量,就算是改了也没关系,再重试。
  • synchronized 是基于悲观锁的思想: 最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想修改,我改完了 解锁后你们才有机会。

CAS

cas 配合 volation 使用的。

可称之为 无锁并发,不加锁的。

image.png

  • 因为没有使用 synchronized ,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

CAS 底层依赖与一个 Unsafe 类(通过反射调用)来直接调用操作系统底层的 CAS 指令。

原子类

juc 中提供了原子操作类,可以提供线程安全的操作,例如,AtomicInteger AtomicBoolean 等,他们底层就是采用 CAS 技术 + volatie 来实现的。

synchronized 优化

java HotSpot 虚拟机中,每个对象都有对象头,(包括class 指针 和 mark Word) 。 Mark word 平时存储这个对象 的 哈嘻码,分代年龄, 当加锁时,这些信息根据情况呗替代为 标记位,线程锁记录指针,重量级锁指针,线程 ID 等内容。

优化前还是偏重量级的,在优化之后性能可能比 cas 更好。

轻量级锁

image.png

轻量级锁的加锁的过程:

每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark word。

锁膨胀

如果在尝试加轻量级锁的过程中, CAS 操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时就需要进行锁膨胀,将轻量级锁变为重量级锁。

重量级锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功,(即这时候持锁线程已经退出了,同步块,释放了锁) 这时当前线程就可以避免阻塞。

在 java 6 之后自旋锁时自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就少自旋甚至不自旋。总之,比较智能。

  • 自旋会占用 cpu 时间,单核cpu 自旋就是浪费,多核 cpu 自旋才能发挥优势。
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞,(等待的时间长了划算)
  • java 7 之后不能控制是否开启自旋功能。

自旋成功:线程2 多次自旋后,线程1 已经执行完,释放了锁,线程2 就直接加锁。
自旋失败: 线程2 多次自旋后,线程1长时间执行,多次重试后失败,进行阻塞。即自旋失败。

偏向锁

image.png

其他优化

  • 减少上锁时间: 同步代码块中尽量短
  • 减少锁的粒度: 将一个锁拆分为多个锁提高并发度:

    • ConcurrentHashmap
    • LongAdder 分为 base 和cells两部分。
    • LinkedBlockingQueue 入队和出队 使用不同的锁。相对于Linked BlockingArray 只有一个锁效率更高。
  • 锁粗化:可以对多 sync 进行 优化,粗化为一次。

  • 锁消除:JVM 会进行代码的逃逸分析,例如某个加锁对象时方法内局部变量,不会被其他线程所访问到,这时候就会被即时编译器忽略掉所有的同步(加锁)操作。
  • 读写分离