一文了解乐观锁,悲观锁,可重入不可重入,独享锁共享锁,偏向,轻量重量锁互斥锁读写锁,自旋锁及各自的优缺点和一些实现形式

前言

锁是高并发中一个非常重要的组成部分,在涉及到高并发的场景中,锁的使用可谓是司空见惯,那么接下来我们就来一起探讨一下java中锁的一些种类和基本使用

锁的分类

java中的锁可以分为一下几类:

  • 乐观锁和悲观锁

  • 公平锁和非公平锁

  • 可重入锁和不可重入锁

  • 独享锁和共享锁

  • 无锁/偏向锁/轻量级锁/重量级锁

  • 自旋锁

    这些分类锁看起来很多,其实个别锁同时有多种叫法,因为主观上的差异,有了不同的名称,例如synchronized,既是悲观锁,也是重量级锁/互斥锁等等,所以读者不要因为分类多,繁杂而苦恼

1. 乐观锁/悲观锁

  • 悲观锁: 多线程下访问操作数据时,总认为数据一定会被其他线程访问操作,因此会提前上锁,可以将它理解为一种饿汉式的加锁方式,悲观锁具有强烈的排他性和独占性,在抢到锁以后,同时访问的其他线程都会被拒之门外,大大影响了线程的执行效率,不过有利也有弊,使用悲观锁,大程度上保证了数据的安全性
  • 乐观锁:与上相反,总认为在访问操作的同时,数据没人会进行争夺资源,因此不会提前上锁,而是在访问数据时判断当前是否有其他线程修改数据,以此来做后续的处理(例如做循环来不断尝试,或者抛出异常,再或者直接跳过等等),这个用懒汉式可能不太恰当,但是稍微有一些共同之处,可以辅助理解
1.1 使用场景

其实对于乐观与悲观锁而言,是从线程的角度出发,根据不同的角度,来划分出乐观与悲观的概念,乐观锁适用于读操作比较多而写操作比较少的地方,悲观锁适用于写操作比较多和读操作比较少的地方

悲观锁最常见的就是
synchronized关键字:比较重的一个锁,创建和销毁都要消耗一定的资源,可以有效的保证数据的安全性,被该关键字修饰的代码块,加锁和解锁都是自动进行的,易于操作,但是不够灵活,响应不可中断,一旦线程获取不到锁资源,就会一直处于等待状态,过程不会终止
ReentrantLock:一个比synchronized灵活的锁,也属于悲观锁,加锁和解锁都需要手动完成,不易操作,但是使用灵活,同时可以响应中断,也可以手动设置等待时长,操作非常灵活
乐观锁常见的是CAS(CompareAndSwap):通过循环不断地判断资源的访问情况,CAS操作主要包含三个操作数-----主存值,old原值,new新值,而其主要的工作就是从主存获取值副本old原值,执行计算后获得new新值,然后使用old原值与主存值比较,一样则将主存值修改为new新值,否则不做操作,java中常使用AtomicInteger和AtomicReference<>(A)(解决aba问题衍生出的一个类,通过使用版本号来标记更改操作),通常在使用过程中会使用一个while循环来不断尝试获取资源

1.2 优缺点

悲观锁:优点:数据安全,保证数据原子性,缺点总是考虑最坏的情况,每次读取数据的时候都会上锁,这就导致其他线程如果想要读取数据就需要等待前者将锁释放,效率明显会下降,jvm中的线程属于内核级线程(KLT),所以在大量的线程获取锁资源时,多次的休眠唤醒,会消耗很大的内核系统资源,降低效率
乐观锁:优点:读操作下,不需要频繁的加锁解锁来消耗系统内核资源,比较轻量,缺点:自旋开销大,在有其他线程访问数据时,如果选择直接跳过等待并执行后面逻辑,则会丢弃这一段代码逻辑,而如果选择通过循环来不断判断锁的情况,那么资源长时间被一个锁锁定时,其他线程就会各自不断地循环判断,这将会给cup带来非常大的开销,所以乐观锁适合于一些读多写少的操作

2. 公平锁/非公平锁

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁的,在多个线程访问这个锁时,后发送请求的线程将被放到队列中,而队列的特点就是先进先出,简单来说,就是所有线程排队获取锁,等待时间长的先获得锁,后来的后拿到,对每个锁而言都是很公平的
  • 非公平锁:是指当有多个线程同时访问锁时,会首先尝试获取锁,获取不到才会放到队列队尾等待,如果在锁释放的同时正好有一个线程请求获取锁,那么此线程无需等待,直接获取锁,所以非公平锁会出现后申请锁先获取锁的情况

