打赏

Java 并发系列之二:java 并发机制的底层实现原理

 

1. 处理器实现原子操作

 

2. volatile

 

/**
补充:
    主要作用内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题。

    轻量级的synchronized,不会造成阻塞。性能比synchronized好得多,不支持原子性操作。为了保证原子性要使用atomic对象,只能保证本身方法的原子性,不能保证多次操作的原子性。(解决方法synchronized)

    原理:当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将变量上的操作与其他内存操作一起重排序。不会被缓存在寄存器或者对其他处理器不可见的地方,因此读取volatile类型的变量时总会返回最新写入得值。

    缺点:如果在代码中使用volatile变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难理解。就加锁机制可以确保可见性又可以确保原子性
何时使用volatile关键字:仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。
  
正确使用:volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭) 使用volatile关键字需要满足的三个条件 1.对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。 2.该变量不会与其他状态变量一起纳入不变性条件中。 3.在访问变量时不需要加锁。 典型用法:检查某个状态标记判断是否退出循环。
抄录来源https://www.jianshu.com/p/54b5aef07505*/

 

3. synchronized

图片来自:https://blog.csdn.net/weixin_36152448/article/details/82380343

4. CAS

5. 锁的内存语义

6. txt

  1 Java 并发机制的底层实现原理
  2     处理器实现原子操作
  3         两个机制
  4             1. 通过总线锁来保证原子性
  5                 处理器提供的LOCK 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器就可以独占共享内存。
  6                 Lock前缀指令会引起处理器缓存回写到内存
  7                 一个处理器的缓存回写到内存会导致其他处理器的缓存失效
  8                 总线锁将CPU和内存之间的通信锁住了,导致锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁比缓存锁开销大。
  9             2. 通过缓存锁来保证原子性
 10                 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存
 11                 缓存锁定
 12                     指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言Lock 信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性
 13                 缓存一致性
 14                     缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已经被锁定的缓存行的数据时,会使缓存行无效。
 15                     每个处理器通过嗅探在总线上的传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存里。
 16                     保证内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致
 17                 不会使用缓存锁的情况
 18                     1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁定
 19                     2. 有些处理器不支持缓存锁定
 20             最近的处理器里,LOCK #信号一般不锁总线,而是锁缓存
 21         Intel提供很多Lock前缀的指定来实现
 22             位测试和修改指令
 23                 BTS, BTR, BTC
 24             交换指令
 25                 XADD,CMPXCHG
 26             操作数和逻辑指令
 27                 ADD OR
 28             被这些指令操作的内存区域会上锁,导致其他处理器不能同时访问它
 29         Lock前缀指令在多核处理器下会引发两件事情
 30             Lock前缀指令会引起处理器缓存回写到系统内存
 31             一个处理器的缓存回写到内存会导致在其他CPU里缓存了该内存地址的数据无效
 32     volatile
 33         特性
 34             volatile可见性:对一个volatile的读,总可以看到任意线程对这个变量最终的写
 35             volatile原子性:volatile对单个读/写具有原子性(64位Long、Double),但是复合操作除外,例如 i++
 36             volatile有序性:编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
 37         内存语义
 38             当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
 39             当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主存中读取共享变量
 40             volatile的写-读与 锁的释放-获取 有相同的内存效果:volatile写和锁的释放有相同的内存语义,volatile读和锁的获取有相同的内存语义。这也是增强volatile的原因,提供了一种比锁更轻量级的线程之间的通信机制。
 41         重排序规则
 42             1. 当第二个操作是volatile写操作时,不管第一个操作是什么,都不能重排序
 43             2. 当第一个操作是volatile读操作时,不管第二个操作是什么,都不能重排序
 44             3. 当第一个操作是volatile写操作,第二个操作是volatile读操作,不能重排序
 45         内存语义的实现
 46             内存屏障
 47                 内存屏障是一组处理器指令,用于实现对内存操作的顺序限制
 48                 基于保守策略的JMM内存屏障插入策略
 49                     在每个volatile写操作的前面插入一个StoreStore屏障
 50                         保证之前的都能刷新到主存
 51                     在每个volatile写操作的后面插入一个StoreLoad屏障
 52                         保证先写后读,能提高效率
 53                     在每个volatile读操作的后面插入一个LoadLoad屏障
 54                     在每个volatile读操作的后面插入一个LoadStore屏障
 55                 实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障
 56                 x86处理器只会对写读作重排序,故只有一个屏障StoreLoad即可正确实现volatile写-读的内存语义
 57         操作系统语义
 58             主存、高速缓存(线程私有)缓存一致?
 59             解决方案
 60                 通过在总线加 Lock 锁的方式,有些是缓存锁定
 61                 通过缓存一致性协议 MESI协议(修改、独占、共享、无效)
 62             实现原则
 63                 Lock前缀指令会引起处理器缓存回写到内存
 64                 一个处理器的缓存回写到内存会导致其他处理器的缓存失效
 65         内存模型
 66             内存屏障 限制重排序
 67             happens-before中的volatile规则
 68         volatile VS synchronized
 69             volatile是轻量级的synchronized
 70             使用恰当,它比synchronized使用和执行成本更低,因为不会引起线程上下文的切换和调度
 71     synchronized
 72         同步、重量级锁
 73         只有使用Synchronized线程处于阻塞,其他Lock, AQS, LockSupport等线程都是处于等待状态
 74         原理
 75             synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证变量的内存可见性。
 76         锁对象
 77             1. 普通同步方法锁,是当前实例对象
 78             2. 静态同步方法,锁是当前类的class对象
 79             3. 同步方法块,锁是括号里面的对象
 80             java中的每一个对象都可以作为锁
 81         实现机制
 82             Java对象头
 83                 synchronized用的锁是保存在Java对象头里的
 84                 包括两部分数据
 85                     Mark Word(标记字段)
 86                         Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
 87                         包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
 88                     Klass Pointer(类型指针)  
 89             monitor
 90                 java 中 Object和Class中都关联了一个Monitor,一个monitor只能被一个线程拥有
 91                 Owner 活动线程
 92                     初始时为NULL表示当前没有任何线程拥有该monitor record, 当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
 93                 实现
 94                     同步代码块采用 monitorenter、monitorexit指令显示的同步
 95                     同步方法使用ACC_SYNCHRONIZED标记符隐式的实现
 96         锁优化
 97             自旋锁
 98                 该线程等待一段时间,不会被立即挂起,循环看持有锁的线程是否会很快释放锁
 99                 自旋数字难以控制(XX: preBlockSpin)
