显示锁

可重入锁

可重入锁也叫作递归锁,指的是一个线程可以多次抢占同一个锁。例如,线程A在进入外层函数抢占了一个Lock显式锁之后,当线程A继续进入内层函数时,如果遇到有抢占同一个Lock显式锁的代码,线程A依然可以抢到该Lock显式锁。

ReentrantLock 介绍

ReentrantLock 是JUC包提供的显式锁的一个基础实现类,ReentrantLock 类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,但是拥有了限时抢占、可中断抢占等一些高级锁特性。此外,ReentrantLock 基于内置的抽象队列同步器(AbstractQueuedSynchronized,AQS)实现,在争用激烈的场景下,能表现出表内置锁更佳的性能。

可重入的含义

表示该锁能够支持一个线程对资源的重复加锁,也就是说,一个线程可以多次进入同一个锁所同步的临界区代码块。

独占的含义

在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁。

不可重入锁

指的是一个线程只能抢占一次同一个锁。

悲观锁

概念

悲观锁就是悲观思想,每次进入临界区操作数据的时候都认为别的线程会修改,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。总体来说,悲观锁适用于写多读少的场景,遇到高并发写时性能高。

存在的问题

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,从而引发性能风险。

解决方案

使用乐观锁去替代悲观锁, 数据库操作中的带版本号数据更新、JUC包的原子类,都使用了乐观锁的方式提升性能。

乐观锁

概念

乐观锁是一种乐观思想,每次去拿数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就要重复读-比较-写的操作。总体来说,乐观锁适用于读多写少的场景,遇到高并发写时性能低。

实现方式

CAS实现乐观锁

CAS操作中包含三个操作数:

  1. 需要操作的内存位置(V)、
  2. 进行比较的预期原值(A)
  3. 拟写入的新值(B)。

如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B;否则处理器不做任何操作。

Java的synchronized轻量级锁是一种乐观锁,JUC中基于抽象队列同步器(AQS)实现的显式锁(如ReentrantLock)都是乐观锁。

version版本号

利用数据库的version版本号实现乐观锁

公平锁

公平锁是指不同的线程抢占锁的机会是公平的、平等的,从抢占时间上来说,先对锁进行抢占的线程一定被先满足,抢锁成功的次序体现为FIFO(先进先出)顺序。

ReentrantLock实例是非公平锁,但是,如果在实例构造时传入了参数true,所得到的锁就是公平锁。

非公平锁

非公平锁是指不同的线程抢占锁的机会是非公平的、不平等的,从抢占时间上来说,先对锁进行抢占的线程不一定被先满足,抢锁成功的次序不会体现为FIFO(先进先出)顺序。

可中断锁

如果某一线程A正占有锁在执行临界区代码,另一线程B正在阻塞式抢占锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待,这种就是可中断锁。

JUC的显式锁(如ReentrantLock)是一个可中断锁。

不可中断锁

一旦这个锁被其他线程占有,如果自己还想抢占,只能选择等待或者阻塞,直到别的线程释放这个锁,如果别的线程永远不释放锁,那么自己只能永远等下去,并且没有办法终止等待或阻塞。

Java的synchronized内置锁就是一个不可中断锁。

共享锁

共享锁允许多个线程同时获取锁,容许线程并发进入临界区。与独占锁不同,共享锁是一种乐观锁,它放宽了加锁策略,并不限制读/读竞争,允许多个执行读操作的线程同时访问共享资源

JUC的ReentrantReadWriteLock(读写锁)类是一个共享锁实现类。

独占锁

独占锁指的是每次只有一个线程能持有的锁。独占锁是一种悲观保守的加锁策略,它不必要地限制了读/读竞争,如果某个只读线程获取锁,那么其他的读线程都只能等待,这种情况下就限制了读操作的并发性,因为读操作并不会影响数据的一致性。

JUC的ReentrantLock类是一个标准的独占锁实现类。