文章目录

  • 引言
  • 锁的独占与共享
    • 内置锁和显式锁的排他性
    • AQS 的模板方法
    • 共享锁应用案例
  • 锁的公平与非公平
    • 插队的诱惑
    • 内置锁和显式锁的公平性
  • 启示录

引言

本文继续讲解 Java 并发编程实践的基础篇,今天来说说并发编程中锁的概念。不同领域,对锁的分类也不同,比如数据库的表锁、行锁等,它们因底层细节的差异,而有了各自的名字。扩展到整个 IT 技术领域,衍生出的那些名目繁多的锁,大抵也都是这样产生的。

Java 语言中,最顶层锁的实现方式,只有内置锁和显式锁两种。但是,这两种锁在实现过程中,遭遇到了各种处理方式的选择,不同的处理方式,也对应着一种锁,比如:

  1. 已经被某个线程持有的锁,是否允许其他线程线程同时持有呢?【独占/共享】
  2. 多个线程阻塞在同一个条件队列上时,先唤醒谁呢?已经有线程排队等待某个锁时,又有新的线程请求该锁,而恰好该锁被释放了,是否允许新线程插队获取锁呢?【公平/非公平】
  3. 已经持有锁的线程,还想继续请求同一把锁,是否允许呢?【可重入】
  4. 线程在请求锁而不得的等待期间,是否允许外部调用 inerterupt 中断该线程呢?【可中断锁】

对于开发人员而言,内置锁和显式锁的实现,是一个白盒,我们只需要知道哪种 API 可以触发某种锁的处理分支就可以了,没有必要去纠结它们之间的具体区别。况且,这些锁的概念,有些是分属不同维度的,貌似也没有可比性。

一起来跟它们过过招吧!

锁的独占与共享

锁的独占与共享,是排他性的两种表现。指已经被某个线程持有的锁,是否允许其他线程线程同时持有?内置锁和显式锁在解决这个问题时,具体是怎么做的呢?这就是独占锁和共享锁产生的背景了,它们的处理差异为:

  1. 独占锁,每次只能有一个线程能持有锁【霸道独享】
  2. 共享锁,则允许多个线程同时获取锁,并发访问共享资源【和谐共享】

内置锁和显式锁的排他性

先来看由 synchronized 代表的监视器层面的内置锁 ,它是以独占方式实现的,只允许一个线程持有某个锁对象,锁未释放,其他线程只能等待。

而以 Lock 为代表的显式锁,它提供了两种锁实现模式,独占和共享。比如,ReentrantLock 是独占锁,ReadWriteLock 的读锁是共享锁,写锁是独占锁。很显然,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 Java 的 ReadWriteLock,读-写锁,它允许一个资源同时被多个读线程访问,或只能被一个写线程访问,但两者不能同时进行,共享也仅限于读操作。如果是写线程获取了锁控制权,那么此时的锁就被降级成独占锁了,即由共享锁变成了独占锁。

事实上,在 “读多-写少” 的并发场景下,乐观锁它允许多个读线程同时访问资源,极大地提高了并发效率,但是在 “写多-读少” 的场景下,效果跟悲观锁就一样的。

AQS 的模板方法

AQS ,全称是 「 AbstractQueuedSynchronizer 」,它是显式锁的底层抽象类,定义了独占锁和共享锁必须实现的方法。而独占和共享,分别对应着 AQS 的内部类 Node 的两个常量 SHAREDEXCLUSIVE ,标识 AQS 队列中等待线程的锁获取模式。

独占锁的子类,必须实现 tryAcquiretryReleaseisHeldExclusively 等方法;共享锁的子类,必须实现 tryAcquireSharedtryReleaseShared 等方法,带有 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 将按线程发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁,如果可用则可直接获取锁,尝试失败才进行排队等待。

插队的诱惑

插队请求锁,带来的实际效益是什么呢?我们来看看排队唤醒的过程:

  1. 第一步,将当前请求锁的线程加入队尾
  2. 第二步,从队头移除一个等待最久的线程
  3. 第三步,把锁分配给线程

插队比按规矩排队更高效,因为时机刚好,只需要执行第三步,当然更简单啦。

内置锁和显式锁的公平性

内置锁和显式锁在公平性方面的表现是,内置锁是非公平锁。多个线程阻塞在同一个条件队列上时,随机唤醒;已经有线程排队等待某个锁时,又有新的线程请求该锁,而此时恰好该锁被释放了,则直接给它。在公平性和排他性方面,内置锁是非公平、排他的,这是底层决定的,没有办法干预。

相比之下,显式锁灵活多了,它允许开发者选择。比如,ReentrantLock 类维护了一个成员变量
private final Sync sync;,它代表了锁获取方式,这个抽象类有两种实现 FairSynNofairSync,从源码中看,类的层级关系为:

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 共享锁 默认非公平,可更改为公平 可重入 可中断

