谈谈个人对乐观锁、悲观锁的理解

前言

线程安全这个话题在面试中常被问到,说到线程安全,那么就绕不开锁,笔者看了敖丙的关于乐观锁和悲观锁的文章,觉得很受用,在这里将一些个人理解写成博客,如有什么错误还请各位看官指正。


正文

1、乐观锁

乐观锁是什么呢?顾名思义,就是线程在进行数据读写的过程中,总是乐观地认为数据不会被修改,所以被称为乐观锁。谈及乐观锁的时候,我们通常以为例,CAS(Compare and Swap)是乐观锁的一种实现方式,它是怎么实现线程安全的呢?首先,线程在读取数据时,不会加锁,在准备写回数据时,先判断原数据是否被修改了,如果没有被修改,就将数据写回,如果被修改了,就重新读入,此时不会将数据写入。

1.1 CAS带来的问题

  1. 自旋给CPU带来巨大压力:首先介绍一下自旋的概念,刚刚提到,当CAS准备写入时,会判断原数据是否被修改,如果被修改就重新读入数据,并重新执行一次CAS操作,这个过程称为自旋。如果CAS操作一直失败,那么就会导致一直自旋,这会给CPU带来巨大压力。

  2. ABA问题:简单来说,就是线程1读取到的数据为A,由于没有加锁,其他线程也可以对这个变量进行读写,假设这个时候,有一个线程2对数据进行读取,并将数据修改为B,然后这时候又来了一个线程3,将数据修改回A,此时线程1再写入过程中进行判断,发现数据还是A,并没有改变,于是执行写入操作。但是事实上,数据是变过的。我们可以用图来进行直观展示。
    在这里插入图片描述
  3. 只能保证一个共享变量的原子操作:CAS只能保证单个变量的原子操作,不能保证多个变量的原子操作,比如我们要保证数据a,b同时更新成功或者失败,而使用CAS可能会出现只有一个变量更新成功的情况。JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作(老实说,AtomicReference不太了解)。

1.2 CAS如何避免ABA的问题

  1. 加入标志位:比如加一个自增的字段,数据被操作一次就加一,或者加时间戳,比较时间戳的值。

2、悲观锁

悲观锁是什么呢?顾名思义,就是线程在读取数据的过程中,悲观地认为其他线程一定会修改数据,因此在一个线程对数据进行读写的的时候,不允许其他线程对数据进行读写,直到该线程释放当前占有的锁。悲观锁有两种,分别是Synchronized和ReentrantLock。我们一个一个来介绍。

2.1 Synchronized

Synchronized是最常用的线程同步手段之一,90%的线程同步问题都可以使用Synchronized来解决,那么它是如何保证同一时间段内只有一个线程能进入临界区呢?我们通过Synchronized分别修饰代码块和方法进行介绍。首先介绍一下什么是Monitor对象。


  • Monitor对象:在JVM中,对象分为3个部分:Header、Instance Data和Padding。

    • Header: Header包含2部分数据,Mark word(标记字段)、Klass Point(类型指针)
      • Mark Word:默认保存了对象的HashCode、分代年龄和锁标志位信息,可以看到,Mark Word的值会根据锁标志位的变化而变化。
      • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。
        在这里插入图片描述

    当Monitor被某个线程持有后,Owner就会指向这个持有它的线程,_EntryList 用来保存已经获取到锁的线程,_WaitSet用来保存等待获取锁的线程。

  • 修饰方法:使用Synchronized修饰方法时,在方法的字节码上会加上一个标志位ACC_SYNCHRONIZED,当其他线程进入这个方法时,会查看方法是否有这个标志位,如果有,就说明这个锁已经被其他线程占用了,当前线程就不能执行这个方法。

  • 修饰代码块:Synchronized修饰代码块时,是通过monitorenter和monitorexit来实现的,,每个对象都对应着该monitor,当一个monitor被拥有之后就被锁住,其他线程就不能运行到monitorentr指令时,会由于无法获取monitor而陷入阻塞,monitor内部维护这一个计数器,这个计数器记录了当前monitor被拥有的次数,当前拥有它的线程可以重复拥有它,当计数器为0时,表示可以释放当前锁了,于是就执行monitorexit指令,此时其它线程就可以获取锁了。

小结:Synchronized修饰方法和代码块都是通过Monitor来实现的,不同之处在于修饰方法时使用标志位ACC_SYNCHORONIZED来实现,而修饰代码块时使用monitorenter和monitorexit来实现。

2.2 ReentrantLock

ReentrantLock也是悲观锁的一种,与Synchronized不同的是,ReentrantLock不是基于monitor来实现同步,而是使用AQS(AbstractQueueSynchronizer),队列同步器来实现的,那么AQS是什么呢?我们先来看看AQS的内部结构。
AQS
可以看到,AQS内部是由一个同步队列和多个等待队列以及一个state标志位构成的(这个图有点歧义),当state标志位的值为1时,说明被lock()的部分有线程在使用,同步队列和等待队列的底层都是双向链表。当前需要获取锁的线程在满足某个条件后,才能进入等待队列,再满足Condition条件后,才能进入同步队列,与其它同步队列中的线程争抢锁。


ReentrantLock有非公平锁和公平锁两种实现方式,公平锁就是先进先出,谁先等待,谁先拥有,非公平锁就是后进入等待的线程有可能先拥有锁。此外,ReentrantLock也是可重入锁,当前线程可以多次进行加锁操作。

扫描二维码关注公众号,回复: 11383729 查看本文章

2.3 Synchronized和ReentrantLock的区别

事实上,Synchronized能做到的,ReentrantLock都能做到,而ReentrantLock能做到的,Synchoronized却不一定能做到,因此,可以认为ReentrantLock比Synchoronized功能上更加强大。同时这里列举一些它们的区别。

  • 实现方式:Synchronized是由jvm实现的,几乎所有的版本都有,而ReentrantLock是由jdk实现的,在jdk1.5版本之后才有的。

  • 性能差异:Synchronized在引入了自旋锁、偏向锁后,性能和ReetrantLock差不多,在引入之前,性能要远低于ReentrantLock。

  • 使用方式:Synchronized自动释放锁,而ReentrantLock需要手动释放锁。

  • ReentrantLock独有的三大功能:
    • 可以实现非公平锁和公平锁的自由切换,默认是非公平锁。
    • 可以结合Condition类实现有条件地分组唤醒线程,而Synchronized结合wait/notify/notifyall只能随机唤醒或全部唤醒线程。
    • 提供了能够中断等待锁的线程的机制,当一个线程一直等待而拿不到锁时,可以中断等待而去执行别的任务。

2.4 如何取舍

虽然ReentrantLock功能比Syncronized更加强大,但是使用起来更加麻烦,如果不追求并发量的话,使用Synchronized会比较好。


总结

本文主要介绍了乐观锁和悲观锁的实现方式,其中重点介绍了悲观锁的两种实现方式:Synchronized和ReentrantLock。CAS可以保证单个变量的原子性,但是不能保证多个变量的原子性操作,Synchronized和ReentrantLock功能是一样的,但是单层原理不同,要保证线程安全,就必须使用锁,至于怎么选择锁,需要看具体的应用场景。


参考资料:
https://blog.csdn.net/qq_35190492/article/details/104691668
https://blog.csdn.net/Baisitao_/article/details/102062329

猜你喜欢

转载自blog.csdn.net/qq_37163925/article/details/106075842