基础篇:独占锁、共享锁、公平锁、非公平锁,叫我如何分得清
文章目录
- 引言
- 锁的独占与共享
- 内置锁和显式锁的排他性
- AQS 的模板方法
- 共享锁应用案例
- 锁的公平与非公平
- 插队的诱惑
- 内置锁和显式锁的公平性
- 启示录
引言
本文继续讲解 Java 并发编程实践的基础篇,今天来说说并发编程中锁的概念。不同领域,对锁的分类也不同,比如数据库的表锁、行锁等,它们因底层细节的差异,而有了各自的名字。扩展到整个 IT 技术领域,衍生出的那些名目繁多的锁,大抵也都是这样产生的。
Java 语言中,最顶层锁的实现方式,只有内置锁和显式锁两种。但是,这两种锁在实现过程中,遭遇到了各种处理方式的选择,不同的处理方式,也对应着一种锁,比如:
- 已经被某个线程持有的锁,是否允许其他线程线程同时持有呢?【独占/共享】
- 多个线程阻塞在同一个条件队列上时,先唤醒谁呢?已经有线程排队等待某个锁时,又有新的线程请求该锁,而恰好该锁被释放了,是否允许新线程插队获取锁呢?【公平/非公平】
- 已经持有锁的线程,还想继续请求同一把锁,是否允许呢?【可重入】
- 线程在请求锁而不得的等待期间,是否允许外部调用
inerterupt
中断该线程呢?【可中断锁】
对于开发人员而言,内置锁和显式锁的实现,是一个白盒,我们只需要知道哪种 API 可以触发某种锁的处理分支就可以了,没有必要去纠结它们之间的具体区别。况且,这些锁的概念,有些是分属不同维度的,貌似也没有可比性。
一起来跟它们过过招吧!
锁的独占与共享
锁的独占与共享,是排他性的两种表现。指已经被某个线程持有的锁,是否允许其他线程线程同时持有?内置锁和显式锁在解决这个问题时,具体是怎么做的呢?这就是独占锁和共享锁产生的背景了,它们的处理差异为:
- 独占锁,每次只能有一个线程能持有锁【霸道独享】
- 共享锁,则允许多个线程同时获取锁,并发访问共享资源【和谐共享】
内置锁和显式锁的排他性
先来看由 synchronized
代表的监视器层面的内置锁 ,它是以独占方式实现的,只允许一个线程持有某个锁对象,锁未释放,其他线程只能等待。
而以 Lock
为代表的显式锁,它提供了两种锁实现模式,独占和共享。比如,ReentrantLock
是独占锁,ReadWriteLock
的读锁是共享锁,写锁是独占锁。很显然,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 Java 的 ReadWriteLock
,读-写锁,它允许一个资源同时被多个读线程访问,或只能被一个写线程访问,但两者不能同时进行,共享也仅限于读操作。如果是写线程获取了锁控制权,那么此时的锁就被降级成独占锁了,即由共享锁变成了独占锁。
事实上,在 “读多-写少” 的并发场景下,乐观锁它允许多个读线程同时访问资源,极大地提高了并发效率,但是在 “写多-读少” 的场景下,效果跟悲观锁就一样的。
AQS 的模板方法
AQS ,全称是 「 AbstractQueuedSynchronizer 」,它是显式锁的底层抽象类,定义了独占锁和共享锁必须实现的方法。而独占和共享,分别对应着 AQS 的内部类 Node
的两个常量 SHARED
和 EXCLUSIVE
,标识 AQS 队列中等待线程的锁获取模式。
独占锁的子类,必须实现 tryAcquire
、tryRelease
、isHeldExclusively
等方法;共享锁的子类,必须实现 tryAcquireShared
和 tryReleaseShared
等方法,带有 Shared
后缀的方法是支持共享锁语义的。JUC
中,Semaphore
是一种共享锁,ReentrantLock
则是一种独占锁。
独占锁获取锁时,需要设置等待线程的节点模式为 Node.EXCLUSIVE
,源码如下:
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
而共享锁,节点模式则为 Node.SHARED
,是添加到等待队列的线程的锁模式:
private void doAcquireShared(int arg) {final Node node = addWaiter(Node.SHARED);boolean failed = true;.....}
共享锁应用案例
来看一个读共享的例子,笔者曾在问答频道回答过 这样一个问题 ,一个使用 ReadWriteLock
的例子中,写线程锁释放锁之后,读线程获取锁的机会还是很少,读线程的并发数并不高,这是为什么呢?
首先,使用读写锁提供一个具有 plus
写操作和 get
读操作的类 ReadWriteFac
:
class ReadWriteFac {ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private volatile int i = 0;Lock r = lock.readLock();Lock w = lock.writeLock();public void plus() {w.lock();System.out.println(Thread.currentThread().getName() + "---获取了写锁");try {i++;System.out.println(Thread.currentThread().getName() + "---将i修改为" + i);r.lock();} finally {w.unlock();//释放写锁 因为上面读锁未被释放 其他写线程无法进入但读线程可以继续System.out.println(Thread.currentThread().getName() + "---释放了写锁\r\n\r\n");try{TimeUnit.SECONDS.sleep(3);}catch(Exception e){}r.unlock();//释放读锁System.out.println(Thread.currentThread().getName() + "---释放了读锁");}}public int get() {r.lock();try {if(i!=10){System.out.println(Thread.currentThread().getName() + "获取到了" + i);}return i;}finally {r.unlock();}}
}
接着,定义一个测试类,创建 10 个写线程, 5 个读线程,读线程循环读取数据,直到 30 秒后程序结束:
public class ReadWriteTest {private static boolean isRun = true;public static void main(String[] args) {ReadWriteFac fac = new ReadWriteFac();Thread thread = new Thread();for (int i = 0; i < 5; i++) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {while (isRun) {fac.get();}}}, "读线程" + i);t1.setPriority(10);t1.start();}for (int i = 0; i < 10; i++) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {fac.plus();}}, "写线程" + i);t1.setPriority(1);t1.start();}try{TimeUnit.SECONDS.sleep(30);isRun = false;}catch(Exception e){}}
}
示例中,读线程使用 while(true)
循环读取最新数据;plus()
方法中,写锁释放之后,线程休眠了 3 秒。理论上,这 3 秒休眠期间,读线程应该有机会获取读锁、继续执行读操作的,但是运行后发现跟预期效果不一致,为什么呢?
笔者推测,根源是写线程过多,当写锁释放的时候,写锁又立即被其他写线程获取了,导致一直看到是写线程占据写锁,而读线程在这休眠的三秒内,获得读锁的机会很少。
读写锁适用读多写少的场景,而这个测试案例恰好相反,写多读少,导致写锁一释放,就被其他的写线程给抢占了,所以读线程依旧没有机会获取读锁。调小写线程个数,打印时间,就能看到写线程释放写锁休眠期间读线程获取读锁的过程了。
锁的公平与非公平
来看第二个问题, 多个线程阻塞在同一个条件队列上时,先唤醒谁呢?已经有线程排队等待某个锁时,又有新的线程请求该锁,而此时恰好该锁被释放了,是否允许新线程插队获取锁呢?
这两个问题都属于锁的公平与非公平概念,它是指线程请求获取锁的过程中,是否允许插队。公平锁实现的方式是,JVM 将按线程发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁,如果可用则可直接获取锁,尝试失败才进行排队等待。
插队的诱惑
插队请求锁,带来的实际效益是什么呢?我们来看看排队唤醒的过程:
- 第一步,将当前请求锁的线程加入队尾
- 第二步,从队头移除一个等待最久的线程
- 第三步,把锁分配给线程
插队比按规矩排队更高效,因为时机刚好,只需要执行第三步,当然更简单啦。
内置锁和显式锁的公平性
内置锁和显式锁在公平性方面的表现是,内置锁是非公平锁。多个线程阻塞在同一个条件队列上时,随机唤醒;已经有线程排队等待某个锁时,又有新的线程请求该锁,而此时恰好该锁被释放了,则直接给它。在公平性和排他性方面,内置锁是非公平、排他的,这是底层决定的,没有办法干预。
相比之下,显式锁灵活多了,它允许开发者选择。比如,ReentrantLock
类维护了一个成员变量
private final Sync sync;
,它代表了锁获取方式,这个抽象类有两种实现 FairSyn
和 NofairSync
,从源码中看,类的层级关系为:
ReentrantLock
默认是非公平的,它的有参构造函数可以传入一个标识改变锁的类型,源码相当简洁:
/*** Creates an instance of {@code ReentrantLock}.* This is equivalent to using {@code ReentrantLock(false)}.*/public ReentrantLock() {sync = new NonfairSync();}/*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
启示录
笔者最初看源码时并没有了解过这些锁的概念,所以看得很困惑。理解了用法之后,意识到,对于开发者来说,上层始终只有内置锁和显式锁这一个概念,其他都是为它的实现服务的。
弄清了内置锁和显式锁的各个行为表现,这些概念就不攻自破了:
锁实现 | 排他性 | 公平性 | 是否可重入 | 是否可中断 |
---|---|---|---|---|
内置锁 | 独占锁 | 非公平 | 可重入 | 不可中断 |
ReentrantLock | 独占锁 | 默认非公平,可更改为公平 | 可重入 | 可中断 |
ReadWriteLock | 共享锁 | 默认非公平,可更改为公平 | 可重入 | 可中断 |
基础篇:独占锁、共享锁、公平锁、非公平锁,叫我如何分得清相关推荐
- 云阶月地,关锁千重(一.公平和非公平)
看到文章的标题是不是很诧异,一个搞技术的为什么要搞这么文艺的话题呢?标题说关锁千重,是不是很形象,我们在开发中的锁不也是多种多样么? Lock 既然之前说了锁千重,那锁到底有多少种,他们的分类又是怎么 ...
- Java锁之公平和非公平锁
Java锁之公平和非公平锁 目录 公平锁和非公平锁概念 公平锁和非公平锁区别 ReentrantLock和synchronized是公平锁还是非公平锁? 1. 公平锁和非公平锁概念 公平锁:是指多个线 ...
- java并发编程(三十五)——公平与非公平锁实战
前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...
- 24-讲一讲公平锁和非公平锁,为什么要“非公平”?
什么是公平和非公平 首先,我们来看下什么是公平锁和非公平锁,公平锁指的是按照线程请求的顺序,来分配锁:而非公平锁指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完 ...
- Java多线程学习十五:公平锁和非公平锁,为什么要“非公平”?
什么是公平和非公平 公平锁 指的是按照线程请求的顺序,来分配锁: 非公平锁 指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完全的随机,不是说线程可以任意插队,而 ...
- 公平锁和非公平锁-ReentrantLock是如何实现公平、非公平的
转载:https://www.jianshu.com/p/5104cd94dbe0 1.什么是公平锁与非公平锁 公平锁:公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁. 非公平锁:非 ...
- java -锁(公平、非公平锁、可重入锁【递归锁】、自旋锁)
1.公平锁.非公平锁 2.可重入锁(递归锁) 3.自旋锁 AtomicReference atomicReference = new AtomicReference();//原子引用线程 下面代码5秒 ...
- java公平索非公平锁_Java 并发编程中使用 ReentrantLock 替代 synchronized
Java 5 引入的 Concurrent 并发库软件包中,提供了 ReentrantLock 可重入同步锁,用来替代 synchronized 关键字原语,并可提供更好的性能,以及更强大的功能.使用 ...
- java公平索非公平锁_java中的非公平锁不怕有的线程一直得不到执行吗
首先来看公平锁和非公平锁,我们默认使用的锁是非公平锁,只有当我们显示设置为公平锁的情况下,才会使用公平锁,下面我们简单看一下公平锁的源码,如果等待队列中没有节点在等待,则占有锁,如果已经存在等待节点, ...
- 乐观锁、悲观锁和公平、非公平
今天心情:我是一个程序员,现在已经走向了逼不得已通过写文章赚取流量来谋生的道路.可是现在流量惨淡,可是我并不惊慌.奥里给. 详细内容链接地址:https://zhuanlan.zhihu.com/p/ ...
最新文章
- php 命令执行crud_如何使用原始JavaScript执行CRUD操作
- 用C++的random_shuffle()函数打乱int数组顺序
- 张亚勤:深度学习更近一步,如何突破香农、冯诺依曼和摩尔瓶颈?
- Linux下如何释放内存
- 怎么看rabbitmq的浏览器信息_没用过消息队列?一文带你体验RabbitMQ收发消息
- 创建IT运维管理门户
- 子查询中的空值导致的问题。
- arthas 查看哪个方法调用最耗时_Arthas实战
- 外卖ERP管理系统(一)
- 全国计算机等级考试题库二级C操作题100套(第03套)
- classcastexception异常_优雅的异常处理
- leetcode613. 直线上的最近距离(SQL)
- 信息学奥赛一本通 1130:找第一个只出现一次的字符 | OpenJudge NOI 1.7 02
- python 实例化过程_python实例化对象的具体方法
- 汉王考勤管理软件mysql数据库配置_求汉王考勤软件数据库表结构
- 最新CCC认证目录范围(2019)
- 老毛桃发帖子 去广告
- Python:混合动力汽车能量管理_动态规划简版(2/2)
- 集成学习——bagging原理及分析
- 数组中有两种数出现奇数次,其他数出现偶数次,打印奇数次的数