Java基础之java中的各种锁详细介绍,悲观锁/乐观锁,可重入锁/非可重入锁

  • Post author:
  • Post category:java


Java提供了种类丰富的锁, 每种锁因特性不同, 在适当的应用场景下能够展示出非常高的效率.

Java中往往是按照是否含有某一特性来定义锁, 我们通过特性将锁进行分组归类, 再使用对比的方式进行介绍, 帮助大家更快捷的理解相关知识. 下面给出本文内容的总体分类目录:

在这里插入图片描述



1. 乐观锁VS悲观锁

乐观锁与悲观锁是一种广义的概念, 体现了看待多线程同步的不同角度, 在Java和数据库都有此概念对应的实际应用.



1.1 悲观锁



1.1.1 概述

  • 总是假设最坏的情况,每次去读数据的时候都认为别人会修改,所以 每次在读数据的时候都会上锁 .
  • 这样别人想读取数据就会阻塞直到它获取锁 (共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
  • 传统的关系型数据库里边就用到了很多悲观锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

    在这里插入图片描述



1.1.2 实现

Java中 synchronized 和 ReentrantLock(可重入锁) 就是悲观锁思想的实现。

在博主上篇文章如何保证线程安全中有详细讲解了synchronized和Lock锁, 详见链接

synchronized和Lock详解



1.2 乐观锁



1.2.1 概述

  • 乐观锁认为自己在读数据时不会有别的线程修改数据, 所以不会添加锁,.
  • 只是在更新数据的时候去判断之前有没有别的线程更新了这个数据.
  • 如果这个数据没有被更新, 当前线程将自己修改的数据成功写入. 如果数据已经被其他线程更新, 则根据不同的实现方式执行不同的操作(例如报错或自动重试).

    在这里插入图片描述



1.2.2 实现方式



(1) CAS算法

  • 在Java中 java.util.concurrent.atomic包下面的原子变量类 就是基于CAS实现的乐观锁.
  • CAS并不是一种实际的锁, 它仅仅是实现乐观锁的一种思想, java中的乐观锁(如自旋锁)基本都是通过CAS操作实现的. CAS是一种更新的原子操作, 比较当前值跟传入值是否一样, 一样则更新, 否则失败.
  • CAS全称为Compare And Swap即比较并交换,其算法公式如下:

    函数公式:CAS(V,E,N) V:表示要更新的变量, E:表示预期值, N:表示新值.

    在这里插入图片描述



(2) 数据库的版本控制

  • 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数. 当数据被修改时,version++即可。
  • 当线程A要更新数据值时, 在读取数据的同时也会读取version值, 在提交更新时, 若现在version值等于读取数据是的version值, 才会提交更新操作.
  • 否则重试更新操作,直到更新成功。

    在这里插入图片描述



1.3 应用场景

根据从上面的概念描述我们发现:

  • 悲观锁适合

    写操作多

    的场景, 先加锁可以保证

    写操作时

    数据正确.
  • 乐观锁适合

    读操作多

    的场景, 不加锁的特点能够是其

    读操作时

    性能大幅度提升.



2. 死锁VS活锁



2.1 死锁



2.1.1 概述

当多个线程

循环等待

彼此占有的资源释放, 而无限期的僵持等待下去的局面.

在这里插入图片描述

  • 线程T1要获得资源A, 才能执行完当前线程并释放手中的资源B, 而A被线程T2占用, 要等着T2释放.
  • 线程T2要获得资源B, 才能执行完当前线程并释放手中的资源A, 而B被线程T1占用, 要等着T1释放
  • 但是目前的情况就是T1和T2都在等待对方的资源释放才会继续执行, 但是他们等待的资源都在对方手中, 如果执行不完又无法释放. 但是要想释放就要先执行完. 进入了死循环, 这就是死锁.



2.1.2 死锁产生的原因



(1)竞争资源, 系统中的资源可以分为两类:
  1. 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,

    CPU和内存

    均属于可剥夺性资源;
  2. 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
  • 产生死锁中的竞争资源之一指的是

    竞争不可剥夺资源

    (例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
  • 产生死锁中的竞争资源另外一种资源指的是

    竞争临时资源

    (临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁.



(2)死锁产生的必要条件, 也就是需要同时具备以下4个条件, 才会有死锁现象, 如果有一项不满足也是不会出现死锁的.

  1. 互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
  2. 请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
  3. 不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
  4. 循环等待:发生死锁时,线程进入死循环,永久阻塞。



2.1.3 解决死锁

  • 破坏互斥条件 : 这个条件我们没有办法破坏, 因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。



2.2 活锁

  • 活锁和死锁在表现上是一样的两个线程都没有任何进展,但是区别在于:死锁,两个线程都处于阻塞状态,说白了就是它不会再做任何动作,我们通过查看线程状态是可以分辨出来的。
  • 而活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功。
  1. 举个生动的例子的话,两个人都没有停下来等对方让路,而是都有很有礼貌的给对方让路,但是两个人都在不断朝路的同一个方向移动,这样只是在做无用功,还是不能让对方通过。



3. 自旋锁VS适应性自旋锁



3.1 什么是自旋锁呢?

  • 线程在获取资源的时候, 资源被占用, 为了让

    当前线程, “稍微等一下”

    , 我们需让

    当前线程

    进行自旋, 也就是等待着.
  • 如果在自旋完成后, 前面锁定同步资源的线程已经释放了锁, 那么

    当前线程

    就可以不必阻塞而是直接获取同步资源, 从而避免切换线程的开销, 这就是自旋锁.

    在这里插入图片描述
  • 自旋锁不能替代阻塞, 自旋等待虽然避免了线程切换的开销,但它要占用处理器时间.
  • 如果锁被占用的时间很短, 自旋等待的效果就会非常好; 反之, 如果锁被占用的时间很长, 那么自旋的线程只会白浪费处理器资源.
  • 所以自旋等待的时间必须要有一定的限度, 如果自旋超过了限定次数(默认10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁, 就应当挂起线程.



3.2 适应性自旋锁


那自旋次数还要手动更改,效率不高,所以JDK1.6以后就引入了自适应的自旋锁(适应性自旋锁)

.

  • 自适应性意味着自旋的时间(次数)不再固定, 而是前一次在同一个锁上的自旋时间及锁的拥有者的转态来决定.
  • 如果在同一个锁对象上, 自旋等待刚刚成功获得过锁, 并且持有锁的线程正在运行中, 那么虚拟机就会认为下次自旋也是很有可能再次成功, 而且允许它有次数较多的自旋次数.
  • 如果对于某个锁, 自旋很少成功获得过, 那么下次自旋则认为它失败几率大, 直接阻塞线程, 避免浪费处理器资源.



4. 无锁VS偏向锁VS轻量级锁VS重量级锁

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

在这里插入图片描述



5. 可重入锁VS非可重入锁

一个线程中有多个子流程, 而资源只有一个, 那么这些子流程如何和资源锁定呢? 所以这时候又引入了

可重入锁和非可重入锁

.

  • 可重入锁又名递归锁, 是指在同一个线程在外层方法获取锁的时候, 再进入线程的内层方法会自动获取锁(不过前提是锁对象也就是资源是一个对象或class). 不会因为之前获取过还没释放而阻塞. 可重入锁的优点就是:

    可一定程度避免死锁.
  • 如下图: 村民代表线程, 多个水桶代表多个子流程, 锁代表唯一的资源.

    在这里插入图片描述
  • 非可重入锁, 就是指在同一个线程在外层方法获取锁的时候, 再进入线程的内层方法必须等外层方法释放锁才能获取, 如果外层方法没有释放锁, 就会出现死锁现象.

    在这里插入图片描述



6. 可中断锁

  • 可中断锁:顾名思义, 就是可以相应中断的锁.在Java中, synchronized就不是可中断锁, 而Lock是可中断锁, Lock.lockInterruptibly()就可以中断锁.
  • 如果某一线程A正在执行锁中的代码, 另一线程B正在等待获取该锁, 可能由于等待时间过长, 线程B不想等待了想先处理其他事情, 我们可以让B中断自己或者在别的线程中中断它,这种就是可中断锁。



7. 共享锁(读锁)VS独享锁(排他锁或写锁)

多个线程能不能共享一把锁呢?如果能就是共享锁, 如果不能就是独享锁.

  • 共享锁顾名思义就是某一资源可以被多个线程公用, 共享锁又名写锁, ReadWriteLock中的readLock()方法获取读锁.
  • 独享锁也叫排他锁或者写锁, 是指该锁一次只能被一个线程所持有. 如果线程A对数据T加上排他锁后, 则其他线程不能再对T加任何类型的锁, 获得排他锁的线程

    即能读数据又能修改数据

    , synchronized就是排他锁, 还有ReadWriteLock的writeLock()能获取写锁.
  • 不过ReadWriteLock读写锁使用时是一个整体, ReadWriteLock其实是互斥锁, 它内部有readLock()和writeLock()两个方法分别获取读锁和写锁, 实现读读数据共享, 读写, 写读,写写过程资源不共享保证线程安全.



8. 公平锁和非公平锁

  • 如果一个线程组里, 能保证每个线程都能拿到锁, 那么这个锁就是公平锁. 相反, 如果保证不了每个线程都能拿到锁, 也就是存在有线程饿死, 那么这个锁就是非公平锁.

    synchronized就是非公平锁, Lock.ReetrantLock可以有公平锁和非公平锁.
  • ReentrantLock虽然有公平锁和非公平锁两种, 但是它们添加的都是独享锁.
  • 非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间.
  • 使用场景的话呢,其实还是和他们的属性一一相关,举个栗子:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁会给业务增强很多的可控制性。



版权声明:本文为weixin_42641909原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。