[多线程] - Java中的锁的分类

前言

相信很多同学在学习多线程的时候,被各种各样的锁弄得七荤八素,今天我就准备针对这些锁做一个梳理,喜欢的同学请记得一键三连!

一、乐观锁和悲观锁

首先我们先来说下乐观锁和悲观锁,乐观锁与悲观锁最初是数据库设计者提出的改变,后在JAVA的并发包中也提供了实现。乐观锁和悲观锁主要是设计者针对线程之间不同的竞争程度提出的两种优化策略。当前线程间对于共享变量如果不存在竞争或者竞争并不激烈的情况下,乐观锁有助于减少因为同步阻塞带来的时间开销。而如果线程间竞争激烈,使用悲观锁又可以减少因为线程自旋造成的时间开销。

具体来说:

  1. 乐观锁:指的是对于当前线程间的竞争状态调用者呈乐观态度,既线程间不存在竞争或竞争并不激烈,并不会频繁的更改共享变量,此时无需对共享变量加锁来确保线程安全。乐观锁常使用CAS算法,其依赖于CPU自带的硬件指令来达到比较交换的效果。
  2. 悲观锁:相当于乐观锁,悲观锁认为当前线程之间对于共享变量存在竞争且竞争激烈,需要通过同步阻塞来保证线程安全。悲观锁解决了CPU冗余自旋造成的性能开销,但是由于其加锁操作会造成线程堵塞,所以需要酌情使用。

针对CAS算法或乐观锁悲观锁不懂得同学可以查看CAS算法(乐观锁与悲观锁)来加深理解

总结一下:乐观锁与悲观锁主要用来描述在针对共享变量线程之间不同的竞争程度下设计者的不同决策。

二、共享锁与排它锁

首先我们要知道什么是排它锁:之前我们提到过的如synchronized,reentrantlock等基本都是排它锁,这些所在同一时刻只允许一个线程进行访问,未持有锁的线程只能够阻塞等待唤醒,而显然这样并不能满足我们日常对于很多业务场景性能优化的要求,因此读写锁也就应运而生。
读写锁可以在同一时刻允许多个线程访问同一个锁对象,与之对应的读写锁分为读锁和写锁两部分,其中读锁又称为共享锁,持有共享锁的线程可以互相在同一时刻访问同一个锁对象,不会发生堵塞。而另一部分的写锁又属于排它锁,持有写锁的对象访问锁对象时同一时间只允许一个线程进行访问,会造成堵塞。
读写锁的调用也非常简单,可以通过ReentrantReadWriteLock中的readlock()方法和writelock()方法的调用来实现。
关于共享锁和排它锁使用可以简单地举例为:

假定:线程A 持有共享锁,线程B持有排它锁,线程C持有共享锁

  1. 线程A访问同步代码块
  2. 线程B访问同步代码块 此时由于线程A持有共享锁进入同步代码,线程B需要等待线程A释放共享锁才能进入
  3. 线程A退出同步代码块,线程B执行同步代码
  4. 线程C范文同步代码块,此时由于线程B持有排它锁,线程C需要等待线程B释放排它锁才能进入
  5. 线程B退出同步代码块,线程C进入同步代码块
  6. 线程A访问同步代码快,此时由于线程A和线程C持有的都是共享锁,因此线程A此时允许与线程C一起执行同步代码。

简单的总结就是:共享锁和共享锁一起可以执行,共享锁与排它锁之间会互斥会产生堵塞。
还有就是需要注意:当排它锁(写锁)开始时,所有晚于写锁的共享锁都需要等待,这样做的目的是读操作能够读到正确的数据,不会出现幻读。

reentrantlock的使用可以参考lock对象的使用这篇文章

三、公平锁和非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取到锁的顺序是按照线程请求先后的顺序来决定,也就是越早到的线程最早获得到锁。具体来说就是当一个线程访问同步代码块时会判断此时是否有等待队列,如果有等待队列,则不会去尝试获取锁而加入队列尾等待。而与之相对的非公平锁是指线程不会按照访问的先后顺序获取锁,每当新的线程尝试获取锁的时候,并不会判断此时等待队列是否有线程等待,而是直接去尝试一次获取锁,如果成功则运行同步代码快,如果失败才会加入等待队列。
这里有个有意思的东西就是:synchronized的等待队列与reentrantlock的等待队列有所不同,synchronized本身是非公平锁,它会将最新访问的线程放入等待队列头部,而reentrantlock则与之相反,会将最新访问的线程放入等待队列尾部。

四、可重入锁和不可重入锁

当一个线程想要获得已被其他线程拥有的排它锁时会陷入阻塞,那么如果一个线程想要访问自己拥有的排它锁的时候会怎么样呢?如果此时线程不会阻塞,我们就将这种锁描述为可重入锁,可重入锁是指当一个线程拥有可重入锁后可以无限次的无需阻塞进入被该锁限制的同步区域。
这里留一个小问题:Java是否支持不可重入锁?为什么?
小伙伴可以在评论区留言发表自己的看法。

五、偏向锁,轻量级锁,重量级锁

偏向锁:Java的设计师发现很多情况下被锁修饰的代码都是被同一个线程多次访问,为了减小线程获得锁的代价,Java引入了偏向锁,当线程持有偏向锁访问同步代码块时不会触发同步阻塞的验证,极大地加快了同步代码的运行效率。
轻量级锁:JDK 1.6 之后为了减少获取锁和释放锁带来的性能开销,引入了轻量级锁。轻量级锁应用于线程之间存在竞争但竞争不激烈的情况,其主要通过线程自旋代替阻塞来提升程序的响应速度。
重量级锁:在Java中提供了一种原子性内置锁synchronized,他依赖于将内置锁抽象为moniter(监视器锁)实现的。在JDK1.6之前使用synchronized的代价是非常庞大的,它依赖于直接对操作系统中的互斥量进行操作,其实现锁功能的代价是挂起和唤醒线程都需要通过操作系统内核完成,十分消耗性能,因此这种锁也被称为重量级锁。
这里我们在比较下不同锁的优缺点:

偏向锁

  1. 优点: 加锁和释放锁不需要额外的操作,和执行非同步方法的差距仅在纳秒级
  2. 缺点: 偏向锁升级为轻量级锁时会引起stw造成卡顿
  3. 适用场景:只有一个线程访问同步块

轻量级锁:

  1. 优点: 不会造成线程堵塞,无需等待线程的休眠和唤醒,提高了程序的响应速度。
  2. 缺点: 过多的自旋会带来额外的cpu消耗
  3. 适用场景:追求响应时间,同步块执行速度非常快

重量级锁:

  1. 优点:线程竞争无需自旋,不会消耗CPU
  2. 缺点:线程阻塞,响应时间慢
  3. 适用场景:追求吞吐量,同步块执行较长

如果还有疑问的同学可以看下Java锁的对比这篇文章详细的学习下三种锁的特性和升级过程。

好了,今天的锁分类就到这里了,如果觉得有收获的同学可以选择一键三连。
祝一键三连的小伙伴们都可以在新的一年找到满意的工作,升职加薪走上人生巅峰!

猜你喜欢

转载自blog.csdn.net/xiaoai1994/article/details/111476653
今日推荐