Lock
源码版本
- JDK 8
前言
- Java 中提供了种类丰富的锁,每种锁因有不同的特性在不同的场景能够展现出较高的性能,本文在概念的基础上结合源码 + 使用场景进行举例,让读者对 Java 中的锁有更加深刻的认识,Java 中按照是否包含某一特性来定义锁,下面是本文中介绍的锁的分类图:
乐观锁 & 悲观锁
- 乐观锁和悲观锁是一种广义上的概念,体现了线程对互斥资源进行同步的两种不同的态度,在 Java 和数据中都有实际的运用。
概念
- 对一个互斥资源的同步操作,悲观锁认为自己访问时,一定有其它线程来修改,因此在访问互斥资源时悲观锁会先加锁;而乐观锁认为自己在访问时不会有其它线程来修改,访问时不加锁,而是在更新数据时去判断有无被其他线程修改,若没被修改则写入成功,若被其他线程修改则进行重试或报错。
适应场景
- 由上面我们可以看出,乐观锁适用于读操作多的场景,而悲观锁适用于写操作多的场景。
源码分析
- 我们常见的synchronized、ReentrantLock 都属于悲观锁,而AtomicInteger.incrementAndGet 则属于乐观锁。
阻塞 & 非阻塞
- 了解阻塞和非阻塞前,大家需要知道唤醒和阻塞一个Java线程需要操作系统进行用户态到内核态的切换,这种切换是十分耗时处理器时间的,如果同步代码块的内容过于简单,状态转换消耗的时间可能比用户代码执行时间还长,这是十分不划算的,因此我们引入了非阻塞的概念。
概念
- 从上面的介绍中我们其实已经可以了解到阻塞和非阻塞的概念。多线程访问互斥资源时,当互斥资源已被占用,阻塞线程,当互斥释放时,唤醒线程进行竞争称为阻塞式同步;而当互斥资源被占用时,不进行线程阻塞而通过自旋等待其它线程释放锁或直接返回错误的方式称为非阻塞式同步,自旋方式又可以分为普通自旋和自适应自旋。
使用场景
- 非阻塞自旋的方式本身是有缺点的,不能完全代替阻塞同步,非阻塞自旋虽然避免了线程切换的开销但是会占用处理器的时间,如果锁被占用的时间很短,那么自旋等待的效果很好,如果锁被占用时间很长那么只会白白浪费处理器时间。所以自旋一般会设置一定限制,比如Java中默认是10次(使用-XX:PreBlockSpin来修改)。
- 自适应自旋意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
- 因此,阻塞式同步适用于同步代码块执行时间比较长,线程获取锁时间间隔比较长的场景,而非阻塞式同步适用于同步代码块执行比较短,线程获取锁时间间隔比较短的场景。
源码分析
- ReentrantLock以及synchronized中的重量级锁都属于阻塞式同步,而 Java 中的原子操作类中的 CAS 失败后自旋则运用了非阻塞自旋的思想。
公平锁 & 非公平锁
概念
- 公平锁和非公平锁指的是获取线程获取锁时的顺序。公平锁指按照锁申请的顺序来获取锁,线程直接进入队列中,队列中的第一个线程才能获取锁。非公平锁指多个线程获取锁时,直接尝试获取锁,只有当线程未获取到锁时才放入队列中。
适应场景
- 公平锁的优点是不会造成饥饿,但整体性能会比非公平锁低,因为除等待队列中的第一个线程,其它线程都需要进行阻塞和唤醒操作。而非公平锁有几率直接获得锁,减少了线程阻塞和唤醒的次数,但可能会造成饥饿。因此在饥饿无影响或不会产生饥饿的场景下优先考虑非公平锁。
源码分析
- ReentrantLock 提供了公平锁和非公平锁两种实现,默认使用非公平锁。
非公平锁
- 我们可以注意到非公平锁实现中两次尝试使用compareAndSetState()来获取锁,其实这里就是类似自旋的作用,避免线程阻塞再唤醒的过程,从而提高性能。
公平锁
可重入锁 & 不可重入锁
概念
- 可重入锁又称递归锁,是指同一线程在外层获取锁后,进入内层方法再次获取同一锁时会自动获取锁。可重入锁的好处是可以一定程度避免死锁。
源码分析
- Java 中 ReentrantLock 和 synchronized 都是可重入锁,我们以 ReentrantLock 为例进行分析:
排它锁 & 共享锁
概念
- 排它锁和共享锁的主要区别在于互斥资源锁是否能被多个线程同时持有。同时只能被一个线程持有称为排它锁;当能够被多个线程同时持有称为共享锁。
作用
- 进一步细化加锁粒度,提高并发性能。比如我们常见读写锁,实现读读不互斥,高效并发读,而读写、写读、写写的过程互斥。
源码分析
- 我们以 ReentrantReadWriteLock 读写锁为例,ReentrantReadWriteLock 中有两把锁 ReadLock 和 WriteLock ,一个是读锁为共享锁,一个是写锁为排它锁:
- 当前线程已获取读锁无写锁,其它线程可以获取读锁;当前线程已获取写锁,仅当前线程可以获取读锁。
- ReentrantReadWriteLock 巧妙的将AQS中的state一分为二高16位为读计数,低16为为写计数,将两个原子性操作(读竞争和写竞争)合并为一个原子操作。
synchronized 中的无锁、偏向锁、轻量级锁、重量级锁
- synchronized 中的无锁、偏向锁、轻量级锁、重量级锁是 synchronized 特有的概念,参考 volatile & synchronized 章节。
作者:Lorin洛林链接:https://juejin.cn/post/7276257296497147956来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
SyncAutoCloseableLoading...