概述
Java中可以通过加锁,来保证多个线程访问某一个公共资源时,资源的访问安全性。Java提出了两种方式来加锁
- 第一种是我们上文提到的通过关键字synchronized加锁,synchronized底层托管给JVM执行的,并且在java 1.6 以后做了很多优化(偏向锁、自旋、轻量级锁),使用很方便且性能也很好,所以在非必要的情况下,建议使用synchronized做同步操作;
- 第二种是本文将要介绍的通过java.util.concurrent包下的Lock来加锁(lock大量使用CAS+自旋。因此根据CAS特性建议在低锁冲突的情况下使用lock)
AQS
概述
- AQS全称AbstractQueuedSynchronizer,译为抽象队列同步器
- AQS底层数据结构是被volatile修饰state和一个Node双向队列
- Lock下的实现类包括ReentrantLock、ReadLock、WriteLock底层都是基于AQS实现锁资源获取或释放
内部结构
根据源码我们可以知道AQS维护了一个volatile的state和一个CLH(FIFO)双向队列
state 是一个由volatile修饰的int型互斥变量,state= 0表示没有任务线程使用该资源,而state>= 1表示已经有线程正在持有锁资源。CLH队列是由内部类Node来维护的FIFO队列
实现原理
当一个线程获取锁资源时首先会判断state是否等于0(无锁状态),如果是0则把这个state更新为1,此时该锁资源被占用。在这个过程中,如果多个线程同时进行state更新操作,就会导致线程的安全性问题。因此AQS底层采用了CAS机制,来保证互斥变量state更新的原子性。未获得锁的线程通过Unsafe类中的park方法去进行阻塞,把阻塞的线程按照先进先出的原则放到CLH双向链表中,当获得锁的线程释放锁后,会从这个双向链表的头部去唤醒下一个等待的线程再去竞争锁。
公平锁和非公平锁
在竞争锁资源时,公平锁要判断双向链表中是否有阻塞的线程,如果有则需要去排队等待。而非公平锁的处理方式是,不管双向链表中是否有阻塞的线程在排队等待,它都会去尝试修改state变量去竞争锁,这对链表中排队的线程来说是非公平的。
Lock接口
Lock实现类
- JDK8中,除了StampedLock为不可重入锁,其他包括ReentrantLock、ReentrantReadWriteLock以及Synchronized关键字都是可重入锁
- 可重入锁是指一个线程抢占到了互斥锁资源且在锁释放之前可以重复获取该锁资源,只需要记录重入次数state递增1即可
- lock实际上是通过更新AQS中的state来控制锁的持有情况
Lock方法
java
// 尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
// 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
// 尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();
ReentrantLock
- 根据源码可以看到实现锁功能的关键成员变量Sync类型的sync继承AQS
- Sync在ReentrantLock中有两个实现类NonfairSync公平锁类型和FairSync非公平锁类型
- ReentrantLock默认是非公平锁实现,在实例化时可以指定选择公平锁或者非公平锁
ReentrantLock获取锁流程
java
//.lock()调用的是AQS的acquire()
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
//tryAcquire:会尝试通过CAS获取一次锁。
//addWaiter:将当前线程加入双向链表(等待队列)中
//acquireQueued:通过自旋,判断当前队列节点是否可以获取锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//---------------------非公平锁尝试获取锁的过程---------------------
protected final boolean tryAcquire(int acquires) {
// AQS的nonfairTryAcquire()方法
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state
int c = getState();
if (c == 0) {
// 目前没有线程获取锁,通过CAS(乐观锁)去修改state的值
if (compareAndSetState(0, acquires)) {
// 设置持有锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 锁的持有者是当前线程(重入锁)
else if (current == getExclusiveOwnerThread()) {
// state + 1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//---------------------当前线程加入双向链表的过程---------------------
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
// 获取末位节点
Node oldTail = tail;
if (oldTail != null) {
// 当前节点的prev设置为原末位节点
node.setPrevRelaxed(oldTail);
// CAS确保在线程安全的情况下,将当前线程加入到链表的尾部
if (compareAndSetTail(oldTail, node)) {
// 原末位节点的next设置为当前节点
oldTail.next = node;
return node;
}
} else {
// 链表为空则初始化
initializeSyncQueue();
}
}
}
//---------------------首节点自旋过程---------------------
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
// 首节点线程去尝试竞争锁
if (p == head && tryAcquire(arg)) {
// 成功获取到锁,从首节点移出(FIFO)
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
ReentrantLock释放锁流程
释放锁本质就是对AQS中的状态值State进行逐步递减操作
java
//.unlock()调用AQS的release()方法释放锁资源
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// Sync的tryRelease()方法
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 获取状态
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 修改锁的持有者为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
ReentrantReadWriteLock
- ReentrantReadWriteLock读写锁可以分别获取读锁或写锁,即将数据的读写操作分开;
- writeLock():获取写锁
- writeLock().lock():为写锁上锁
- writeLock().unlock():为写锁释放锁
- readLock():获取读锁
- readLock().lock():为读锁上锁
- readLock().unlock():为读锁释放锁
- 读锁使用共享模式,写锁使用独占模式。即不存在写锁时,读锁可以被多个线程同时持有;存在写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁;而当有读锁时,写锁就不能获得
- 适用于读多写少应用场景,如缓存
Condition
概述
- Condition也是一种线程通信的机制,通过await和singalAll()实现线程阻塞和唤醒
- 底层数据结构是复用AQS的Node类,由不带头结点的链表实现的队列
- await实现原理:通过LockSupport.park将当前线程置于Waiting阻塞状态,直到其他线程调用signal或signalAll将等待队列的队头结点移入到同步队列中,使其有机会通过自旋获取到锁
- signal/signalAll:将等待队列的队头结点移入到同步队列中,并通过LockSupport.unpark唤醒该线程
- 与Object的wait/notify机制对比
- Condition支持不响应中断,而object不能
- Lock可以支持多个condition等待队列,object只能支持一个
- Condition能够对await设置超时时间,而object不能
- 可以通过Lock+Condition实现生产者-消费者问题(在后文并发实践篇会有相关示例)
Condition实践
java
package com.bierce;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock + Condition: 实现线程按序打印
* 案例:开启3个线程,id分别为A、B、C,并打印10次,而且按顺序交替打印如:ABCABCABC...
*/
public class TestCondition {
public static void main(String[] args) {
PrintByOrderDemo print = new PrintByOrderDemo();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {// 打印10次
print.loopA(i);
}
},"A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
print.loopB(i);
}
},"B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
print.loopC(i);
}
},"C").start();
}
}
class PrintByOrderDemo{
private int number = 1; //当前正在执行线程的标记
private Lock lock = new ReentrantLock();
private Condition ConditionA = lock.newCondition();
private Condition ConditionB = lock.newCondition();
private Condition ConditionC = lock.newCondition();
public void loopA(int totalLoop){
lock.lock();
try {
if ( number != 1){ //判断当前是否打印A
ConditionA.await();
}
for (int i = 1; i <= 1; i++) {
System.out.print(Thread.currentThread().getName()); //打印A
}
//唤醒其他线程
number = 2;
ConditionB.signal();
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void loopB(int totalLoop){
lock.lock();
try {
if ( number != 2){ //判断当前是否打印B
ConditionB.await();
}
for (int i = 1; i <= 1; i++) {
System.out.print(Thread.currentThread().getName()); //打印B
}
//唤醒其他线程
number = 3;
ConditionC.signal();
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void loopC(int totalLoop){
lock.lock();
try {
if ( number != 3){ //判断当前是否打印C
ConditionC.await();
}
for (int i = 1; i <= 1; i++) {
System.out.print(Thread.currentThread().getName()); //打印C
}
//唤醒其他线程
number = 1;
ConditionA.signal();
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}