100                 存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失。
101                 缺点
102                     自旋次数无法确定
103             适应性自旋锁
104                 自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
105                 自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
106             锁消除
107                 若不存在数据竞争的情况,JVM会消除锁机制
108                 判断依据
109                     变量逃逸
110             锁粗化
111                 将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获得锁
112             轻量级锁
113                 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
114                 通过CAS(CompareandSwap),即比较并替换,来获取和释放锁
115                 缺点
116                     在多线程环境下,其运行效率比重量级锁还会慢
117                 性能依据
118                     对于绝大部分的锁,在整个生命周期内部都是不会存在竞争的
119             偏向锁
120                 为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
121                 主要尽可能避免不必要的CAS操作,如果竞争锁失败,则升级为轻量级锁
122     CAS
123         Compare And Swap, 整个JUC体系最核心、最基础理论,Java中通过锁和CAS实现原子操作
124         内存地址V,旧的预期值A,要更新的值B,当且仅当内存地址V中的值等于旧的预期值A时才会将内存值V得值修改为B,否则什么也不干
125         native中存在四个参数
126         JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现的。
127         缺陷
128             ABA问题
129                 检查不到值的变化,实际上却变化了
130                 解决方案
131                     变量前追加版本号版本号
132                     AtomicStampedReference
133                         这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子的方式将该引用和该标志的值设置为给定的更新值。
134             循环时间太长
135                 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
136                 解决方法
137                     JVM如果能支持处理器提供的pause指令,那么效率一定会提升
138                     pause作用
139                         1. 可以延迟流水线执行指令(depipeline),使CPU不会消耗过多的执行资源
140                         2. 避免在退出循环的时候因内存顺序冲突而引起的CPU流水线被清空,从而提高CPU的执行效率
141             只能保证一个共享变量原则操作
142                 对多个共享变量操作时,CAS无法保证操作的原子性,需要用锁
143                 解决方案
144                     AtomicReference类保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作
145         原子操作类Atomic
146             java.util.concurrent.atomic里的原子操作类提供了线程安全地更新一个变量的方式
147             4大类型13个原子操作类
148                 基本类型类
149                     AtomicBoolean
150                     AtomicInteger
151                     AtomicLong
152                 数组
153                     AtomicIntegerArray
154                     AtomicLongArray
155                     AtomicReferenceArray
156                 引用
157                     AtomicReference
158                     AtomicReferenceFieldUpdater
159                     AtomicMarkableReference
160                 属性
161                     AtomicIntegerFieldUpdater
162                     AtomicLongFieldUpdater
163                     AtomicStampedReference
164             核心底层
165                 CAS
166                     Unsafe只提供了3中CAS方法
167                         final native boolean compareAndSwapObject()
168                         final native boolean compareAndSwapInt()
169                         final native boolean compareAndSwapLong()
170         CAS V.S. 锁
171             JVM中除了偏向锁,其他锁(轻量级锁、互斥锁)的实现方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。
172     锁的内存语义
173         锁的功能
174             让临界区互斥执行
175             锁的内存语义
176         锁的释放与获取的内存语义
177             释放锁的线程向获取同一个锁的线程发送消息
178             线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
179             线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从内存中读取共享变量
180             volatile的写读与 锁的释放获取 有相同的内存效果:volatile写和锁的释放有相同的内存语义,volatile读和锁的获取有相同的内存语义。这也是增强volatile的原因,提供了一种比锁更轻量级的线程之间的通信机制。
181         锁的释放与获取的内存语义两种实现
182             1. 利用volatile 变量的 写读 锁具有的内存语义
183             2. 利用CAS(AQS里面的一个调用本地的方法,调用处理器cmpxchg指令添加lock前缀)所附带的volatile读和volatile写的内存语义
184         公平锁和非公平锁的内存语义
185             公平锁和非公平锁释放时,最后都要写一个volatile变量state
186             公平锁获取时,首先会去读volatile变量。
187             非公平锁获取时,首先会用CAS更新volatile变量,CAS同时具有volatile读和volatile写的内存语义
188         JUC 包的实现
189             通用化的实现模型
190                 1. 声明共享变量为volatile
191                 2. 使用CAS的原子条件更新来实现线程之间的同步
192                 3. 同时配合volatile的读写和CAS具有的volatile读写的内存语义来实现线程之间通信
193                 4. 基础类基于123:AQS、非阻塞结构、原子变量类
194                 5. 高层类基于4:Lock、同步器、阻塞队列、Executor、并发容器
195         锁的升级与对比
196             JAVA1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态
197             这几种状态会随着竞争情况的逐渐升级,锁可以升级但是不能降级,这种策略是为了提高获得锁和释放锁的效率
198             无锁
199             偏向锁
200                 背景
201                     大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
202                     偏向锁使用了一种等到竞争出现才释放锁的机制。当其他线程尝试竞争偏向锁时持有偏向所的线程才会释放锁。
203                 重要操作
204                     获得偏向锁
205                         1. 一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID
206                         2. 以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要看对象头Mark Word里面是否存储着指向当前线程的偏向锁,如果有,表示线程已经获得了锁,如果没有,下一步
207                         3. 判断Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有,使用CAS竞争锁; 如果有,则尝试使用CAS将对象头的偏向锁线程ID指向当前线程
208                     撤销偏向锁
209                         其他线程访问同步块,尝试获得偏向锁,失败,启动撤销偏向锁
210                         1. 首先暂停持有偏向锁的线程
211                         2. 解锁,将线程ID设为空
212                         3. 恢复线程
213                     关闭偏向锁
214                         java 7里默认是启用的
215                         关闭延迟
216                             XX:BiasedLockingStartupDelay=0
217                         关闭偏向锁,程序默认会进入轻量级锁
218                             XX:UseBiasedLocking=false
219                 应用
220                     优点
221                         加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差别
222                     缺点
223                         如果线程间存在锁竞争,会带来额外的锁撤销的消耗
224                     使用场景
225                         适用于只有一个线程访问同步块场景
226             轻量级锁
227                 注意,轻量级锁解锁失败,锁就会膨胀成为重量级锁,就不会恢复到轻量级锁状态,当线程处于这个状态,其他线程试图获取锁时,会被阻塞住,当持有所得线程释放锁之后会唤醒这些线程,被唤醒的线程会进行新一轮的夺锁之争。
228                 重要操作
229                     轻量级加锁
230                         1. 访问同步块,分配空间并复制Mark Word到栈 Displaced Mark Word
231                         2. CAS尝试将对象头中Mark Word替换为指向锁记录的指针,成功则获得锁,执行同步体;失败表示其他线程竞争锁,当前线程通过使用自旋来获取锁
232                     轻量级解锁
233                         1. 解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头
234                         2. 如果成功,表示没有竞争发生,如果失败,表示当前锁存在竞争(因为别的线程在争夺锁),锁就会膨胀成为重量级锁(别的线程阻塞),释放锁并唤醒等待的线程
235                 应用
236                     优点
237                         竞争的线程不会阻塞,提高了程序的响应速度
238                     缺点
239                         如果始终得不到锁竞争的线程,使用自旋会消耗CPU
240                     使用场景
241                         追求响应时间
242                         同步块执行速度非常快
243             重量级锁
244                 应用
245                     优点
246                         线程竞争不使用自旋,不会消耗CPU
247                     缺点
248                         线程阻塞,响应时间缓慢
249                     使用场景
250                         追求吞吐量
251                         同步块执行速度较长
View Code

 

7. 参考网址

  1. 参考来源:http://cmsblogs.com/wp-content/resources/img/sike-juc.png
  2. 《Java并发编程的艺术》_方腾飞PDF 提取码:o9vr
  3. http://ifeve.com/the-art-of-java-concurrency-program-1/
  4. Java并发学习系列-绪论
  5. Java并发编程实战
  6. 死磕 Java 并发精品合集
 
 

 

/**

线程试图通过类似于数绵羊的传统方法进入休眠状态。为了使这个示例能正确执行,asleep必须为volatile变量。否则,当asleep被另一个线程修改时,执行判断的线程却发现不了。

**/

volatile boolean asleeep;

...

while(!asleep)

countSomeSheep();

posted @ 2019-07-17 11:31  海米傻傻  阅读(986)  评论(0编辑  收藏  举报