并发中关键字的语义

一、volatile的内存语义

1. 简单的举例

可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步;但是复合操作是不生效的;

package com.youyou.ch1.demo;

public class Vola {
    volatile int a = 1; //使用volatile 声明int类型的变量

    public int getA()
    {
        return a; //对单个volatile 变量进行读
    }

    public void setA(int a)
    {
        this.a = a; //对单个volatile 变量进行写
    }
    public void inc()
    {
        a++; //对复合(多个)volatile 变量进行 读/写
    }
}
View Code

这样的代用 volatile 关键字的代码和下面的是一样的;

package com.youyou.ch1.demo;

public class VolaLikeSyn {
    int a = 0; //普通的变量

    public synchronized int getA()
    {
        return a; //对单个普通 变量进行读
    }

    public synchronized void setA(int a)
    {
        this.a = a; //对单个普通 变量进行写
    }
    public void inc() //普通方法的调用
    {
        int temp = getA(); //调用同步方法
        temp = temp + 1; //普通的写操作
        setA(temp); //调用同步的方法
    }
}
View Code

2. volatile变量自身具有下列特性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

2.1 volatile写的内存语义如下:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

2.2 volatile读的内存语义如下:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

3. JMM对volatile的内存屏障插入策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。

 4. volatile的实现原理

有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令。(这个Lock指令,使我们在反编译的时候也看不见是,是汇编层面的关键字)

将当前处理器缓存行的数据写回到系统内存 ,这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

二、锁的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

synchronized的实现原理:

1. 使用monitorenter和monitorexit指令实现的

  • monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
  • 每个monitorenter必须有对应的monitorexit与之配对
  • 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
  • sychronized“方法”通常不是用monitorenter和monitorexit指令实现的。往往是由“方法调用指令”检查常数池里的ACC_SYCHRONIZED标志

2. 了解各种锁

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态

2.1 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。

2.2 轻量级锁

无竞争时通过CAS操作来加锁和解锁。

2.3 重量级锁

三、final的内存语义

1. 编译器和处理器要遵守两个重排序规则。

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

 2. final域为引用类型

  • 增加了如下规则:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

3. final语义在处理器中的实现

  • 会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
  • 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障
原文地址:https://www.cnblogs.com/lys-lyy/p/11152452.html