Java并发机制底层实现-Java内存模型


Java内存模型

提出问题

  1. 线程之间如何进行通信
  2. 线程之间如何进行同步

通信

两个通信方式

  1. 共享内存
  2. 消息传递

同步

  1. 共享内存之中,线程间是显示同步的
  2. 消息传递是隐式同步因为接收消息是发生在发送之后

Java并发使用的是共享内存的模型,其中的通信总是隐式进行的,整个过程对程序员全程透明。

JMM

Java中的实例域,静态域,数组元素都存储在堆内存中,堆内存在线程之间是共享的

JMM定义了一个线程和主存之间的抽象关系:线程之间的共享变量是存储在主内存之中的,每个线程有一个虚拟的本地内存(一个抽象概念),其中包含了缓存,写缓冲区,寄存器等硬件的优化

简易的通信模型

线程A写-》本地内存A-》主存

主存-》本地内存B-》线程B读

写是相当于发送消息,而读就是获得消息

线程之间发送消息必须要经过主存

从源代码到指令序列的重排序

为了提供性能,处理器和编译器都会进行重排序:

以下按顺序进行:

  1. 编译器重排序
  2. 指令级重排序
  3. 内存系统重排序(其中2,3统称为处理器重排序

内存屏障

为了防止一些特定情况下的处理器重排序,JMM需要在一些操作中加入内存屏障指令!!! (Memory Barriers , Intel称之为Memory Fence)

LoadLoadLoadStoreStoreStoreStoreLoad数据依赖
SPARC-TSONNNYN
x86NNNYN
IA64YYYYN
PowerPCYYYYN

以上是不同处理器对于内存屏障的支持情况,可以看到Store-Load是基本上处理器都会支持的,它也十分强大,可以代替其他的内存屏障,就是消耗大,同时出现数据依赖情况的话处理器是都不支持内存屏障的。内存屏障是用来支持内存可见性的!

屏障类型指令示例说明
LoadLoad在volatile的读操作后会有此指令确保volatile的读指令先于后续的普通读指令以及其他操作
LoadStore在volatile的读指令后会有此指令确保volatile的读指令先于后续的普通写指令和其他操作
StoreStore在volatile的写指令前确保volatile的写指令之前的的Store指令已经对其他处理器可见(刷新到内存)
StoreLoad在volatile的写指令后确保volatile写指令执行完

Happens before思想

规则:

  1. 程序顺序规则:线程中的每个操作,happens-before线程的后续操作
  2. 监视器操作:一个锁的解锁是happens-before一个锁的加锁
  3. volatile变量规则:一个volatile变量的写优先于一个volatile变量的读
  4. 传递性:A - B B-C A-C

注意:这个思想并没有要求这个指令一定按照这个顺序来,这个思想的目的是前一个操作的结果对于下一个操作是可见的。也就是程序员的角度来看,只要最后的结果和按顺序来的一致就好

重排序

数据依赖性

  1. 写后读
  2. 读后写
  3. 写后写

数据依赖性也就是如果数据依赖于前面的数据,如果执行顺序发生改变将会影响程序的结果

所以编译器和处理器就不会对有数据依赖性的数据进行重排。但是这种对数据依赖性的保护只是在一个线程中,在多线程的情况下做不到保护数据依赖性,这时候就需要自己引入同步机制。

as-if-serial

这个语义其实就一个核心,程序执行的结果不能改变,其实执行的顺序可能发生了变化,但是给了程序员一种假象,这个是按他写的代码顺序来执行的。

程序顺序规则

如前文提到,happens-before思想并不会强求去按顺序执行,JMM仅仅是要求前一个的结果对后一个操作可见,并且改变顺序并不会影响执行的结果,那么JMM就会允许这样的重排序

其实软硬件就一个目标:不改变程序的结果的前提下去实现高效

重排序会影响多线程

重排序很有可能会影响多线程的结果

这里有一个格外的点:存在控制依赖关系时,编译器和处理器会采用猜测的方法,先提前读取计算值,并存在重排序缓冲中(Reorder Buffer)。

但是在多线程中,这样的做法,很有可能就会影响计算结果

顺序一致性

数据竞争的概念:

  1. 在一个线程中写一个变量
  2. 在另一个线程中读取同一个变量
  3. 而且写和读没有正确的同步

这样的数据竞争就很容易引起错误的结果

如果程序正确同步了,那么就把这样的执行称之为具有顺序一致性

顺序一致性内存模型

  1. 在同一个线程中,所有操作按代码顺序执行
  2. 所有程序都只能看到一个单一的操作执行顺序。在顺序一致性的内存模型中,每一个操作都必须原子执行并且立刻对所有线程可见;

可以理解为在一个时间,内存只允许一个线程使用,使用完以后就要把数据同步

线程A :1, 2, 3

线程B :4, 5, 6

Type1:线程间正确的同步了

1, 2, 3, 4, 5,6

这种执行方式一看就是符合顺序一致性

对于JMM来说,这时候可能会做出一些优化,而这时候的重排序并不会影响程序的结果,而且效率更高;

Type2:没有同步

4, 1, 2, 3, 5, 6

可以看到从整个程序上来看,这是无序的,但是从单个线程来看,这是有序的。

两个线程都能看到这个顺序;这是因为其中每一个操作都是对任意线程立刻可见的

但是在JMM中,并没有保障这个操作都是对任意线程立刻可见的,只有在当前线程的本地内存刷新到了主存中去。那么才会可见。

对于没有同步的线程,JMM只提供最小安全性,也就是你读取到的不能是无中生有的值,都是之前赋值的值或者初始值。

两个模型的差异:

  1. 顺序一致性保证单线程内的操作按程序的顺序执行,JMM不保证
  2. 顺序一致性保证所有操作都是对所有线程可见的,JMM不保证
  3. JMM不保证对于64位的long double的写原子性,而顺序一致性保证。这是因为对32位计算机来说,64位的读写要分成两次来进行。

volatile的内存语义

先进行一个总结之后与锁对比学习

  1. volatile的写操作相当于消息传递的发送消息
  2. volatile的读操作相当于获得消息

对volatile的新理解:

volatile long v1 = 0L;

public void set(long l) {
    v1 = 1;
}

public void getAndIncreme() {
	v1 ++;
}

public long get() {
    return v1;
}

相当于把读写上锁:

long v1 = 0L;

public synchronized void set(long l) {
    v1 = 1;
}

public void getAndIncreme() {
	v1 ++;
}

public synchronized long get() {
    return v1;
}

其中两段代码的效果是一致的

总而言之

volatile变量自身特性:

  1. 可见性。对一个volatile变量的读,总是能看到这个volatile变量最后的写入
  2. 原子性。对于单个的volatile变量的读/写具有原子性,但是类似于volatile++就不具有

volatile的可见性主要是来自于volatile底层使用了缓存锁定,busring + MESI

从JSR-133开始volatile的写-读实现了线程的通信

写:线程A-》本地内存-》主内存

读:主内存-》本地内存-》线程B

写的时候就会给其他的线程发送消息,我已经对主存中的这个变量进行修改了,你们的本地内存里面的已经无效啦。(也就是MESI协议的操作

其他的线程需要根据主存去修改自己的本地内存;

volatile内存语义的实现

主要还是内存屏障!!!

  1. 在volatile写之前插入StoreStore
  2. 在volatile写后面插入StoreLoad
  3. 在volatile读后面插入LoadLoad
  4. 在volatile读后面插入LoadStore

内存屏障的插入策略很保守,但可以处理任意的处理器平台。

也体现了JMM的策略,保证程序的正确性的前提下,再去追求执行效率,但是也会根据具体情况去优化缩减内存屏障

JSR-133增强了volatile的内存语义

主要也就是增强了volatile变量的读写和普通变量的读写的重排序规则

锁的内存语义

这个可以对比着前面的volatile来学习

  1. 释放锁相当于发送消息
  2. 获得锁相当于获得消息

释放锁happens-before获得锁

volatile写-读 和 锁释放-获得有一样的内存语义

锁内存语义的实现

可重入锁reentrantLock

通过lock()方法获得锁,unlock()去释放锁。底层是依赖了AQS(AbstractQueuedSynchronized)

AQS后期还要细分整理,其中还有共享类型。

它是定义了一个双端队列,队列的每个结点有不同的状态;同时维护一个volatile变量state,去判断锁有多少线程在请求;

ReentrantLock分为公平锁和非公平锁

公平锁

可以理解为排队的锁,一个个排在队列中,等待着去调用,但是需要有唤醒线程的消耗

lock()的调用轨迹:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. ReentrantLock:tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可以看到,其中获得锁就是在getState这个上,state是一个volatile变量。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

释放锁就是写State变量

非公平锁

可以理解为有插队的锁,锁到了之后先用CAS去插队,插队成功就占有锁,失败就滚回去排队;

compareAndSetState就是CAS可以代替volatile的读写操作

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

为什么CAS可以实现读写volatile的内存语义

还是根据了cmpxchg这个指令,这个指令帮助实现;

同时会用os::is_MP()做一个判断是否为多处理器机器,如果是的话,CAS的指令需要加lock指令

intel手册上对lock前缀的说明:

  1. 确保对内存读-改-写的原子执行,在Pentium以及之前的处理器,是进行总线锁,但是这样开销太大,在P6处理器开始,Intel使用缓存锁。
  2. 禁止该指令之前与之后的读写指令重排
  3. 把写缓冲区的数据全部刷新到内存

其中2, 3就是内存屏障效果,以及可以完全代替volatile

所以,锁释放-获得的内存语义

  1. 可以通过volatile的写-读操作
  2. CAS所附带的volatile的写读内存语义

concurrent包的实现

通用实现模式:

  1. 声明共享变量为volatile
  2. 使用CAS的原子条件更新来实现线程之间的同步
  3. 同时配合volatile的读写和CAS的同样效果来实现线程之间的通信

基本上所有的线程同步和锁都是基于volatile写-读和CAS。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我将回答你的问题。首先,Java中的ReentrantLock是一种可重入的互斥锁,它的底层实现依赖于CAS(Compare and Swap,比较并交换)算法和AQS(AbstractQueuedSynchronizer,抽象队列同步器)框架。CAS算法是一种无锁算法,在Java中通过sun.misc.Unsafe类来实现。它利用CPU底层的原子指令完成对内存中的数据进行原子性操作,保证了数据的一致性。而AQS框架则是一种实现锁、信号量等同步操作的基础框架,主要通过 FIFO 队列实现等待机制,同时提供了公平锁和非公平锁两种实现方式。 Java内存模型是一种抽象的概念,它定义了各种线程在何时、何地和如何访问共享内存。Java内存模型通过保证原子性、可见性和有序性来保证线程之间的并发执行正确性。其中原子性是指操作的不可分割性,比如对于32位的数据,它们必须被一次性地读取或写入,不能被分解成两个16位的操作。可见性是指对于共享变量的修改对于其他线程来说是可见的,主要通过volatile关键字和synchronized关键字来保证。有序性是指执行顺序必须满足一定规则,比如一个事件在发生前必须先发生另一个事件。 Java线程池是一种可以提高程序性能的技术,在Java中通过ThreadPoolExecutor类来实现。它主要包括核心线程池、任务队列、最大线程池、线程工厂和拒绝策略等几个部分。其中核心线程池和最大线程池决定了线程池的线程数量,任务队列决定了线程池中的任务调度策略,线程工厂则决定了线程池中线程的创建方式,而拒绝策略则是当任务队列已经满了且线程池中的线程已被占用时,如何处理新的任务请求。 最后来回答你的问题:volatile关键字是一种Java线程间的同步机制,它保证一个变量在多个线程之间的可见性,也能保证一定程度的指令重排序。当一个变量被声明为volatile后,所有线程都能看到这个变量的最新值,而不管这个变量是否在本地CPU缓存中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值