2.1 两者应用

   //true表示ReentrantLock设置为公平锁
   private  ReentrantLock lock = new ReentrantLock(true);   

这里这是一个例子,在传入true后,在多个线程同时请求访问锁资源时,就会按照先后顺序依次来获取,设置false自然为非公平锁

2.2 优缺点

公平锁

优点:保证每个等待的线程都能获得锁,
缺点是整体的吞吐效率下降,队列中第一个线程获取锁,那么之后所有线程都会阻塞等待,对于CPU要每次要唤醒这些等待的锁需要花费一些开销

非公平锁

优点:先尝试获取锁,再排队,会有更多的机会去争抢锁资源,减少CPU唤醒线程的开销,相较于公平锁,吞吐效率高,因为可能线程不阻塞直接抢到锁资源
缺点:因为不会排队,所以队列中等待的线程可能会饿死,或者等很久时间获得锁

3. 可重入锁/不可重入锁

  • 可重入锁:可重入锁又名递归锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。通俗来讲对于已经获取锁的当前线程,在调用当前线程的当前对象其他上锁的方法时,也会自动获取锁,而其他线程不可以
  • 不可重入锁:反之当一个线程获取对象后不可以获取本对象的其他锁方法

3.1 表现形式

synchronized和ReentrantLock都是可重入锁,synchronized默认就是一个可重入锁,在被其修饰的代码块或者方法中,是可以多次调用本线程本对象的方法的,而ReentrantLock根据名字也可以看出是一个可重入锁
在可重入锁中,当同一个线程多次访问该锁时,内部会有有count值来对同一个对象进行记录(每次自增1),而当释放锁资源时,count–,以为篇幅原因,就不上代码了,有兴趣读者可以做深入的了解
在JDK中没有提供不可重入锁,不可重入锁判断依据是一个线程对象是否为null,当有线程获取锁资源时,线程对象不为null,调用wait()方法阻塞,等待释放锁以后的唤醒操作,当释放锁后,该线程对象值重新为null,同时唤醒其他睡着的线程

3.2 优缺点

可重入锁

很多文章说可重入锁可以避免死锁,更加准确来说,是能在很大程度上避免死锁,可重入锁是同一对象可以递归地获取本对象其他的锁资源,但如果不是同一对象,还是可能会发生死锁,只是这种可能性很小

不可重入锁

容易造成死锁,效率比较低,同一对象如果调用本对象其他方法,会发生阻塞,而内部无法释放锁资源,外面的线程又获取不到锁而导致锁资源

4. 独享锁与共享锁

独享锁:独享锁也叫排他锁,是指该锁一次只能被一个线程锁持有,其他线程则只能等待该线程将所释放,见名知意,在获取该锁后,是归该线程独有的,其他线程无法获取
共享锁:是指该线程可以同时被多个线程共同访问,获得共享锁后,只能读,不能写

4.1 具体应用

synchronized,ReetrantLock,WriteLock等等,一些互斥锁,凡是不能同时被多个线程访问的都是独享锁.
有人可能会认为自旋锁CAS也能归属到共享锁类中,这个其实是不严谨的,CAS看似可以多个线程同时争抢资源,但是,当要执行compareAndSet()方法赋值时,该方法底层也是有锁的,具有原子性质的,在某一时刻某个线程原值正好和主存值相等要做修改时,其他线程是无法同时对其修改,所以CAS不能算作共享锁,如果单单只获取副本而不修改主存值的话,可以叫做这个行为是共享
那么独享锁和共享锁,最形象的就是ReentrantReadWriteLock读写锁

4.1.1 ReentrantReadWriteLock的使用

可以看到该对象内部有两个子类WriteLock和ReadLock,分别代表写锁和读锁,通过ReentrantReadWriteLock.writeLock()和reaLock()方法获取子类对象,在同一时刻,可以多个线程同时获取读锁

  for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lock.readLock().lock();
                System.out.println("此为读锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.readLock().unlock();
            }).start();

运行后会同时将日志打印出来,说明在不同线程中是可以同时获取多个读锁的,这也就是共享锁的具体表现
另外就是writeLock()写锁,将上面代码的lock.readLock()换为writeLock()后发现执行结果每0.5s打印一条,说明写锁是非共享的独有锁,一时间只能有一个线程获取锁.

