并发编程第三周
1 锁的分类
1.1 可重入锁,不可重入锁
Java提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是可重入锁
可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。
不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到,因为A锁被当前线程占用着,需要等待自己释放锁再获取是锁。
1.2 乐观锁,悲观锁
Java提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是悲观锁
Java中提供的CAS操作,就是乐观锁的一种实现
悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCK,WAITING),线程挂起会涉及到用户态和内核态的切换,消耗资源
- 用户态:jvm可以自行执行的指令,不需要借助操作系统执行
- 内核态:jvm不可以自行执行的指令,需要借助操作系统执行
**乐观锁:**获取不到锁可以再让CPU调度,重新尝试获取锁资源
Atomic原子类就是基于CAS乐观锁实现的
1.3 公平锁,非公平锁
Java提供的synchronized只能时非公平锁
ReentrantLock,ReentrantReadWriteLock可以实现公平和非公平
**公平锁:**线程A获取到了锁资源,线程B没有拿到,线程B排队,线程C来了,锁被A持有,同时B在排队。直接排到B后面,等待B拿到锁资源或者B取消后,才可以尝试区竞争锁资源。
非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B排队,线程C来了,直接先去尝试竞争一波
- 拿到锁:插队成功
- 没有拿到锁:依然排队到B后面,等待B拿到锁资源或者B取消后,才可以尝试区竞争锁资源。
1.4 互斥锁,共享锁
Java提供的synchronized,ReentrantLock 互斥锁
ReentrantReadWriteLock有互斥锁也有共享锁
互斥锁:同一时间点,只会有一个线程持有当前互斥锁
共享锁:同一时间点,当前共享锁可以被多个线程同时持有
2 深入synchronized
2.1 类锁,对象锁
synchronized的使用一般就是同步方法块和同步代码块
synchronizated的锁时基于对象实现的
使用同步方法:
- static:此时使用的时当前类.class作为锁(类锁)
- 非static:此时使用的时当前对象做为锁(对象锁)
2.2 synchronized的优化
锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,即便写了synchronized也不会触发
public synchronized void method(){
// 没有操作临界资源
// 此时这个方法的synchronized你可以认为木有~~
}
锁膨胀:如果在一个循环中,频繁的获取和释放锁资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免,频繁的获取和释放锁资源带来不必要的消耗。
锁升级:ReentranLock的实现,是先基于乐观锁CAS尝试获取资源,如果拿不到锁资源,才会挂起线程。synchronizated在jdk1.6之前,完全就是获取不到锁,立即挂起当前线程,所以性能差。
synchronizated在jdk1.6之后做了锁升级的优化
-
无锁,匿名偏向:当前对象没有作为锁的存在
-
偏向锁:如果当前锁资源只有一个线程频繁的获取和释放,那么这个线程过来,只需要判断当前指向的线程是否位当前线程。
-
如果是,直接拿走锁资源
-
如果不是,基于CAS的方式尝试将偏向锁指向当前线程。如果获取不到,锁升级,升级位轻量级锁(偏向锁状态出现了锁竞争的情况)
-
-
轻量级锁:会采用自旋锁的方式取频繁的以CAS的形式获取锁资源。(采用的是自适应自旋锁)
如果成功获取到,拿着锁资源走
如果自旋一定次数,没有拿到锁资源,锁升级
-
重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。
2.3 synchronized实现原理
synchronized是基于对象实现的。
Java对象在堆内存中的存储
展开MarkWord
MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、
偏向锁延迟:偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点,才可以做偏向锁撤销,在明知道有并发情况下,就可以选择不开启偏向锁,或者设置偏向锁延迟开启。因为在jvm启动时,需要加载的.class文件到内存中这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,jvm启动初期,会有一个延迟4s开启偏向锁的操作。如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向
整个锁升级状态的转变: