1 简介
JUC包简介
- JUC全称java.util.concurrent,是JDK提供的并发工具包
- JUC包由Doug Lea大师编写,提供了大量实用,高性能的工具类
JUC包结构图
- JUC包下包含了阻塞队列、线程池、并发容器等,另外还包含了两个子包atomic和locks
- JUC包的整体实现图
- JUC包中类的实现主要是依赖于CAS和volatile关键字
2 locks包
1 Lock接口
- Lock接口概述
- 锁是用来控制多个线程访问共享资源的方式,一个锁能够防止多个线程同时访问共享资源
- 在Lock接口出现之前,Java程序主要是靠synchronized关键字实现锁功能的
而JDK5之后JUC包中增加了Lock接口,Lock接口提供了与synchronized一样的锁功能
- Lock接口是JUC包提供的一种全新的互斥同步手段
- Lock接口的实现类通过CAS操作和volatile关键字来保证线程安全
通常使用Lock的方式如下
Lock lock = new ReentrantLock(); //ReentrantLock是Lock的实现类
lock.lock();
try {
.......
} finally {
lock.unlock();
}
- synchronized同步块执行完成或者遇到异常时锁会自动释放,而Lock必须调用
**unlock()**
方法释放锁
因此为了避免同步代码出现异常影响解锁,需要在finally块中释放锁
- 虽然Lock接口失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性
- Lock接口API
Lock接口定义了6个方法
//获取锁
void lock();
//获取锁的过程能够响应中断
void lockInterruptibly() throws InterruptedException;
//非阻塞式响应中断能立即返回,获取锁返回true反之返回fasle
boolean tryLock();
//超时获取锁,在超时内或者未中断的情况下能够获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
unlock();
//创建一个与Lock对象绑定的条件对象,当前线程必须获得了锁才能在条件对象上等待
//线程等待时会先释放锁,当再次获取锁时才能从等待中返回
Condition newCondition();
- Lock的实现
- ReentrantLock是常用的Lock接口实现类
- ReentrantLock的源码并没有多少代码,并且ReentrantLock基本所有方法的实现都是调用了其静态内部类Sync(同步器)中的方法,而Sync类继承自抽象类AbstractQueuedSynchronizer(AQS)。
- 因此要想理解Lock的实现原理关键是要理解AQS
2 AQS同步器
1 简介
- AQS概述
- AQS全称AbstractQueuedSynchronizer(抽象阻塞同步器),是位于locks包下的抽象类,是一种通用的同步器机制,JUC中的同步器都继承自该类
- 同步器是用来构建阻塞式锁和其他同步组件的基础框架
在AQS同步器出现之前,程序员会自己通过一种同步器去实现另一种相近的同步器,这显然不够优雅,因此在JDK5引入AQS同步器
- AQS同步器自身没有实现任何同步接口(如Lock),它仅仅是定义了锁状态的若干获取和释放方法来供自定义同步组件的使用
- AQS同步器既支持独占式获取锁状态,也可以支持共享式获取锁状态(独占模式是只有一个线程能够获取锁,而共享模式可以允许多个线程获取锁),这样就可以方便的实现不同类型的同步组件
- 常用的Lock接口实现类ReentrantLock就是基于AQS的
- AQS同步器与锁的关系
- 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
- 锁面向使用者,其定义了使用者与锁交互的接口,隐藏了实现细节
同步器是面向锁的实现者,它简化了锁的实现方式、屏蔽了锁状态的管理、线程的排队、等待和唤醒等底层操作。
- 同步组件通过调用同步器提供的方法可以很方便地实现需要的功能
锁和同步器很好的隔离了使用者和实现者所需关注的领域
- AQS的实现特点
- AQS的核心是锁的状态,用int类型属性state来表示
- 继承AQS的子类需要重写AQS的几个方法来自定义维护state的方式,进而控制如何获取锁和释放锁
AQS提供了以下方法来维护state
- `**getState()**` - 获取state
- `**setState()**` - 设置state
- `**compareAndSetState()**` - CAS操作设置state,注意**使用CAS设置state仅保证设置state时的原子性,并不会反复重试,AQS的原理仍然是阻塞式的锁**
- AQS提供了基于FIFO的阻塞队列,类似于Monitor的EntryList,不过Monitor的EntryList是基于C++实现的,而AQS的阻塞队列是基于Java实现的
- AQS提供了条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet,不过AQS支持多个条件变量,而Monitor仅隐式支持一个
- AQS的使用
AQS被推荐定义为自定义同步组件中继承自AQS的静态内部类,例如ReentrantLock的定义
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//Sync是抽象的是因为ReentrantLock内部还实现了FairSync和NonfairSync,它们都继承自Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
...
}
2 AQS的设计模式
- 模板方法设计模式
AQS使用模板方法设计模式,即AQS将一些方法开放给子类进行重写,而AQS给同步组件所提供模板方法又会重新调用其被子类所重写的方法
- AQS提供的模板方法
在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义
模板方法不需要重写,但是模板方法会调用被子类重写的方法
**void acquire(int arg)**
独占式获取同步状态,如果获取失败则将线程插入阻塞队列进行等待
**void acquireInterruptibly(int arg)**
与acquire()
方法相同,但在阻塞队列中等待的时候可以被中断
**boolean tryAcquireNanos(int arg, long nanosTimeout)**
在**acquireInterruptibly()**
基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
**boolean release(int arg)**
释放同步状态,该方法会唤醒在阻塞队列中的下一个节点
- AQS可重写的方法(独占模式)
AQS中被protected修饰的方法可以根据需要被继承自AQS的子类重写,只需要重写以下三个方法
**protected boolean tryAcquire(int arg)**
独占式尝试使用CAS获取一次锁(实际上是尝试使用CAS设置锁状态),获取失败时返回false,否则返回true
实现该方法需要查询当前锁状态并判断锁状态是否符合预期,然后再通过CAS操作设置锁状态
**protected boolean tryRelease()**
独占式尝试释放锁
protected boolean isHeldExclusively()
当前锁是否在独占模式下被线程占用,即表示锁是否被当前线程所独占
3 自定义独占式不可重入锁
实现Lock接口需要实现多个方法,如果方法内部的逻辑全部自己实现比较困难。但是我们自定义的同步器已经实现了大部分锁的底层逻辑,因此我们实现Lock接口的方法时只需要编写很少一部分代码
class MyLock implements Lock {
MySync sync = new MySync();
//自定义非重入同步器
static class MySync extends AbstractQueuedSynchronizer {
//尝试获取锁
@Override
protected boolean tryAcquire(int acquires) {
//尝试使用CAS操作将state设置为1
if (compareAndSetState(0, 1)) {
//加锁成功,将锁的owner设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//尝试释放锁
@Override
protected boolean tryRelease(int acquires) {
//注意以下两语句的顺序不能颠倒
//因为AQS的state被volatile修饰,而volatile的写屏障可以保证在此之前语句的操作被其他线程看到
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//是否持有独占锁
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
protected Condition newCondition() {
return new ConditionObject();
}
}
@Override
//尝试加锁,若不成功则进入阻塞队列
public void lock() {
sync.acquire(1);
}
@Override
//尝试加锁,若不成功则进入阻塞队列,可被打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
//尝试加锁一次,若不成功则返回,不进入阻塞队列
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
//尝试加锁,若不成功则进入阻塞队列,阻塞时间有上限
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
//释放锁
public void unlock() {
sync.release(1);
}
@Override
//生成条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
测试 ```java MyLock lock = new MyLock(); new Thread(() -> { log.debug(“locking…”); lock.lock(); try {
log.debug("get lock!");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("unlocking...");
lock.unlock();
} }, “t1”).start();
new Thread(() -> { log.debug(“locking…”); lock.lock(); try { log.debug(“get lock!”); } finally { log.debug(“unlocking…”); lock.unlock(); } }, “t2”).start();
- 上述代码输出
```java
15:24:38 [t1] c.Test - locking...
15:24:38 [t1] c.Test - get lock!
15:24:38 [t2] c.Test - locking...
15:24:43 [t1] c.Test - unlocking...
15:24:43 [t2] c.Test - get lock!
15:24:43 [t2] c.Test - unlocking...
4 AQS原理
- 同步组件依赖于AQS,同步组件的方法大量调用了AQS的模板方法(即AQS的API),因此了解了AQS的实现原理也就能很轻松的了解同步组件的实现原理
1 AQS的属性
//阻塞队列的头节点
private transient volatile Node head;
//阻塞队列的尾节点
private transient volatile Node tail;
//同步状态
private volatile int state;
//独占模式下的持有同步状态的线程
//该属性是AQS父类中的属性,放在此处方便阅读
private transient Thread exclusiveOwnerThread
2 阻塞队列
阻塞队列概述
- 当同步状态被某个线程占有,其他请求该同步状态的线程将会阻塞,从而进入阻塞队列。
- 就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是通过链式方式进行实现。
阻塞队列节点的属性
在AQS有一个静态内部类Node,表示阻塞队列中的节点,其属性如下
//节点等待状态
volatile int waitStatus;
//当前节点/线程的前驱节点
volatile Node prev;
//当前节点/线程的后继节点
volatile Node next;
//加入阻塞队列的线程引用
volatile Thread thread;
- 从上述属性我们可以看出阻塞队列是一个双向链表
节点的等待状态
//节点从同步队列中取消
static final int CANCELLED = 1;
//后继节点的线程处于等待状态
static final int SIGNAL = -1;
//当前节点位于条件变量的等待队列中
static final int CONDITION = -2;
//初始状态
static final int INITIAL = 0;
AQS与阻塞队列
AQS的属性中有两个重要属性
//阻塞队列的头节点
private transient volatile Node head;
//阻塞队列的尾节点
private transient volatile Node tail;
AQS通过阻塞队列的头节点和尾结点来管理阻塞队列,实现包括获取锁失败的线程进行入队,释放锁时对阻塞队列中的线程进行通知等核心方法
3 获取同步状态原理
获取同步状态方法源码
**void acquire(int arg)**
public final void acquire(int arg) {
//如果尝试获取同步状态失败,则创建一个Node对象并将其加入到阻塞队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
**boolean acquireQueued(final Node node, int arg)**
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
//获取node的前驱节点
final Node p = node.predecessor();
//如果前驱节点是哨兵,则再次调用tryAcquire()尝试获取同步状态
if (p == head && tryAcquire(arg)) {
//成功获取同步状态,从阻塞队列中摘掉node
setHead(node);
p.next = null; // help GC
return interrupted;
}
//获取同步状态再次失败,调用shouldParkAfterFailedAcquire()方法,阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
**boolean shouldParkAfterFailedAcquire(Node pred, Node node)**
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//This node has already set status asking a release to signal it,
//so it can safely park.
return true;
if (ws > 0) {
//Predecessor was cancelled. Skip over predecessors and indicate retry.
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//waitStatus must be 0 or PROPAGATE.
//Indicate that we need a signal, but don't park yet.
//Caller will need to retry to make sure it cannot acquire before parking.
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
**boolean parkAndCheckInterrupt()**
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
获取同步状态流程分析
现假设已经线程0持有了同步状态,此时线程1尝试获取同步状态
- 线程0持有同步状态,没有发生竞争
- 线程1尝试获取同步状态失败(子类重写的
tryAcquire()
逻辑,假设逻辑是如果state已经是1则返回false),执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
方法 - 进入
addWaiter()
方法,该方法用于加入节点至阻塞队列
- 图中三角表示Node的waitStatus,0为默认正常状态
- 第一个Node称为Dummy或哨兵或头节点(惰性创建),用来占位,并不关联线程
头节点虽然不关联线程(指向null),但是头节点变量本身并不为null,而是指向一个node对象
- 进入
acquireQueued()
方法
线程1会在acquireQueued()
方法中再次尝试获取同步状态
- 线程1在一个死循环中不断尝试获得锁,如果失败则调用
LockSupport.park()
进入等待状态 - 如果关联线程1的节点是紧邻哨兵的节点,那么再次尝试获取锁,此时线程0没有释放锁,线程1获取同步状态再次失败
- 进入
shouldParkAfterFailedAcquire()
方法,此时前驱节点的waitStatus为0,将前驱节点的waitStatus改为 -1,方法返回false- 节点的waitStatus为-1表示该节点有责任唤醒其后继节点
- 再次进入循环尝试获取同步状态,若再次失败,进入
shouldParkAfterFailedAcquire()
方法,此时该方法返回true,并进入**parkAndCheckInterrupt()**
方法parkAndCheckInterrupt()
方法调用LockSupport.park(this)
阻塞当前线程(线程1)
4 释放同步状态原理
释放同步状态方法方法源码
**boolean release(int arg)**
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
**void unparkSuccessor(Node node)**
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//将头节点waitStatus置为0
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//找到与头节点最近的waitStatus为-1的后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
//唤醒后继节点
if (s != null)
LockSupport.unpark(s.thread);
}
释放同步状态方法流程分析
现假设已经线程0持有了同步状态,多个线程竞争失败
- 初始状态,多个线程阻塞
- 执行
tryRelease()
方法,线程0尝试释放锁,如果成功,则将exclusiveOwnerThread置为null、state置为0
tryRelease()
方法会调用release()
来唤醒一个线程
- 执行
release()
方法,由于当前头节点不为null且头节点的waitStatus为-1,因此进入unparkSuccessor()
方法:找到阻塞队列中离头节点最近的一个waitStatus为-1的Node(图中为表示线程1的Node),调用LockSupport.unpark()
恢复其运行 - 线程1被唤醒,继续执行其
acquireQueued()
方法,尝试获取同步状态
若获取成功,则:
- 将exclusiveOwnerThread设置为Thread-1、state设置为1
- 将表示线程1的Node置为头节点
- 将之前的头节点与阻塞队列断开,此后该头节点可被垃圾回收
若获取失败,如又有一个新的线程4获取到了同步状态,那么线程1获取同步状态失败,重新调用LockSupport.park()
方法阻塞线程1
由于等待最久的线程1不一定能获取到同步状态,因此这是不公平的策略
5 可打断原理
- 如果希望在等待锁的时候可以被打断,则基于AQS的同步组件会调用
**acquireInterruptibly()**
而不是**acquire()**
方法
**void acquireInterruptibly(int arg) throws InterruptedException**
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取同步状态失败后,进入doAcquireInterruptibly()而不是acquireQueued()
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
**void doAcquireInterruptibly(int arg)**
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//注意这里与acquireQueued()的区别
//这里会直接抛出异常,而acquireQueued()不会
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
**boolean parkAndCheckInterrupt()**
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 当park()被打断时,会返回true并清除打断标记
return Thread.interrupted();
}
6 条件变量等待原理
条件变量概述
- 每一个条件变量就对应着一个等待队列(类似于Monitor的WaitSet)
- 条件变量的实现类是ConditionObject
- ConditionObject是AQS的内部类
等待与唤醒
- 等待
可以在条件变量上调用await()
方法,使得当前线程在该条件上等待,进入WAITING状态
- 唤醒
可以在条件变量上调用signal()
或signalAll()
方法,唤醒条件变量上的一个或全部线程
- 条件变量的属性
//等待队列的头节点
private transient Node firstWaiter;
//等待队列的尾结点
private transient Node lastWaiter;
条件变量等待方法源码
**void await() throws InterruptedException**
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//将代表当前线程的Node加入当前条件的等待队列中
//等待队列是一个链表,类似于同步器的阻塞队列
Node node = addConditionWaiter();
//将节点对应的持有的锁全部释放掉并唤醒阻塞队列中的线程
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//进入WAITING状态,等待被唤醒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
**Node addConditionWaiter()**
private Node addConditionWaiter() {
//###如果当前线程没有持有锁,则抛出异常###
//因此await()与Object::wait()方法一样,调用的前提是持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//新建一个代表当前线程的节点并加入到当前条件的等待队列中
Node node = new Node(Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
条件变量等待流程分析
- 初始状态,线程0获取锁,多个线程阻塞
- 线程0在一个Condition对象上调用
**await()**
方法
await()方法会创建一个代表当前线程的Node对象(该Node对象的WaitStatus为-2,表示在Condition的等待队列中等待),之后将该Node加入Condition对象的等待队列中
- 该等待队列类似于AQS的阻塞队列,但是没有头节点
- 线程0进入等待队列后,将调用fullRelease(),释放掉线程0的所有锁,并唤醒阻塞队列中的线程
7 条件变量唤醒原理
条件变量唤醒方法源码
**void signal()**
public final void signal() {
//如果不是锁的持有者调用该方法则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//唤醒Condition等待队列中的第一个线程
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
**void doSignal(Node first)**
private void doSignal(Node first) {
do {
//将first节点从等待队列中断开
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
//将first节点转移到AQS的阻塞队列中
//如果转移失败,则尝试唤醒first的后一个节点
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
**boolean transferForSignal(Node node)**
final boolean transferForSignal(Node node) {
//先将Node的WaitStatus从2转为0
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
//enq()方法将node加入到AQS的阻塞队列,如果成功了返回node的前驱节点
Node p = enq(node);
//将前驱节点的WaitStatue改为-1
int ws = p.waitStatus;
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
条件变量唤醒流程分析
- 初始状态,Thread0在等待队列,Thread1持有锁
- 持有锁的线程1调用Condition对象的
signal()
方法唤醒该对象等待队列的一个线程
singal()
方法调用doSignal()
方法将表示线程0的Node从Condition对象的等待队列中断开
doSignal()
方法调用transferForSignal()
方法,将代表线程0的Node的状态改为0,并将其插入到AQS阻塞队列的队尾,同时将Node的前驱节点的状态改为-1
- 需要注意的是,线程0在整个唤醒的过程中都处于
**park()**
状态(WAITING状态)没有改变,唤醒仅是让线程0可以参与后续锁的竞争
8 阻塞队列(同步队列)与等待队列的关系
5 Condition接口与Object监视器方法
- Condition接口与Object监视器方法概述
- 任何一个Java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如
wait()
、wait(long timeout)
与notify()
、notifyAll()
几个方法实现等待/唤醒机制
- 任何一个Java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如
同样的, 在JUC包的Lock体系下依然会有同样的方法实现等待/唤醒机制。
- Object的wait()和notify()/notifyAll()是与Monitor配合完成线程间的等待/唤醒机制
Condition与AQS配合(ConditionObject是AQS的内部类)完成等待通知机制
前者是Java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。
- Condition接口与Object监视器方法的对比
| 对比项 | Object Monitor | Condition |
| —- | —- | —- |
| 前置条件 | 线程获取到锁对象 |
- 线程调用lock.lock()
获取到锁
- 存在Condition对象
| | 调用方式 | 锁对象上调用 | Condition对象上调用 | | 等待队列(WaitSet)个数 | 1 | 多个 | | 线程进入等待队列时释放锁 | √ | √ | | 线程进入等待队列时响应中断 | √ | √ | | 线程进入等待队列时不响应中断 | ×(必须响应中断) | √ | | 线程进入等待队列时进入超时等待状态 | √ | √ | | 唤醒等待队列中的一个线程 | √ | √ | | 唤醒等待队列中的全部线程 | √ | √ |
3 ReentrantLock类
1 ReentrantLock简介
ReentrantLock(重入锁)类概述
- ReentrantLock是java.util.concurrent.locks包下的类
- ReentrantLock是Lock接口的一个实现类,也是在实际编程中使用频率很高的一个锁
ReentrantLock基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
- synchronized是在Java语法级别(关键字级别)保护临界区,而ReentrantLock则是在对象级别保护临界区,因此在使用ReentrantLock前需要先创建ReentrantLock对象
- Lock应该确保在finally块中释放锁,否则一旦临界区中的代码抛出异常时,则可能永远不会释放持有的锁
2 ReentrantLock与synchronized
- ReentrantLock与synchronized功能的区别
ReentrantLock与synchronized一样是可重入的,在基本用法上ReentrantLock也与synchronized很相似,只是代码写法上稍有区别。不过ReentrantLock与synchronized相比增加了一些高级功能,主要是以下四项(a、b实际上是LockSupport的特性)
- 超时加锁(主动避免长期等待锁释放)
当持有锁的线程长期不释放锁的时候,正在阻塞等待锁的线程可以选择放弃等待,改为处理其他事情
- 可中断加锁(被动避免长期等待锁释放)
等待持有锁的线程释放锁的线程,也可以被中断
可中断可以避免死锁
- 公平锁
- 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- 非公平锁是指在锁被释放时,任何一个等待锁的线程都有机会获得锁
- synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数将ReentrantLock配置为公平锁
- ReentrantLock一旦使用公平锁,其性能将急剧下降,会明显影响吞吐量
- 锁绑定多个条件
- 一个ReentrantLock对象可以同时绑定多个Condition对象(Condition类位于java.util.concurrent.locks包)
- 在synchronized中,锁对象的wait()和notify()/notifyAll()方法相配合,可以实现一个隐含的条件,但是无法和多于一个的条件相关联
ReentrantLock则可以通过newCondition()方法与多个条件相关联
- 换句话说,synchronized只有一个WaitSet,而ReentrantLock可以有多个WaitSet,每个WaitSet对应一个条件,线程可以根据不同的条件进入不同的WaitSet,这样可以做到更为精准的唤醒,减少虚假唤醒
- ReentrantLock与synchronized的选择
- 由于JDK6针对synchronized关键字的优化,两者在性能上已相差无几,因此不再是选择的参考因素
- 在功能上,ReentrantLock是synchronized的超集,如果需要使用ReentrantLock独有的功能肯定要选择ReentrantLock
- 在synchronized和ReentrantLock都可满足需要时,优先选择synchronized,原因如下
- synchronized是在Java语法层面上的同步,足够清晰简单,被更多程序员熟知
- Lock必须确保在finally块中释放锁,但这是由程序员自己来保证的,而使用synchronized的话则可以由JVM来确保即使出现异常,锁也能被自动释放
- 从长远来看,synchronized更容易被继续优化,而ReentrantLock则难以继续优化
3 ReentrantLock的使用
- 可中断加锁
注意如果想让进入BLOCKED状态的线程可被中断,那么在加锁时就不能使用reentrantLock.lock()
方法,而必须使用**reentrantLock.lockInterruptibly()**
方法
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
//使用可被打断的方式加锁
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
reentrantLock.unlock();
}
}, "t1");
reentrantLock.lock();
try {
log.debug("主线程获得了锁");
t1.start();
Thread.sleep(1000);
t1.interrupt();
log.debug("执行打断");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
- 上述代码输出
15:48:49 [main] c.Test - 主线程获得了锁
15:48:49 [t1] c.Test - 启动...
15:48:50 [main] c.Test - 执行打断
15:48:50 [t1] c.Test - 等锁的过程中被打断
java.lang.InterruptedException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:944)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1263)
at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:317)
at Test.lambda$main$0(Test.java:18)
at java.base/java.lang.Thread.run(Thread.java:834)
- 超时等待
超时等待需要使用**reentrantLock.tryLock(long timeout)**
方法实现
- 如果该方法不传入参数,则线程在尝试获取锁失败后将直接退出阻塞状态
- 如果传入参数则会在阻塞时间到达timeout后退出阻塞 ```java private static final ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug(“启动…”); if (!reentrantLock.tryLock()) { log.debug(“获取立刻失败,返回”); return; } try { log.debug(“获得了锁”); } finally { reentrantLock.unlock(); } }, “t1”);
reentrantLock.lock();
try {
log.debug("主线程获得了锁");
t1.start();
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
- 上述代码输出
```java
16:00:10 [main] c.Test - 主线程获得了锁
16:00:10 [t1] c.Test - 启动...
16:00:10 [t1] c.Test - 获取立刻失败,返回
- 公平锁
如果想将ReentrantLock配置为公平锁,实例化ReentrantLock对象时,给ReentrantLock构造函数传入true即可
- ReentrantLock构造函数如下
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 条件变量
- 使用
**reentrantLock.newCondition()**
可以产生一个新的条件对象Condition - 等待
在Condition对象上调用**condition.await()**
方法即可在指定条件上等待
- 在执行
condition.await()
方法之前必须先获得锁,执行该方法后将释放锁 - 执行
await()
方法的线程可以被唤醒也可以被中断(类似wait()),中断将抛出异常- 唤醒
在Condition对象上调用**condition.signal()**
/**condition.signalAll()**
方法即可唤醒某个或所有等待指定条件的线程
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
static volatile boolean hasCigarette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
while (!hasCigarette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitBreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
//唤醒等待早餐的线程
sendBreakfast();
Thread.sleep(1000);
//唤醒等待香烟的线程
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟来了");
hasCigarette = true;
//随机唤醒一个waitCigaretteQueue条件上的线程
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
//唤醒所有waitCigaretteQueue条件上的线程
waitBreakfastQueue.signalAll();
} finally {
lock.unlock();
}
}
- 上述代码输出
16:38:24 [main] c.Test - 送早餐来了
16:38:24 [Thread-1] c.Test - 等到了它的早餐
16:38:25 [main] c.Test - 送烟来了
16:38:25 [Thread-0] c.Test - 等到了它的烟
4 ReentrantLock原理
- ReentrantLock类结构图
- ReentrantLock内部实现了一个抽象类Sync,其继承自AQS
- FairSync和NonfairSync均继承自Sync,分别实现了公平同步器和非公平同步器’,ReentrantLock实际使用的是这两个同步器
- FairSync和NonfairSync的区别是对于
**tryAcquire()**
方法实现的不同,其他方法都继承自Sync
1 ReentrantLock源码
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer{...}
//非公平同步器
static final class NonfairSync extends Sync{...}
//公平同步器
static final class FairSync extends Sync{...}
//ReentrantLock默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//可以指定ReentrantLock为公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//直接调用AQS的acquire方法
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
...
}
- 从上述代码中可以看到ReentrantLock的核心方法均是调用的非公平同步器或公平同步器的方法
2 NonfairSync源码
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
- NonfairSync的方法与Sync的方法基本一致,只是起到了一个包装的作用
3 FairSync源码
static final class FairSync extends Sync {
//公平版本的tryAcquire
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//注意这里与nonfairTryAcquire()方法的区别
//当锁可用时,公平同步器会先检查AQS阻塞队列中是否有节点
//如果AQS没有节点则去竞争锁,否则不参与竞争,直接进入阻塞队列
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
3 Sync源码
abstract static class Sync extends AbstractQueuedSynchronizer {
//tryAcquire()方法在Sync的两个子类中实现,NonefairSync会调用该方法
//该方法表现为不公平策略
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果当前锁无人获得,则将state设置为1
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已经获得了锁,则返回true,即发生了锁重入
else if (current == getExclusiveOwnerThread()) {
//锁重入后令state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
//锁释放时,令state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//当state为0时才释放锁,因为此时当前线程还持有锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
//state大于0,此时并不释放锁,只是将锁重入的计数(state)减1
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
4 锁重入原理
**Sync::nonfairTryAcquire()**
和**Sync::boolean tryRelease()**
```java @ReservedStackAccess final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState();//如果当前锁无人获得,则将state设置为1 if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已经获得了锁,则返回true表示加锁成功,此时发生了锁重入 else if (current == getExclusiveOwnerThread()) {
//锁重入后令state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
} return false; }
@ReservedStackAccess protected final boolean tryRelease(int releases) { //锁释放时,令state— int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false;
//当state为0时才释放锁,因为此时当前线程还持有锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
//state大于0,此时并不释放锁,只是将锁重入的计数(state)减1
return free;
}
- 从上述代码中可以看到,**在加锁和释放锁时通过state的值和exclusiveOwnerThread属性来判断**
- 当获取锁时,若state的值大于0,则说明锁已经被线程获取,又判断持有锁的线程和当前线程是否相等,若相等,则**加锁成功,state的值加1**
- 当**释放锁时,state的值减1**,**若此时state的值为0,则释放锁成功并返回true**,否则返回false
<a name="O0fAk"></a>
#### 4 非公平锁原理
- **当ReentrantLock初始化为非公平锁时,使用的同步器是NonfairSync,此时ReentrantLock的属性Sync指向NonfairSync对象**
<br />
- `**ReentrantLock::lock()**`
```java
public void lock() {
sync.acquire(1);
}
- 可以看到调用lock时实际调用的是
AQS::acquire()
方法,而acquire()
方法又会调用NonfairSync::tryAcquire()
方法,而tryAcquire()
方法直接调用Sync::nonfairTryAcquire()
**Sync::nonfairTryAcquire()**
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果当前锁无人获得,则将state设置为1
//由于这里直接尝试使用CAS操作去竞争锁,不检查AQS阻塞队列,因此是非公平的策略
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已经获得了锁,则返回true,即发生了锁重入
else if (current == getExclusiveOwnerThread()) {
//锁重入后令state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 从上述代码可以看到,任何线程在调用
**nonfairTryAcquire()**
方法时都会直接去竞争锁,而不会去检查AQS阻塞队列,这可能导致AQS阻塞队列的线程获取不到锁,最终导致不公平
5 公平锁原理
- 当ReentrantLock初始化为公平锁时,使用的同步器是FairSync,此时ReentrantLock的属性Sync指向FairSync对象
- FairSync与NonfairSync的唯一区别就是
**tryAcquire()**
方法的区别- 对于FairSync来说,新的线程尝试获取锁时,如果发现AQS阻塞队列中已经有线程在等待,则直接放弃竞争,进入阻塞队列
- 具体实现细节见FairSync源码
4 ReentrantReadWriteLock类
1 简介
- ReentrantReadWriteLock(读写锁)概述
- 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用Java提供的关键字synchronized或者JUC包中实现了Lock接口的ReentrantLock。
上述两种锁都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁
- 在一些大部分只是读数据而写数据很少的业务场景中,如果仅仅是读数据的话并不会影响数据正确性(不会出现脏读),而在这种业务场景下依然使用独占锁的话,很显然会影响性能。
- 针对这种读多写少的情况,JUC包还提供了另外一个实现Lock接口的ReentrantReadWriteLock类。
- ReentrantReadWriteLock特点
- 读写锁允许同一时刻被多个读线程访问,但是在写线程访问时所有的读线程和其他的写线程都会被阻塞。
- 读写锁中有两种锁,分别是读锁(ReadLock)和写锁(WriteLock),读锁可以用来保护共享数据的读取,写锁用来保护共享数据的写入,读锁与写锁相互配合可以保证
- 读-读操作并行
- 读-写操作串行
- 写-写操作串行
- 读写锁的其他特性如下
- 公平锁
与ReentrantLock一样,读写锁默认非公平锁,但是也可以指定为公平锁
- **重入性**
与ReentrantLock一样,支持锁的重入
- **条件变量**
读锁不支持条件变量,写锁支持条件变量
- **锁重入时不支持锁升级**
一个线程获取读锁后不能再获取写锁,必须先释放读锁再获取写锁
- **锁重入时支持锁降级**
一个线程获取写锁后,还可以获取读锁
由于在写锁释放前可以获取读锁,因此线程获取读锁后再释放写锁,就完成了写锁到读锁的降级
2 ReentrantReadWriteLock使用实例
现定义一个线程安全的数据容器类DataContainer,其内部使用ReentrantReadWriteLock的读锁保护读取数据的
read()
方法,使用写锁来保护写入数据的write()
方法DataContainer源码
@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
//读写锁
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
//读锁
private final ReentrantReadWriteLock.ReadLock rLock = rw.readLock();
//写锁
private final ReentrantReadWriteLock.WriteLock wLock = rw.writeLock();
//读取操作使用读锁保护
public Object read() {
log.debug("获取读锁...");
rLock.lock();
try {
log.debug("读取");
Thread.sleep(1000);
return data;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("释放读锁...");
rLock.unlock();
}
return null;
}
//写入操作使用写锁保护
public void write() {
log.debug("获取写锁...");
wLock.lock();
try {
log.debug("写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("释放写锁...");
wLock.unlock();
}
}
}
测试
@Slf4j(topic = "c.Test")
public class Test {
static DataContainer dataContainer = new DataContainer();
public static void main(String[] args) throws InterruptedException {
log.debug("读-读并发测试");
readRead();
Thread.sleep(3000); //等待读-读测试完毕
log.debug("读-写并发测试");
readWrite();
}
//读-读测试
static void readRead() {
new Thread(() -> {
dataContainer.read();
}, "r_t1").start();
new Thread(() -> {
dataContainer.read();
}, "r_t2").start();
}
//读-写测试
static void readWrite() throws InterruptedException {
new Thread(() -> {
dataContainer.read();
}, "r_t3").start();
Thread.sleep(100); //等待一下,保证t1获取到读锁
new Thread(() -> {
dataContainer.write();
}, "w_t1").start();
}
}
测试结果
16:53:11 [main] c.Test - 读-读并发测试
16:53:11 [r_t1] c.DataContainer - 获取读锁...
16:53:11 [r_t2] c.DataContainer - 获取读锁...
16:53:11 [r_t1] c.DataContainer - 读取
16:53:11 [r_t2] c.DataContainer - 读取
16:53:12 [r_t2] c.DataContainer - 释放读锁...
16:53:12 [r_t1] c.DataContainer - 释放读锁...
16:53:14 [main] c.Test - 读-写并发测试
16:53:14 [r_t3] c.DataContainer - 获取读锁...
16:53:14 [r_t3] c.DataContainer - 读取
16:53:14 [w_t1] c.DataContainer - 获取写锁...
16:53:15 [r_t3] c.DataContainer - 释放读锁...
16:53:15 [w_t1] c.DataContainer - 写入
16:53:16 [w_t1] c.DataContainer - 释放写锁...
- 从测试结果可以看到
- 读-读操作可以并行
- 读-写操作时,由于已经有线程开始了读操作,执行写操作的线程被阻塞
3 应用 - 实现Redis缓存与数据库一致性
- 应用实现的功能
- 查询数据
查询数据时先看Redis中是否有数据,有的话直接从Redis中查,没有的话则从MySQL中查并将结果存入Redis
- 更新数据
更新数据时需要将数据更新到MySQL并将Redis中的缓存删除
1 非线程安全实现
源码
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
JedisPool jedisPool;
@Override
public User findOne(int id) {
Jedis jedis = jedisPool.getResource();
String key = "person:" + id;
//查询数据时先从Redis缓存中查询
User value = (User) SerializeUtil.unserialize(jedis.get(key.getBytes()));
if (value != null) {
System.out.print("Get from Redis: ");
return value;
}
//Redis中没有查询到,从MySQL中查并将结果存入Redis
value = userMapper.findByCondition(id);
jedis.set(key.getBytes(), SerializeUtil.serialize(value));
System.out.print("Get from MySQL: ");
return value;
}
@Override
public int update(User user) {
Jedis jedis = jedisPool.getResource();
String key = "person:" + user.getId();
//先更新数据库再清理缓存
int rows = userMapper.update(user);
jedis.del(key);
return rows;
}
}
测试 ```java ApplicationContext app = new ClassPathXmlApplicationContext(“applicationContext.xml”); UserService userService = (UserService) app.getBean(“userService”);
System.out.println(“==========> 查询”); System.out.println(userService.findOne(1)); System.out.println(userService.findOne(1)); System.out.println(userService.findOne(1));
System.out.println(“==========> 更新”); User user = new User(1, “常桐华”, “520”); System.out.println(userService.update(user)); System.out.println(userService.findOne(1));
- 上述代码测试结果如下
```java
==========> 查询
14:47:59,308 INFO DruidDataSource:655 - {dataSource-1} inited
Get from MySQL: User{id=1, username='崔奕宸', password='123'}
Get from Redis: User{id=1, username='崔奕宸', password='123'}
Get from Redis: User{id=1, username='崔奕宸', password='123'}
==========> 更新
1
Get from MySQL: User{id=1, username='常桐华', password='520'}
上述代码中存在三个问题
- 对于Redis的操作是非线程安全的
findOne()
方法中,当多个线程同时首次查询同一个数据,多个线程可能都会到MySQL中去查询,导致缓存穿透update()
方法涉及两个重要操作:1. 删除缓存内容 2. 更新数据库,两个操作的顺序不同(即更新策略的不同)会导致不同的线程安全问题,分析如下
- 更新策略1:先删缓存再更新数据库
- 如上图所示的执行顺序,该更新策略可能导致某些线程一直查询到旧值而查询不到新值
- 更新策略2:先更新数据库再删缓存
- 如上图所示的执行顺序,该更新策略可能导致少量的线程查询到旧值,但是该状况不会持续
- 对于不需要严格线程安全的场景,该更新策略比较适合
2 线程安全实现
- 对于上述出现的三个问题,可以通过ReentrantReadWriteLock来解决
- 对MySQL或Redis的写操作使用写锁来保护
- 对MySQL或Redis的读操作使用读锁来保护
源码如下
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
JedisPool jedisPool;
@Autowired
ReentrantReadWriteLock lock;
@Override
public User findOne(int id) {
Jedis jedis = jedisPool.getResource();
String key = "person:" + id;
//需要读取Redis,加读锁保护
lock.readLock().lock();
User value;
try {
//查询数据时先从Redis缓存中查询
value = (User) SerializeUtil.unserialize(jedis.get(key.getBytes()));
if (value != null) {
System.out.print("Get from Redis: ");
return value;
}
} finally {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
//可能多个线程试图查询数据库,因此这里需要双重检查防止缓存穿透
value = (User) SerializeUtil.unserialize(jedis.get(key.getBytes()));
if (value != null) {
System.out.print("Get from Redis: ");
} else {
//Redis中没有查询到,从MySQL中查并将结果存入Redis
value = userMapper.findByCondition(id);
jedis.set(key.getBytes(), SerializeUtil.serialize(value));
System.out.print("Get from MySQL: ");
}
return value;
} finally {
lock.writeLock().unlock();
}
}
@Override
public int update(User user) {
Jedis jedis = jedisPool.getResource();
String key = "person:" + user.getId();
//需要写入数据库,加写锁保护
lock.writeLock().lock();
//先更新数据库再清理缓存
try {
int rows = userMapper.update(user);
jedis.del(key);
return rows;
} finally {
lock.writeLock().unlock();
}
}
}
5 LockSupport类
- LockSupport类概述
- LockSupport是java.util.concurrent.locks包下的类
- LockSupprot提供了线程的阻塞原语,用来阻塞线程和唤醒线程
- AQS中阻塞队列底层使用LockSupport来阻塞和唤醒线程
由于LockSupport阻塞线程时,具有超时阻塞、可中断阻塞的特性,因此使用AQS的锁也具有超时阻塞、可中断阻塞的特性
1 LockSupport类API
- LockSupport类中的方法都是静态方法
**void LockSupport.park()**
阻塞当前线程,使线程进入WAITING状态,可以通过中断或者unpark()
方法返回
**void LockSupport.parkNanos(long nanos)**
阻塞当前线程,使线程进入TIMED_WAITING状态,最长不超过nanos纳秒
**void LockSupport.parkUntil(long deadline)**
阻塞当前线程,使线程进入TIMED_WAITING状态,直到deadline
**void LockSupport.unpark(Thread thread)**
唤醒线程thread,该方法可以提前唤醒thread,即如果先调用**unpark(thread)**
,thread再调用**park()**
,thread仍然不会被阻塞,因为被提前唤醒了
2 park()/unpark()实现原理
- 每个线程都会与一个许可证关联
许可证实际上是Threadde native实现中的一个成员,代表线程是否可以阻塞的许可permit
当线程调用**park()**
方法时,会检查许可证是否可用
- 如果不可用线程就阻塞
- 否则线程不阻塞,然后把许可证设置为不可用并返回
当线程调用**unpark()**
方法时,会将许可证设置为可用
- 注意只有一个许可证,因此多次连续调用
unpark()
方法和调用一次unpark()
方法的效果相同
- 实际上LockSupport阻塞和唤醒线程的功能是依赖于sun.misc.Unsafe类,其这是一个用C++编写的很底层的类
3 park()/unpark()和wait()/notify()的区别
**wait()**
/**notify()**
方法必须配合Monitor一起使用
**park()**
/**unpark()**
可以在任何地方使用
**park()**
/**unpark()**
使用时没有先后顺序,都可以使线程不阻塞
**wait()**
必须在**notify()**
前先使用,如果先**notify()**
再**wait()**
,则线程会一直等待
**notify()**
只能随机释放一个线程,并不能指定某个特定线程,**notifyAll()**
是释放锁对象中的所有线程
**unpark()**
可以唤醒指定的线程
- 调用
**wait()**
方法会使当前线程释放锁资源,但使用的前提是必须已经获得了锁
**park()**
不会释放锁(因为LockSupport与Monitor无关)
4 park()与中断
- 当中断标志位true时,线程调用
**park()**
不会被阻塞
park()
的伪代码如下
park() {
if(permit > 0) {
permit = 0;
return;
}
if(中断状态 == true) {
return;
}
阻塞当前线程; // 将来会从这里被唤醒
if(permit > 0) {
permit = 0;
}
}
- 还可以发现
park()
方法并不会清除中断标志位,因此只要中断状态为true,线程就不会被**park()**
阻塞
中断线程时通过调用
**unpark()**
来唤醒线程- 被中断时,
park()
方法并不会抛出异常而只是被唤醒,这样就可以由线程自己决定如何处理中断 - 在调用
thread.interrupt()
时,该方法还会调用一次thread.unpark()
,从而解除thread的阻塞
- 被中断时,
实例 ```java Thread t1 = new Thread(() -> { log.debug(“线程第一次阻塞”); LockSupport.park(); log.debug(“线程被唤醒”);
log.debug(“线程第二次阻塞”); LockSupport.park(); log.debug(“阻塞失败”); }, “t1”); t1.start();
Thread.sleep(1000); log.debug(“中断t1”); t1.interrupt();
上述代码输出
```java
17:28:18 [t1] c.Test - 线程第一次阻塞
17:28:19 [main] c.Test - 中断t1
17:28:19 [t1] c.Test - 线程被唤醒
17:28:19 [t1] c.Test - 线程第二次阻塞
17:28:19 [t1] c.Test - 阻塞失败
3 atomic包
- 原子类概述
- 在并发编程中很容易出现并发安全的问题,比如多个线程执行变量更新操作i++,就有可能获取不到正确的值,而针对这个问题,最常用的方法是通过synchronized加锁来达到线程安全的目的。由于synchronized采用的是悲观锁策略,并不是特别高效的一种解决方案。
- 实际上,在JUC包下的atomic包提供了一系列的操作简单、性能高效,并能保证线程安全的类去更新基本类型变量、数组元素、引用类型以及对象中的字段。
- atomic包下的这些原子类都是采用的都是乐观锁策略去更新数据,其内部使用CAS操作具体实现。
1 原子更新基本类型
- atomic包提供的原子更新基本类型的类主要有以下三个
- AtomicBoolean
以原子更新的方式更新boolean类型数据
- AtomicInteger
以原子更新的方式更新int类型数据
- AtomicLong
以原子更新的方式更新long类型数据
上述三个类的用法基本一致,以AtomicInteger为例进行分析
2 AtomicInteger类API
构造方法
**AtomicInteger()**
**AtomicInteger(int initialValue)**
构造一个AtomicInteger对象,可以传入一个int类型参数作为AtomicInteger的值
实例方法
**boolean atomicInteger.compareAndSet(int expectedValue, int newValue)**
以原子的方式尝试将对象中的值更新为newValue
- 如果对象中的旧值与expectedValue不相等,则更新失败并返回false
- 如果对象中的旧值与expectedValue相等,则更新成功并返回true
- 除了该方法,其他方法基本都是不断尝试操作,直到操作成功,并且不需要提供预期值
**int atomicInteger.incrementAndGet() **
**int atomicInteger.getAndIncrement()**
以原子的方式将对象中的值进行加1操作,并返回相加后的结果或旧值
- 功能上等同于
**++i**
和**i++**
**int atomicInteger.addAndGet(int delta)**
**int atomicInteger.getAndAdd(int delta)**
以原子的方式将对象中的值加上传入的delta,并返回相加后的结果或旧值
**int atomicInteger.getAndSet(int newValue)**
将对象中的值更新为新值,并返回旧值
**int atomicInteger.updateAndGet(IntUnaryOperator updateFunction)**
**int atomicInteger.getAndUpdate(IntUnaryOperator updateFunction)**
以原子的方式更新值,并返回更新后的结果或旧值
该方法的参数是一个函数式接口,其内有一个控制具体更新操作的方法,因此我们可以自定义如何更新值
@FunctionalInterface
public interface IntUnaryOperator {
int applyAsInt(int operand);
}
方法源码
public final int updateAndGet(IntUnaryOperator updateFunction) {
//获取旧值
int prev = get();
//创建新值
int next = 0;
for (boolean haveNext = false;;) {
if (!haveNext)
//根据函数式接口中定义的方法计算新值
next = updateFunction.applyAsInt(prev);
//类似于compareAndSet方法,传入预期值和新值,如果设置成功则返回新值,否则重试
if (weakCompareAndSetVolatile(prev, next))
return next;
haveNext = (prev == (prev = get()));
}
}
实例
AtomicInteger i = new AtomicInteger(5);
i.updateAndGet((val) -> val * 10);
3 原子更新引用
- atomic包提供的原子更新引用类型的类主要有以下三个
注意,更新的是引用本身,即引用不同的实例
- AtomicReference
原子更新引用类型
- AtomicStampedReference
原子更新带有戳的引用类型,与AtomicReference
- AtomicMarkableReference
原子更新带有标记位的引用类型,与AtomicReference
上述三个类的用法基本一致,而且与AtomicInteger类也是比较类似的
通过下述实例即可了解AtomicReference的使用 ```java interface DecimalAccount { //获取余额 BigDecimal getBalance();
//取款 void withdraw(BigDecimal amount);
/**
- 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
- 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List
ts = new ArrayList<>(); for (int i = 0; i < 1000; i++) {
} ts.forEach(Thread::start); ts.forEach(t -> {ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}); System.out.println(account.getBalance()); } }try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
class DecimalAccountSafeCas implements DecimalAccount {
private AtomicReference
public DecimalAccountSafeCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
@Slf4j() public class Test { public static void main(String[] args) { DecimalAccountSafeCas account = new DecimalAccountSafeCas(new BigDecimal(“10000”)); DecimalAccount.demo(account); } }
- **原子引用类型在比较时,使用的是**`**==**`**进行比较,实例如下**
```java
class User {
String name;
User(String str) {
name = str;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
@Slf4j()
public class Test {
public static void main(String[] args) {
User user1 = new User("cyc");
User user2 = new User("cyc");
User user3 = new User("qxj");
AtomicReference<User> atomicReference = new AtomicReference<>(user1);
System.out.println(atomicReference.compareAndSet(user2, user3));
System.out.println(atomicReference.get());
}
}
- 上述代码输出
false
User{name='cyc'}
4 原子更新数组元素
- atomic包下提供能原子更新数组中元素的类主要有以下三个
- AtomicIntegerArray
原子更新整型数组中的元素
- AtomicLongArray
原子更新长整型数组中的元素
- AtomicReferenceArray
原子更新引用类型数组中的元素
这几个类的用法一致,以AtomicInteger为例进行分析
- AtomicIntegerArray类API
该类的方法与AtomicInteger类也是比较类似的,只不过在AtomicIntegerArray的方法中会多一个指定数组索引位的参数i,如
**boolean atomicIntegerArray.compareAndSet(int i, int expectedValue, int newValue)**
尝试使用CAS操作更新数组中索引为i的元素
**int atomicIntegerArray.getAndSet(int i, int newValue)**
以原子更新的方式将数组中索引为i的元素设置为新值并返回旧值
**int atomicIntegerArray.addAndGet(int i, int delta)**
以原子更新的方式将数组中索引为i的元素与delta相加并返回计算结果
**int atomicIntegerArray.getAndIncrement(int i)**
以原子更新的方式将数组中索引为i的元素自增加1并返回旧值
5 原子累加器
原子累加器概述
- atom包提供了两个高性能累加器DoubleAddr和LongAddr,其中LongAddr较为常用,因此以LongAddr为例进行解析
- AtomicLong也提供有累加操作,但是LongAddr的累加性能比AtomicLong好约4-5倍
LongAddr加速累加原理
- AtomicLong的原理是依靠底层的CAS来保障原子性的更新数据,在要添加或者减少的时候,如果出现竞争就会使用死循环不断地CAS直到满足条件,从而达到更新数据的目的
- LongAddr性能提升的原因很简单,就是在有竞争时设置多个累加单元Cell,多个线程累加自己的单元,如Therad-0累加Cell[0]、Thread-1累加Cell[1],最后将结果汇总。
这样多个线程在累加时操作的不同的Cell单元,因此减少了CAS重试失败,从而提高性能。
- LongAddr的累加单元会随着冲突的发生而增加,但是累加单元的个数不会超过CPU核心数
LongAddr源码(待)
4 线程安全集合
1 线程安全集合概述
Java中线程安全集合可以分为三大类
- 遗留的线程安全集合
- Hashtable
- Vector
- 特点
- 集合创建的时间比较早
- 方法由synchronized关键字修饰来保证线程安全性
- 在高并发的情况下,每次只有一个线程能够获取synchronized锁对象,性能较差
- 经过Collenctions方法修饰的线程安全集合
- Collections.synchronizedCollection()
- Collections.synchronizedList()
- Collections.synchronizedMap()
- Collections.synchronizedSet()
- Collections.synchronizedNavigableMap()
- …
- 特点
- Collections中提供了多个可以将线程不安全的类修饰为线程安全类的方法
- 线程安全的实现简单粗暴,使用装饰器模式,在调用所有方法前需要先获取synchronized锁对象
- 在高并发的情况下,每次只有一个线程能够获取synchronized锁对象,性能较差
- JUC包提供的线程安全集合
JUC包下提供的线程安全集合众多,但是可以根据它们的前缀将它们分为三大类
- Blocking(如LinkedBlockingQueue、ArrayBlockingQueue)
名称含有Blocking的容器是阻塞队列的实现,这一类实现基于ReentrantLock锁,并提供用来阻塞线程的方法
- CopyOnWrite(如CopyOnWriteArrayList、CopyOnWriteSet)
前缀为CopyOnWrite的容器的实现是通过在修改时拷贝的方式来保证线程安全,适用于读多写少的场景,写操作的开销较重
- Concurrent(如ConcurrentHashMap、ConcurrentLinkedQueue)
- 前缀为Concurrent的容器一般性能都比较高
- 内部采用CAS操作优化锁并采用多把锁(锁的粒度比较细),提高并发度和吞吐量
- 性能较高的同时也存在弊端,即弱一致性。Concurrent容器的弱一致性有以下表现
- 遍历时弱一致性,即当利用迭代器遍历容器时,如果容器的内容发生修改,迭代器仍然可以继续遍历,并且遍历到的值仍然是旧的
注意:对于非线程安全的集合来说,在迭代时如果集合被其他线程修改,则抛出异常,不再继续遍历
- 获取容器大小时弱一致性,即`**size()**`**方法返回的结果未必正确**
- 读取弱一致性,即**可能出现脏读**
1 ConcurrentHashMap
1 简介
- HashMap和Hashtable的弊端
HashMap
- HashMap是非线程安全的
- 在JDK8之前HashMap在出现冲突时采用头插法,若在多线程情况下扩容可能会出现CPU使用率接近100%的情况,导致机器卡死,此时产生了并发死链问题
- 在JDK8之后,由于改变了冲突处理的方法(改成了尾插法),同时还改变了计算hash值的方法,因此不会出现并发死链问题,但仍不意味着线程安全,还可能出现丢数据等问题
Hashtable
- 为了保证线程安全,我们可以使用古老的Hashtable类,该类基本上所有的方法都采用synchronized进行线程安全的控制。
- 使用Hashtable时,在高并发情况下每次只有一个线程能够获取synchronized锁对象,这样的并发性能很差
- ConcurrentHashMap概述
- ConcurrentHashMap是JUC包提供的高性能线程安全的HashMap实现
ConcurrentHashMap和HashMap一样都实现了Map接口,因此其API与HashMap基本一致
- ConcurrentHashMap大量使用了synchronized关键字以及CAS操作以保证操作的线程安全性
- 底层数据结构采用数组+链表+红黑树的数据形式
- 为什么ConcurrentHashMap不用ReentrantLock而是Synchronzied呢?
实际上,synchronzied做了很多的优化,包括偏向锁、轻量级锁、锁自旋等,可以依次向上升级锁状态,因此synchronized和ReentrantLock在性能上已相差无几。
又因为ConcurrentHashMap没有用到ReentrantLock独有的功能,因此多采用简单的synchronized
2 重要属性和内部类
// 控制hash表的初始化和大小调整的标志
// -1: 表示hash表正在初始化
// -(1+调整表大小的线程数量): 表示hash表正在扩容
// 0: ConcurrentHashMap刚构造时的值,此时hash表为null
// 正数值:表示hash表已经初始化(实际可能还没初始化,因为是懒惰初始化)或扩容完毕,
// 值为hash表下一次扩容的阈值
private transient volatile int sizeCtl;
// HashMap实际的底层数据结构,即hash表
// hash表容量总是2的幂,在第一次插入数据时才初始化(懒惰初始化)
// 使用volatile修饰,因此在操作中会使用CAS操作来保证其线程安全性
transient volatile Node<K,V>[] table;
// hash表中存储的元素,表示链表中的节点,其内保存一对键值、hash值、下一个Node的引用
// 键值对即是HashMap中的键和值,hash值是核心,用于快速定位Node
// Node的hash值为正数
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
...
}
// hash表扩容时:新的hash表
private transient volatile Node<K,V>[] nextTable;
/* ---------------- 特殊的Nod -------------- */
// hash表扩容时:放置在原hash表中,标志该位置的链表已经搬迁到新hash表中
// 在将原hash表中的链表搬迁到新hash表时,每搬迁完成一个hash表的位置(或该位置没有链表),
// 便放置一个ForwardingNode
// 在搬迁时如果其他线程调用get()方法并且在hash表相应位置发现是一个ForwardingNode,则
// 该线程便知道应该去新的hash表中去获取数据
// ForwardingNode的hash值为-1,next为null
static final class ForwardingNode<K,V> extends Node<K,V> {...}
// 在compute和computeIfAbsent时用来占位,计算完成后换为普通的Node
static final class ReservationNode<K,V> extends Node<K,V> {...}
// 红黑树(TreeBin)相关的Node
// 当链表长度超过8时,若hash表容量不足64则对hash表扩容,否则将链表转为红黑树
// 当红黑树中的节点小于6时,红黑树又会转换回链表
// 作为红黑树的头节点,存储root和first
// TreeBin的hash值为-2,next为null
static final class TreeBin<K,V> extends Node<K,V> {...}
// 作为红黑树中的普通节点,存储parent、left、right
static final class TreeNode<K,V> extends Node<K,V> {...}
3 实现流程分析
- ConcurrentHashMap底层数据结构
ConcurrentHashMap底层使用【Node数组 + 链表(Node)或 红黑树(TreeNode)】实现
以下数组简称(table),链表简称(bin)
- 初始化table - CAS
使用CAS创建table来保证并发安全,懒惰初始化
- 链表树化/table扩容 - synchronize
- 当bin.length > 8且table.length < 64时,尝试扩容table
扩容时以bin为单位进行(逐个将bin从老table搬到新table),需要使用synchronized锁住bin,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它bin进行扩容,扩容时平均只有1/6的节点会被复制到新table中
- 当table.length超过64时,会将链表树化,树化过程会用synchronized锁住链表头
- put - CAS/synchronize
- 如果该bin尚未创建,只需要使用CAS创建bin;
- 如果已经有了,synchronized锁住链表头进行后续put操作,元素添加至bin的尾部
- get
无锁操作,仅需要保证可见性
table扩容过程中如果get操作拿到的是ForwardingNode,它会让get操作在新table 进行搜索
- size(算法思想与LongAddr相同)
元素个数保存在baseCount中,并发时的个数变动保存在CounterCell[]当中,最后统计size时累加即可
可以发现线程安全措施主要在以下两个方面
- 创建
ConcurrentHashMap有两种创建操作,分别是创建hash表和创建链表头节点
创建操作均使用CAS来保证原子性
- 更改
ConcurrentHashMap有三种更改操作,分别是向链表中加入新节点、修改链表为红黑树、hash表扩容
上述更改操作均使用synchronized关键字锁住头节点的方式来保证线程安全性
4 源码分析
1 重要工具方法
// 获取hash表中第i个链表的链表头
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// 使用CAS操作修改hash表中第i个链表的链表头的值,c为旧值,v为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改hash表中第i个链表的链表头的值,v为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
2 构造器源码分析
ConcurrentHashMap有多种重载的构造器,以参数最全的构造器为例
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
// initialCapacity:ConcurrentHashMap的初始容量
// loadFactor: 负载因子,即hash表扩容时的阈值,默认为0.75
// concurrencyLevel: 并发度
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 当初始容量小于并发度时,将初始容量改为并发度大小
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
// cap是hash表实际的初始容量,需要对指定的初始容量进行两步处理
// size = 初始容量除以负载因子并向上取整
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// cap = size向上取2的幂,如size=11,则cap=16
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
// 将hash表下一次大小存入sizeCtl
this.sizeCtl = cap;
}
- 可以看到hash表实现了懒惰初始化,在构造方法中仅仅计算了table的大小
3 get()源码分析
public V get(Object key) {
Node<K,V>[] tab;
Node<K,V> e, p;
int n, eh;
K ek;
// spread()方法类似于HashMap中的扰动方法hash()
// h为key对应的hash值
int h = spread(key.hashCode());
// 当hash表不为空且其中有元素,则调用tabAt()返回hash表(n - 1) & h位置的链表头节点e
// (n - 1) & h相当于取模运算,比取模运算效率更高
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头节点就是要找的key,直接返回头节点的val
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 当头节点的hash值为负数,表示hash表当前位置的节点为ForwardingNode或TreeBin
// 此时需要调用它们的find()方法来查找对应的节点
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//遍历链表来查找目标Node
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- 从源码中可以看到整个get()方法中没有使用任何线程安全措施
- ConcurrentHashMap与HashMap的
get()
方法几乎一样,但是HashMap中没有ForwardingNode,而ConcurrentHashMap的get()
方法可能调用ForwardingNode的find()
方法
4 put()源码分析
put()
方法的线程安全机制可以使多个线程并行写入hash表的不同位置 ```java public V put(K key, V value) { return putVal(key, value, false); }
final V putVal(K key, V value, boolean onlyIfAbsent) { // onlyIfAbsent: 表示当key已经存在时,是否用新值覆盖旧值
// HashMap允许有空的key或value,而ConcurrentHashMap不允许有空的key或value
if (key == null || value == null) throw new NullPointerException();
// 获取key的hash值
int hash = spread(key.hashCode());
// 链表节点计数(链表长度)
int binCount = 0;
// 进入死循环
for (Node<K,V>[] tab = table;;) {
// f是链表头节点,fh是头节点的hash值,fv是头节点的val
// i是链表在hash表的下标,n是hash表的长度
Node<K,V> f; int n, i, fh; K fk; V fv;
// 当hash表还没有创建,调用initTable()创建hash表
if (tab == null || (n = tab.length) == 0)
// initTable()方法使用CAS操作保证不会有多个线程同时创建hash表
tab = initTable();
// 当链表头节点为null,创建头节点并使用CAS操作将头节点置于hash表相应位置
// 如果成功则退出循环
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// 当头节点是ForwardingNode,则帮忙扩容(帮助搬迁链表)
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
// 进入到当前else,说明发生了hash值冲突
V oldVal = null;
// 锁住链表头节点(锁的不是整个hash表,锁的粒度很细)
synchronized (f) {
// 再次确认一下hash表当前位置的头节点没有被移动过
if (tabAt(tab, i) == f) {
// 如果头节点的hash值大于0,说明是普通节点(是一个链表)
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同的key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 用新的value覆盖老的value
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 到最后一个节点了,仍然没找到
if ((e = e.next) == null) {
// 将当前的key和value追加到链表中
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
// 当头节点的hash值小于0,说明是红黑树头节点(或ReservationNode)
else if (f instanceof TreeBin) {
Node<K,V> p;
// 红黑树的binCount固定为2
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
// 释放锁
}
// 当binCount!=0,说明之前发生了hash冲突
if (binCount != 0) {
// 检查链表长度是否超过阈值(默认为8),如果超过阈值则需要将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
// treeifyBin()不一定直接将链表转为红黑树,可能会先尝试扩容hash表
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 基于LongAddr累加器,增加size的计数
addCount(1L, binCount);
return null;
}
<a name="BCFTG"></a>
#### 5 initTable()源码分析
```java
// 该方法需要保证多个线程调用该方法时,只有一个线程可以成功创建hash表
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 进入循环,条件是hash表还没有被创建
while ((tab = table) == null || tab.length == 0) {
// 当发现sizeCtl变为了负数,说明有其他线程在创建hash表,则让出当前CPU时间片
// 该操作是防止没有执行创建hash表操作的线程在while循环中长期占用CPU
if ((sc = sizeCtl) < 0)
Thread.yield();
// 尝试使用CAS操作将sizeCtl的值改为-1,表示正在初始化hash表
// 此过程只可能有一个线程进入else if代码块执行,其他线程执行失败将进入下一轮while循环
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl值大于0,则hash表的容量为sizeCtl,否则为默认值
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 创建容量为n的Node数组并赋给hash表
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 重新计算sizeCtl的值,表示下一次扩容时hash表的容量
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
6 addCount()源码解析
- put()方法会调用该方法来累加size,由于可能有多个线程累加size,因此该方法内有LongAddr的思想
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell c; long v; int m;
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
2 LinkedBlockingQueue
1 简介
- LinkedBlockingQueue底层数据结构
顾名思义,LinkedBlockingQueue是底层是链表的阻塞队列
- LinkedBlockingQueue的大小
LinkedBlockingQueue构造的时候若没有指定大小,则默认大小为Integer.MAX_VALUE,当然也可以在构造函数的参数中指定大小
- LinkedBlockingQueue三种添加元素方法
三种方法都是向阻塞队列队尾添加元素,不同的是当队列满时三种方法有不同的处理方式
**boolean add(E e)**
队列满时抛出IllegalStateException异常
**boolean offer(E e)**
队列满时返回false
**void put(E e) throws InterruptedException**
队列满时将会进入Condition对象notFull中等待
- LinkedBlockingQueue三种取元素方法
三种方法都是从阻塞队列队头取元素,不同的是当队列为空时三种方法有不同的处理方式
**E remove()**
队列空时抛出NoSuchElementException异常
**E poll()**
队列空时返回null
**E take() throws InterruptedException**
队列空时进入Condition对象notEmpty中等待
2 重要属性和内部类
// 链表中的节点,其中包含节点关联的对象item以及指向下一个节点的指针
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 阻塞队列中元素的个数
private final AtomicInteger count = new AtomicInteger();
// 链表的头节点,头节点不关联对象(head.item == null)
transient Node<E> head;
// 链表的尾节点
private transient Node<E> last;
// 保护从队列取元素操作的锁
private final ReentrantLock takeLock = new ReentrantLock();
// 等待取元素的等待队列,当队列为空时,想要取元素的线程将进入该Condition等待
private final Condition notEmpty = takeLock.newCondition();
// 保护向阻塞队列放元素操作的锁
private final ReentrantLock putLock = new ReentrantLock();
// 等待存元素的等待队列,当队列满时,想要存元素的线程将进入该Condition等待
private final Condition notFull = putLock.newCondition();
3 实现流程分析
- 初始化链表
last = head = new Node<E>(null)
Dummy节点用来占位,item为null
- 当一个节点入队
last = last.next = node
再来一个节点入队,last = last.next = node
- 出队
h = head
first = h.next
h.next = h
,这样做用于帮助GChead = first
,临时变量first和h没有用了E x = first.item;``first.item = null;``return x;
,此时first指向的节点成为Dummy
4 加锁分析
- LinkedBlockingQueue保证线程安全时的巧妙之处
LinkedBlockingQueue保证线程安全且尽量保持性能的核心在于使用了两把锁和Dummy节点
- 如果使用一把锁,同一时刻最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行,此时消费者与消费者线程仍然串行;生产者与生产者线程仍然串行
- Dummy节点的引入是让两把锁保护不同的对象,避免竞争
- LinkedBlockingQueue两把锁保护行为分析
- 当链表节点总数大于等于2时(包括Dummy节点),putLock保证的是last节点相关操作的线程安全,takeLock保证的是head节点相关操作的线程安全,两把锁合作保证了入队和出队的线程安全
- 当节点总数等于1时(就一个 dummy 节点),两把锁保护的是同一个对象,但是此时取元素的线程会被notEmpty条件阻塞,因此也不存在竞争
5 源码分析
1 put()源码分析
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final int c;
// 将放入队列的元素e包装为Node
final Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// 维护队列中元素的个数
final AtomicInteger count = this.count;
// 操作尾结点前先上锁,上锁时可被打断
putLock.lockInterruptibly();
try {
// 判断队列是否已满,满了就进入notFull等待
while (count.get() == capacity) {
notFull.await();
}
// 入队,就是将node加入队列尾部并让last指向node
enqueue(node);
// 元素个数增1
c = count.getAndIncrement();
// 如果队列中还有空位,叫醒其他put()线程
// 这一操作的原因是消费者在消费后使用的并不是singleAll()去叫醒所有生产者
// 因此每一个生产者在生产后还需要负责唤醒另一个生产者
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中有一个元素,唤醒一个消费者线程
if (c == 0)
// 这里使用signal()而不是signalALL()是为了减少竞争
signalNotEmpty();
}
2 take()源码分析
- 与
put()
方法的思路基本一致,只不过是反过来的public E take() throws InterruptedException {
final E x;
final int c;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
// 出队方法
x = dequeue();
c = count.getAndDecrement();
// 消费者唤醒另一个消费者
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时,唤醒生产者线程
if (c == capacity)
signalNotFull();
return x;
}
6 LinkedBlockingQueue与ArrayBlockingQueue的对比
- Linked支持有界,Array强制有界
- Linked底层实现是链表,Array底层实现是数组
- Linked节点的创建是懒惰的,而Array需要提前初始化Node数组
因此LinkedBlockingQueue比较节省内存
- Linked每次入队会生成新Node,而Array的Node是提前创建好的
- Linked两把锁,Array一把锁(因为只有一个数组,因此只能一把锁)
因此LinkedBlockingQueue的性能是优于ArrayBlockingQueue
7 LinkedBlockingQueue与ConcurrentLinkedQueue的对比
ConcurrentLinkedQueue的设计与LinkedBlockingQueue非常像,也是
- 两把“锁”,同一时刻可以允许两个线程同时(一个生产者与一个消费者)执行
- 引入了Dummy节点
不同的是,ConcurrentLinkedQueue的“锁”实际上是使用CAS来实现的,无阻塞
3 CopyOnWriteArrayList
CopyOnWriteArrayList概述
- CopyOnWriteArrayList底层实现采用了写入时拷贝的思想,即增删改操作时会先将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,实现读写分离
- 读写锁只能做到读-读并发,而CopyOnWriteArrayList可以做到读-写并发
- CopyOnWriteArrayList适合读多写少的场景
CopyOnWriteArrayList的弱一致性
- CopyOnWriteArrayList写入时拷贝的思想难以避免弱一致性
- 并发高和一致性是矛盾的,需要权衡