基础篇:独占锁、共享锁、公平锁、非公平锁,叫我如何分得清相关推荐

  1. 云阶月地,关锁千重(一.公平和非公平)

    看到文章的标题是不是很诧异,一个搞技术的为什么要搞这么文艺的话题呢?标题说关锁千重,是不是很形象,我们在开发中的锁不也是多种多样么? Lock 既然之前说了锁千重,那锁到底有多少种,他们的分类又是怎么 ...

  2. Java锁之公平和非公平锁

    Java锁之公平和非公平锁 目录 公平锁和非公平锁概念 公平锁和非公平锁区别 ReentrantLock和synchronized是公平锁还是非公平锁? 1. 公平锁和非公平锁概念 公平锁:是指多个线 ...

  3. java并发编程(三十五)——公平与非公平锁实战

    前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...

  4. 24-讲一讲公平锁和非公平锁,为什么要“非公平”?

    什么是公平和非公平 首先,我们来看下什么是公平锁和非公平锁,公平锁指的是按照线程请求的顺序,来分配锁:而非公平锁指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完 ...

  5. Java多线程学习十五:公平锁和非公平锁,为什么要“非公平”?

    什么是公平和非公平 公平锁 指的是按照线程请求的顺序,来分配锁: 非公平锁 指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完全的随机,不是说线程可以任意插队,而 ...

  6. 公平锁和非公平锁-ReentrantLock是如何实现公平、非公平的

    转载:https://www.jianshu.com/p/5104cd94dbe0 1.什么是公平锁与非公平锁 公平锁:公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁. 非公平锁:非 ...

  7. java -锁(公平、非公平锁、可重入锁【递归锁】、自旋锁)

    1.公平锁.非公平锁 2.可重入锁(递归锁) 3.自旋锁 AtomicReference atomicReference = new AtomicReference();//原子引用线程 下面代码5秒 ...

  8. java公平索非公平锁_Java 并发编程中使用 ReentrantLock 替代 synchronized

    Java 5 引入的 Concurrent 并发库软件包中,提供了 ReentrantLock 可重入同步锁,用来替代 synchronized 关键字原语,并可提供更好的性能,以及更强大的功能.使用 ...

  9. java公平索非公平锁_java中的非公平锁不怕有的线程一直得不到执行吗

    首先来看公平锁和非公平锁,我们默认使用的锁是非公平锁,只有当我们显示设置为公平锁的情况下,才会使用公平锁,下面我们简单看一下公平锁的源码,如果等待队列中没有节点在等待,则占有锁,如果已经存在等待节点, ...

  10. 乐观锁、悲观锁和公平、非公平

    今天心情:我是一个程序员,现在已经走向了逼不得已通过写文章赚取流量来谋生的道路.可是现在流量惨淡,可是我并不惊慌.奥里给. 详细内容链接地址:https://zhuanlan.zhihu.com/p/ ...

最新文章

  1. php 命令执行crud_如何使用原始JavaScript执行CRUD操作
  2. 用C++的random_shuffle()函数打乱int数组顺序
  3. 张亚勤:深度学习更近一步,如何突破香农、冯诺依曼和摩尔瓶颈?
  4. Linux下如何释放内存
  5. 怎么看rabbitmq的浏览器信息_没用过消息队列?一文带你体验RabbitMQ收发消息
  6. 创建IT运维管理门户
  7. 子查询中的空值导致的问题。
  8. arthas 查看哪个方法调用最耗时_Arthas实战
  9. 外卖ERP管理系统(一)
  10. 全国计算机等级考试题库二级C操作题100套(第03套)
  11. classcastexception异常_优雅的异常处理
  12. leetcode613. 直线上的最近距离(SQL)
  13. 信息学奥赛一本通 1130:找第一个只出现一次的字符 | OpenJudge NOI 1.7 02
  14. python 实例化过程_python实例化对象的具体方法
  15. 汉王考勤管理软件mysql数据库配置_求汉王考勤软件数据库表结构
  16. 最新CCC认证目录范围(2019)
  17. 老毛桃发帖子 去广告
  18. Python:混合动力汽车能量管理_动态规划简版(2/2)
  19. 集成学习——bagging原理及分析
  20. 数组中有两种数出现奇数次,其他数出现偶数次,打印奇数次的数

热门文章

  1. 交叉编译 ncurses-6.2
  2. 【转】Ninject的使用
  3. DynamipsGUI桥接Loopback网卡的方法
  4. Zipline学习笔记
  5. Unity Cinemachine插件全功能详解
  6. 天津春考计算机重点知识,春季高考试题-天津春季高考试题.doc
  7. 计算机启动后花屏然后无信号,电脑花屏,显示屏突然无信号(黑一下)或者一直黑...
  8. Vite HMR原理解析
  9. Javascript实现数组排列组合
  10. 教你如何破解xp开机密码