本章主要就根据锁的分类,大致的了解一些锁的概念
锁的7大分类
锁的分类是根据锁的特性来分类的。一把锁可能有多种特性,比如ReentrantLock既可以是可中断锁,又是可重入锁 7大类分别是:
-
偏向锁/轻量级锁/重量级锁; -
可重入锁/非可重入锁; -
共享锁/独占锁; -
公平锁/非公平锁; -
悲观锁/乐观锁; -
自旋锁/非自旋锁; -
可中断锁/不可中断锁。
偏向锁/轻量级锁/重量级锁
偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。
偏向锁
如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
偏向锁的升级:线程访问代码获取锁对象后,会在对象的对象头记录下锁的threadId,如果该线程再次获取锁的时候,需要比较当前线程的threadId与对象头的threadId是否一致。如果一致,不需要CAS来加锁,解锁。如果不一致其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
偏向锁的取消:偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
轻量级锁
JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
轻量级锁升级: 线程1获取轻量锁时会把锁对象的对象头MarkWord复制一份到线程1的栈帧创建的用于存储锁记录的空间(DisplaceMarkWord),然后用CAS把对象头中的内容替换成线程1存储的锁记录的地址。 如果在线程1复制对象头的同时,线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败。那么线程2尝试使用自旋来等待线程1释放锁。 自旋的时间太长也不行,自旋是要消耗CPU的,如果自旋的次数到了线程1还没有释放锁,线程3也来竞争锁对象。此时轻量级锁升级为重量级锁。
重量级锁
重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
可重入锁/非可重入锁
可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。这样会导致死锁。
对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。synchronized 也是重入锁。
共享锁/独占锁
共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
写锁(独占锁):是指该锁一次只能被一个线程锁持有。对ReentrantLock和Sychronized而言都是独占锁。
读锁(共享锁):是指该锁可被多个线程持有。对ReentrantReadWriteLock而言,其读锁是共享锁,其写锁是独占锁。读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。
公平锁/非公平锁
公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。
如果你用默认的构造函数来创建ReentrantLock对象,默认的锁策略就是非公平的。Sychronized也是非公平锁
在构造ReentrantLock对象的时候传入一个true就是公平锁: ReentrantLock lock = new ReentrantLock(true)
悲观锁/乐观锁
悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
java中的读写锁,ReentrantLock,synchronize都是悲观锁
CAS是乐观锁
自旋锁/非自旋锁
自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
Java之所以没有自旋锁的API是因为自旋锁并不是一种锁,而是一种锁优化技术。
Java虚拟机默认情况下是开启这种锁优化技术的,可以使用XX:+UseHeavyMonitors来关闭这种锁优化技术,直接使用重量级锁,也就是所说的内核锁。
可中断锁/不可中断锁
在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。
请关注我的公众号,一起讨论后端技术
本文使用 mdnice 排版