在很多场景中需要读写互换,那么如果想要在获取读锁的过程中修改数据,或者在获取写锁后想要读取数据是否可以呢?
这里就涉及到了锁升级降级

  • 锁降级:写线程获取写锁后,是可以直接获取读锁的,不会阻塞,但是要注意,在获取到读取锁后,写锁是没有被释放的,即使获取读锁,也不能直接成为共享锁,其他线程仍然无法访问读锁,一定要在降级后,将写锁释放掉,才能起到效果
  • 锁升级:在获取读锁后在去拿写锁,这个是不支持的,可以试想一下,当所有读线程都使用这个读锁的时候,其中一个线程突然改掉数据,那么后果就是数据不同步,因此,释放所有的读锁,才可以获得读锁
4.1.2 ReentrantReadWriteLock的读写锁机制
  • 读-读不互斥:线程可以同时获取读锁(共享锁)
  • 读-写互斥: 获取读锁的同时不能使用写锁(即不能锁升级)
  • 写-写互斥: 同一时刻只能有一个线程获取写锁(独享锁)

好的,为了让读者能够了解共享与独享的概念,这里拓展了以上读写锁的知识

4.2 优势

这里我认为用优缺点不太合适,因为他们使用于不同的场合,本就是为了适应一些场合而产生的,所以应该讲一下各自的优势
共享锁

  • 读取效率高
  • 维护数据安全性
  • 保证数据的同步

独享锁
优点

  • 实现原子性
  • 保证数据同步性

缺点:执行效率低,比较耗费资源

5. 偏向锁,轻量级锁,重量级锁

在JDK1.6之后才出现这个概念的,早之前使用synchronized在本文没有细讲,可以看java线程锁synchronized同步方式,这篇文章,synchronized是一个重量级的锁,获取到资源之后,其他线程根本无法访问,效率低下,所以随后引出了偏向锁轻量级锁和重量级锁的概念,来减缓对系统资源的消耗,提升运行效率

偏向锁:只有一个线程来争取锁资源,在没有锁竞争的前提下,该锁会在此对象的monitor中保存这个线程id,当线程下次获取锁时,自动让其获取锁资源,减轻上锁的资源消耗
轻量级锁:在当前为偏向锁的状态下(使用锁的线程只有一个),有另外一个线程前来获取锁资源,偏向锁升级为轻量级锁,其他线程会不断尝试获取锁资源,此为轻量级锁,最有代表为使用CAS(CompareAndSwap)对锁资源的获取
重量级锁:当有大量线程来争夺锁资源时,轻量级锁如果都通过不断自旋来争夺,会有很高的CPU使用成本,此时,轻量级锁上升为重量级锁,常见的重量级锁synchronized和ReetrantLock

5.1 应用场景

偏向锁:没有锁资源的竞争(无竞争下),自始至终仅有这一个线程在使用锁资源,使用轻量,偏向锁可以很好的提高性能
轻量级锁:不止一个线程要获取锁资源(轻度竞争下),但线程数量不会很大,此时偏向锁已经无法满足需求,则各个线程开始不断询问,然后争抢资源,提高效率
重量级锁:大量线程访问资源时(重度竞争下),如果都在不断循环争抢资源,会给CPU造成很大的压力,而使用重量级锁一定程度上可以使线程进入休眠状态,即使休眠和唤醒会消耗系统资源,相较于大量亢奋线程争夺资源,后者占据更大的性能消耗量

5.2 优势

偏向锁:在单个线程访问资源时,只需要初始化一次,下次再请求锁资源时,只需要判断线程id便可以轻松获取资源,对性能有很大的提升,避免多余的操作
轻量级锁:当有多个线程但量不多的情况下,无需阻塞,多线程可以同时读取资源,提高多线程下的运行效率
重量级锁:加上synchronized锁是为了保证服务器选择的顺序性,使得并发操作的同一时刻有且仅有一个线程能够读取/写入数据表记录,防止多线程并发造成的脏数据。

我们可以发现jdk1.6以后新加入的这种获取锁的机制,是逐级的,根据不同的情况来对锁的分配进行了分级,不同级别的数量,对应了不同的操作,高效地减缓了系统的负担

6. 自旋锁

自旋锁的概念其实和轻量级锁是一样的,线程状态及上下文切换消耗,当访问资源耗时短,频繁地切换上下文必将导致系统内核负担过重,需要自我循环来保持线程避开阻塞,处于一种亢奋的状态不断地争夺资源,这里就不做过多的解释

7. 总结:

本文讲解了锁的分类,对于锁的叫法是有很多的,没有明确的定义,但总体上的锁本文中都是涉及到的,还讲解了各类锁的定义,使用场景和优缺点,帮助读者理解这些锁的含义,其中对CAS作了原理上的讲解,还讲述了读写锁的用法等等,对于锁分类概念含糊的小伙伴,会有很多的帮助

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

问心彡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值