ReentrantLock锁

关于reentrantLock锁的一些问题

ReentrantLock是什么

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

1
public class ReentrantLock implements Lock, java.io.Serializable {}

ReentrantLock 里面有一个内部类 SyncSync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

ReentrantLock 的特点是可重入,支持公平锁和非公平锁两种方式,从其构造方法就能看出,在传入参数为true时,ReentrantLock为公平锁,默认为非公平锁。

1
2
3
4
5
6
7
8
public ReentrantLock() {
sync = new NonfairSync();
}
//基于参数,创建FairSync还是NonFairSync
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

阅读ReentrantLock代码可知,它主要利用CAS+AQS 队列来实现。以非公平锁为例,当线程竞争锁时首先使用CAS抢占锁,成功则返回,失败则进入AQS队列并且挂起线程;当锁被释放时,唤醒AQS中的某个线程,从被挂起处再次尝试获取锁(当AQS队列头节点的下一个节点不为空时,直接唤醒该节点;否则从队尾向前遍历,找到最后一个不为空的节点并唤醒),获取锁失败则再次进入队尾。

CAS

学习CAS首先了解什么是原子类

即为java.util.concurrent.atomic包下的所有相关类和API

  • 没有CAS之前

    多线程环境不使用原子类保证线程安全i++(基本数据类型) 常用synchronized锁,但是它比较重 ,牵扯到了用户态和内核态的切换,效率不高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class T3
{
volatile int number = 0;
//读取
public int getNumber()
{
return number;
}
//写入加锁保证原子性
public synchronized void setNumber()
{
number++;
}
}
  • 使用CAS之后

    多线程情况下使用原子类保证线程安全(基本数据类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class T3
{
volatile int number = 0;
//读取
public int getNumber()
{
return number;
}
//写入加锁保证原子性
public synchronized void setNumber()
{
number++;
}
//=================================
//下面是新版本
//=================================
AtomicInteger atomicInteger = new AtomicInteger();

public int getAtomicInteger()
{
return atomicInteger.get();
}

public void setAtomicInteger()
{
atomicInteger.getAndIncrement();//先读再加
}
}

CAS是什么

CAS是什么

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

执行CAS操作的时候,将内存位置的值与预期原值比较: 如果相匹配,那么处理器会自动将该位置值更新为新值, 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

CAS的缺点

ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销很大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

AQS

AQS是什么?

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。

AQS 就是一个抽象类,主要用来构建锁和同步器。

1
2
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}

AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如 ReentrantReadWriteLockSynchronousQueue等等皆是基于 AQS 的。

AQS的底层原理

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

CLH 队列结构如下图所示:

AQS(AbstractQueuedSynchronizer)的核心原理图(图源Java 并发之 AQS 详解open in new window)如下:

  • AQS使用一个int成员变量来表示同步状态
  • 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置
  • AQS资源共享方式:独占Exclusive(排它锁模式)和共享Share(共享锁模式)

待续。。。