Update 并发相关口水话.md

master
Omooo 4 years ago
parent f5f8ffd074
commit d15f483e77
  1. 54
      blogs/Java/口水话/并发相关口水话.md

@ -11,6 +11,10 @@
1. wait、notify/notifyAll 等待通知机制
2. Thread 的 sleep、join、yield 和线程中断
5. 线程死锁
6. volatile 的实现原理
7. synchronized 的实现原理
8. Lock 的实现原理
9. 原子类的实现原理
#### 进程和线程区别?
@ -42,4 +46,52 @@
死锁产生的条件必须具备以下四个条件:互斥条件、请求并持有、不可剥脱和环路等待。想要避免死锁,只需要破坏掉至少一个构成死锁的条件即可,但是目前只有请求并持有和环路等待条件是可以被破坏的。造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。
然后比较滑稽的是,现代操作处理死锁的办法是直接忽视。虽然看起来这似乎不是一个解决死锁问题的可行办法,但是确实大多数操作系统所采用的,代价是一个重要的考虑因素,对于许多系统,死锁很少发生(如一年一次),发生死锁就直接人工重启了。使用频繁的死锁预防、死锁避免和死锁检测与恢复相比,这种办法更便宜。
然后比较滑稽的是,现代操作处理死锁的办法是直接忽视。虽然看起来这似乎不是一个解决死锁问题的可行办法,但是确实大多数操作系统所采用的,代价是一个重要的考虑因素,对于许多系统,死锁很少发生(如一年一次),发生死锁就直接人工重启了。使用频繁的死锁预防、死锁避免和死锁检测与恢复相比,这种办法更便宜。
#### volatile 的实现原理
volatile 保证了共享变量的可见性和有序性。
可见性是指一个线程修改了共享变量,另一个线程可以立即感知到。有序性是指禁止编译器或处理器重排序。
volatile 是如何保证可见性的呢?
其实是 JVM 在 volatile 写的时候加一个 lock 前缀,它包含两层含义,第一个是将当前处理器缓存行的数据写回到系统内存,第二个就是这个写内存的操作会使其他 CPU 里缓存了该内存地址的数据无效。但是,就算回写到内存,如果其他处理器缓存的值还是旧的还是有问题的,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,即每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,如果内存地址被修改就会把当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作的时候,就会重新拉一份新的值。
volatile 是如何保证有序性的呢?
#### synchronized 的实现原理
先说一下 synchronized 的基本使用,对于普通方法,锁是当前实例对象,对于静态方法,锁是当前类的 Class 对象,对于同步代码块,锁是括号里配置的对象。
对于锁方法,就是在编译方法的时候 ACCESS_FLAGS 加一个 synchronized 标识位,Access_Flags 就是访问标识位,除此之外还有常见的 public、private、static 等等。
对于锁代码块,其实就在代码块的前后增加一对 monitorenter 和 monitorexit 指令。
在 Java 1.6 时,synchronized 做了大量优化,引入了轻量级锁和偏向锁。此时锁有四种状态,分别是无锁、偏向锁、轻量级锁和重量级锁。这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。
在讲这四种状态之前,首先要先讲一下对象头。
synchronized 用的锁的信息是存放在 Java 对象头的 Mard Word 标记字段中的,它里面保存了对象的 HashCode、分代年龄和锁标志位。锁标志位用两个 bit 表示,00 表示轻量级锁,10 表示重量级锁,01 表示偏向锁和无锁,它们两个再用一个 bit 表示是否是偏向锁。
下面就先讲一下偏向锁,为什么要有偏向锁呢?其实呢,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头里记录锁偏向的线程 ID,下次该线程再次进入只需要判断线程 ID 就可以了。
轻量级锁就是在获取锁的时候,如果获取不到就自旋一段时间再次获取,也就是自旋锁,如果在指定次数没有成功,就会膨胀为重量级锁,当前线程阻塞掉。默认次数好像是 15,当然,后面出了自适应自旋锁,会根据上次自旋的次数来设置。因为长时间的自旋会消耗 CPU,所以会有限制次数这一说。
#### Lock 的实现原理
#### 原子类的实现原理
原子类即是指 Java 中的 Atomic 类,比如 AtomicInteger、AtomicLong、AtomicStampedReference、AtomicReference 等。都是通过 CAS 来做的。
CAS 即比较并替换,它的通过硬件来保证操作的原子性。
在 Java 中,UnSafe 类提供了对 CAS 的简单封装,Atomic 类内部也都是使用 UnSafe 类来做的,UnSafe 类是可以直接操作内存的,一般在应用程序中是不能使用的,它是由启动类加载器加载的。
CAS 存在的问题也比较多,但是现在基本上都已经有解决方案。
首先是 ABA 问题,解决思路就是加一个版本号,可以使用 AtomicStampedReference 来解决。
其次是循环时间长开销大,这个问题的解决可以参考 Java8 新增的 LongAdder 类。在高并发场景下,AtomicLong 会导致大量线程自旋,严重损耗 CPU,这时候可以把 long 值分为一个 base 加上一个 Cell 数组,也就是把竞争分到多个 Cell 上,最后取值时就是 base 加上多个 Cell 的值。
最后是 CAS 的一个限制,就是只能保证一个共享变量的原子操作。解决办法就是可以把多个共享变量合成一个共享变量,比如 ThreadPoolExecutor 的 ctl 字段包含了线程池状态和 Worker 线程数量。或者可以使用 AtomicReferecne 类来保证引用对象之间的原子性,也就是把多个变量放在一个对象里进行 CAS 操作。
Loading…
Cancel
Save