Java并发编程的相关知识(2)--volatile和synchronized和final关键字,happens-before

java并发编程底层原理 多线程编程中synchronized和valatile都扮演重要的角色,volatile是轻量级的synchronized。


多线程编程中synchronized和valatile都扮演重要的角色,volatile是轻量级的synchronized。

volatile

volatile保证了共享变量的“可见性”。
可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
原子性:对单个volatile变量的读、写具有原子性,但类似volatile++这种复合操作不具有原子性。
缓存一致性在缓存中的共享变量保持数据一致性
volatile的两条实现原则。

  • Lock前缀指令会引起处理器缓存回写到内存中。
  • 一个处理器的缓存会写到内存会导致其他处理器的缓存无效。

volatile的使用优化

LinkedTransferQueue队列集合类在使用volatile变量时,用一种追加字节的方式来优化队列和入队的性能。
追加到64字节原理:一般处理器的缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,如果队列的头结点和为节点都不足64字节,处理器会将他们都读到一个高速缓存行中,**在多处理器下每个缓存器都会缓存同样的头结点,尾结点。当一个处理器试图修改头结点时,会将整个缓存行锁定,在保证缓存一致性的情况下,会导致其他处理器不能访问自己高速缓存中的尾结点,**而队列的入队和出队操作则需要不停修改头结点和尾结点。所以在多处理器的情况下将会严重影响到队列的入队和出队效率。
不使用这种方式的场景:
(1)缓存行非64字节宽的处理器。
(2)共享变量不会被频繁地写。

volatile的内存语义

从内存语义角度来说,volatile的读、写和锁的释放。获取有相同的效果:volatile写和锁的释放有相同的内存语义,volatile读与锁的获取有相同的内存语义。
volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本都内存中的共享变量值刷新到主内存。
volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
在这里插入图片描述

volatile内存语义的实现

为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。
=1.JMM针对编译器制定的volatile重排序规则如下:
在这里插入图片描述
结论

  • 当第二个操作是volatile写时,无论第一个操作是什么,都不能重排序。确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,无论第二个操作是什么,都不能重排序。确保volatile读之后的操作不会被编译器重排序到volatile写之前。
  • 当第一个操作是volatile写时,第二个操作是volatile读时,不能重排序。

2.编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatile读操作的前面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障
    上述内存屏障策略非常保守,但你能保证在任意处理器平台,任意的程序中都能得到正确的volatile的内存语义。
    volatile写插入内存屏障后的生成的指令序列示意图
    在这里插入图片描述
    volatile读插入内存屏障后的生成的指令序列示意图
    在这里插入图片描述

synchronized

Java中的每一个对象都可以作为锁。具体表现3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是synchronized括号里配置的对象。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处,任何对象都有一个monitor与之关联,当且一个monitor对象被持有后,它将处于锁定状态。

锁的内存语义

在这里插入图片描述

  • 线程A释放一个锁,实质上是线程A想接下来将要获取这个锁的某个线程发出了消息。
  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的消息。
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A 通过主内存向线程B发送消息。

锁的内存语义实现

在ReetrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。
ReetrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer.AQS使用一个整型的volatile变量来维护同步状态。ReetrantLock的类图如下:
在这里插入图片描述
ReetrantLock分为公平锁和非公平锁,
使用公平锁时
加锁方法lock调用ReetrantLock>FairSync>AQS(acquire)>AQS(tryAcquire).tryAcquire里面获取volatile的关键字state变量,然后CAS更新state
释放锁时,ReetrantLock(unlock)>AQS(release)>Sync(tryRelease)
公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取一个volatile变量后立即变得对获取锁的线程可见。
使用非公平锁时
加锁方法lock调用ReetrantLock(lock)>NonfairSync(lock)>AbstractQueuedSynchronizer:CAS
CAS:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
CAS此操作具有volatile读和写的内存语义。

concurrent包的实现

首先,声明共享变量为volatile.然后,使用CAS的原子条件更新来实现线程之间的同步。同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。concurrent包的实现如下:
在这里插入图片描述

final域的内存语义

对于final域,编译器和处理器要遵守两个规则。

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
    写final重排序规则可以确保:在对象引用为任何线程可见之前,对象的final域已经被正确初始化过了,而普通域不具备这个保障
    读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。
    对于final域是引用类型final int [] intArray
    ==增加约束:==在=构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

happens-before的定义

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C ,那么A happens-before C.
  5. start()规则:如果线程A执行操作ThreadB.start(),那么A 线程的Thread.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A 执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程B中的任意操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值