曾几何时,面试官问:java都有哪些锁?小白,一脸无辜:用过的有synchronized,其他不清楚。面试官:回去等通知!
今天我们庖丁解牛说说,各种锁有什么区别、什么场景可以用,通俗直白的分析,让小白再也不怕面试官八股文拷打。
之前的文章说过《JAVA并发编程synchronized全能王的原理》、以及《JAVA并发编程JUC包之CAS原理》,synchronized、CAS都是常见的锁。那么锁的类型是如何划分的呢?
一、按特性划分
1.1 悲观锁和乐观锁
按线程访问共享资源时,要不要锁住同步资源来定义。比如synchronized就是悲观锁,数据库的行锁、表锁都是悲观锁。
CAS就是乐观锁,数据库版本号机制也是乐观锁。
悲观锁的应用场景:大量更新修改操作,使用悲观锁,可以保证数据强一致性。
乐观锁的应用场景:读多写少的场景,提高吞吐量。
1.2 可重入锁和不可重入锁
一个线程,是否可以重复获取同一把锁。比如synchronized、ReentrantLock是可重入锁,可以重复加锁。可重入锁在加锁N次后,解锁就需要进行N次解锁。效率较低,但是好处是可以避免死锁,因为在方法内部递归,如果使用普通锁,可能会出现死锁。而可重入锁支持线程重复进入同一个代码块。
1.3 公平锁和非公平锁
多线程竞争同一个锁时,是否需要排队,以及是否可以插队。
公平锁,就是多个线程按顺序申请锁。比如new ReentrantLock(true),就是公平锁。
非公平锁,就是多个线程获取锁不是按申请顺序获得,可以插队。比如new ReentrantLock(false), synchronized。
非公平锁的缺点,就是可能导致线程饥饿现象,某个线程或多个线程由于优先级较低,长期被其他优先级较高的线程插队抢夺锁资源,导致无法获得CPU执行。
1.4 独享锁和共享锁
独享锁、共享锁,也叫排它锁和非排它锁。
依据是:多个线程能否同时共享同一个锁。
独享锁=排它锁,比如synchronized、ReentrantLock,ReentrantReadWriteLock的write写锁,都是独享锁。
共享锁=非排它锁,比如ReentrantReadWriteLock的readLock读锁就是共享锁。
也有人说,互斥锁,其实也是独享锁。有人说读写锁,对应的就是ReentrantReadWriteLock。
二、按上锁方式划分
2.1 隐式锁和显式锁
按是否显式的执行加锁和解锁过程。比如synchronized,我们使用它后,不需要显式的解锁。而JUC包的ReentrantLock就需要显式的lock(),unlock().
三、其他类型锁
自旋锁,锁获取失败后, 线程不会阻塞,而是循环尝试获取锁直至成功。比如CAS、轻量级锁都是自旋锁。
分段锁,将锁的粒度划分更细,当一个线程对一段数据进行加锁访问时,其他段的数据可以被其他线程同时访问。比如concurrentHashMap,就是使用锁分段技术。
四、synchronized的锁状态
无锁、偏向锁、轻量级锁、重量级锁四种状态。其中JDK1.6新增了偏向锁和轻量级锁。这四种状态是随着多线程竞争,逐级升级的状态,不可逆不可降级。是JVM特定给synchronized提升效率而做的优化。
偏向锁:就是只有一个线程访问共享资源时,不会加锁,只是记录当前持有锁资源线程的锁类型,然后该线程每次执行这部分共享资源,都只是判断是否持有锁,以及当前锁类型。
轻量级锁:在偏向锁的基础上,如果当前有两个线程在交替竞争锁,这时候将锁状态膨胀为【轻量级锁】。同时竞争失败的线程,不阻塞,用CAS自旋循环等待获取锁。
重量级锁:轻量级锁,自旋一定次数后,膨胀为重量级锁,竞争失败的线程直接进入阻塞状态,等持有锁的线程释放锁后再唤醒。