锁 能够防止多个线程同时访问共享资源
在Lock接口出现之前,Java程序主要是靠synchronized关键字实现锁功能的,而JDK5之后,并发包中增加了Lock接口,它提供了与synchronized一样的锁功能。synchronized能干的事情Lock锁都能干,但是Lock锁需要显示的加锁解锁,并且Lock锁增加了可中断获取锁、超时获取锁以及共享锁等synchronized关键字所不具备的同步特性
通常使用Lock锁的形式如下:
Lock lock = new ReentrantLock();
try{
lock.lock();
……
}finally{
//将解锁操作放到finally块中,无论正常或者异常情况都会解锁
lock.unlock();
}
void lock(); //获取锁
void lockInterruptibly()throws InterruptedException; //获取锁的过程能够响应中断
boolean tryLock(); //非阻塞式响应中断能立即返回、获取锁返回true、失败返回false
boolean tryLock(long time,TimeUnit unit); //超时获取锁、在超时内或未中断的情况下能获取锁
//获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释放锁,再次获取到锁才能从等待中返回
Condition newCondition();
void unlock();//释放锁
Lock接口的实现子类:
观察子类ReentrantLock中的所有的方法都是调用了其静态内部类Sync中的方法,而Sync继承了AbstractQueuedSynchronizer(AQS-简称同步器)
同步器是用来 构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态和一个FIFO队列构成同步队列
它的子类必须重写AQS的几个用protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState、setState以及compareAndSetState这三个方法。
子类推荐使用静态内部类来继承AQS实现自己的同步语义,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件使用,同步器即支持独占式获取同步状态,也支持共享式获取同步状态
锁是面向使用者 -> 它定义了使用者与锁交互的接口,隐藏了实现细节
同步器是面向锁的实现者 -> 它简化了锁的实现方式、屏蔽了同步状态的管理、线程的排队、等待和唤醒等底层操作
AQS的设计使用的是模板设计模式
它将一些与状态相关的核心方法开放给子类重写,而后AQS会使用子类重写的关于状态的方法进行线程的排队、阻塞以及唤醒等操作
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
//自定义锁
class Mutex implements Lock{
private Sync sync = new Sync();
//自定义同步器
//子类必须重写AQS的用protected修饰的方法
//独占锁
class Sync extends AbstractQueuedSynchronizer{
//0--> 1 当前线程获得锁
@Override
protected boolean tryAcquire(int arg) {
if(arg != 1){
throw new RuntimeException("arg不为1");
}
//期望是0,想改为1,如果成功返回true
if(compareAndSetState(0,1)){
//此时线程成功获取同步状态
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//1-->0 释放锁
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0){
throw new IllegalMonitorStateException();
}
//将持有线程置空、将状态还原为0
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//判断当前线程是否是持有锁线程
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
//以下这些是继承Lock接口的方法
//但是实际上都是调用了静态内部类Sync的具体实现
//---------------------------------------------------------
@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,time);
}
@Override
public void unlock() {
//模板方法释放锁
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
//---------------------------------------------------------
}
public class Test{
public static void main(String[] args) {
Lock lock = new Mutex();
for(int i = 0 ; i < 10;i++){
Thread thread = new Thread(()->{
try{
lock.lock();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
执行情况:
上面这个例子实现了独占锁的语义,在同一时刻只允许一个线程占有锁。我们在主线程中启动了10个线程,分别睡眠5s。从执行情况我们可以看出,当前Thread-0正在执行并且占有锁时其他线程需要等待获取锁。
从这个例子就可以很清楚的看出来,在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的管理,线程排队等底层实现。
总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理
AQS核心组成:同步队列、独占锁的获取与释放、共享锁的获取与释放、可中断锁、超时锁
这一系列功能的实现依赖于AQS提供的模板方法
独占锁
void acquire(int args);//独占式获取同步状态,如果获取失败插入同步队列进行等待
void acquireInterruptibly(int arg);//在1的基础上,此方法可以在同步队列中响应中断
boolean tryAcquireNanos(int arg,long nanosTimeOut);//在2的基础上增加了超时等待功能,到了预计时间还未获得锁直接返回
boolean tryAcquire(int arg); //获取锁成功返回true、失败返回false
boolean release(int arg); //释放同步状态、该方法会唤醒在同步队列的下一个结点
共享式锁
void acquireShared(int arg);//共享获取同步状态、同一时刻多个线程获取同步状态
void acquireSharedInterruptibly(int arg);//在1的基础上增加响应中断
boolean tryAcquireSharedNanos(int arg,long nanosTimeOut);//在2的基础上增加超时等待
boolean releaseShared(int arg);//共享式释放同步状态
同步队列
当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列
在AQS内部有一个静态内部类Node,这是同步队列中每个具体的结点
AQS同步队列采用带头尾结点的双向链表
结点有以下属性:
volatile int waitStatus; // 节点状态
volatile Node prev; // 当前节点的前驱节点
volatile Node next; // 当前节点的后继节点
volatile Thread thread; // 当前节点所包装的线程对象
Node nextWaiter; // 等待队列中的下一个节点
结点状态:
1. int INITIAL = 0; // 初始状态
2. int CANCELLED = 1; // 当前节点从同步队列中取消
3. int SIGNAL = -1; // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继
节点的线程继续运行。
4. int CONDITION = -2; // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
5. int PROPAGATE = -3; // 表示下一次共享式同步状态获取将会无条件地被传播下去。
以Renntrant的非公平锁举例:
调用lock()方法是获取独占锁
final void lock() {
//CAS成功,将当前线程设置为持有锁线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//CAS失败,调用AQS提供的acquire()方法
acquire(1);
}
lock方法使用CAS来尝试将同步状态改为1,如果成功则将同步状态持有线程置为当前线程。否则将调用AQS提供的acquire()方法
简易版图:
unlock()方法实际调用AQS提供的release()方法
public final boolean release(int arg) {
//如果同步状态释放成功(tryRelease返回true)
if (tryRelease(arg)) {
//获取当前同步队列的头结点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后继结点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//获取当前结点的状态
int ws = node.waitStatus;
//ws == -1 表示后继结点在阻塞状态 唤醒必须将该结点状态改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取头结点的后继结点
Node s = node.next;
//如果没有后继结点 || 后继结点的状态为初始状态
if (s == null || s.waitStatus > 0) {
s = null;
//从尾结点开始找离node结点最近的状态为-1的结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果有后继结点会调用LookSupport.unpark()方法,该方法会唤醒该结点所包装的线程
if (s != null)
LockSupport.unpark(s.thread);
}
release()方法是unlock()方法的具体实现。
首先获取头结点的后继结点,当后继结点不为null时会调用LockSupport.unpark()方法唤醒后继结点包装的线程
我们通过学习源码看看响应中断是怎么实现的?
可响应中断式锁可调用方法lock.lockInterruptibly();而该方法其底层会调用AQS的acquireInterruptibly方法,源码为:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//线程中断直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//线程获取锁失败调用 doAcquireInterruptibly(arg)
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//将线程包装为Node结点插入到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//当前结点的前驱结点为头结点并且成功获取同步状态
//当前结点包装的线程获得锁将该结点设置为头结点。
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//look!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//---------------------------------------
throw new InterruptedException();
//---------------------------------------
}
} finally {
if (failed)
cancelAcquire(node);
}
}
与acquire几乎一样,唯一的区别在于在调用parkAndCheckInterrupt()将当前线程阻塞返回true时即线程阻塞时该线程被中断,代码抛出中断异常
通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果
该方法会在三种情况下才会返回:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果线程被中断抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//根据超时时间和当前时间算出截止时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//当前线程获得锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//重新计算超时时间
nanosTimeout = deadline - System.nanoTime();
//已经超时返回false
if (nanosTimeout <= 0L)
return false;
//没有超时,将当前线程的前驱结点的状态设置为-1
//阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
超时获取锁和可中断获取锁基本一致
区别在于获取锁失败后,增加了一个时间处理,如果当前时间超过截止时间,线程将不再等待直接退出 返回false
ReentrantLock独占式重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。
想支持重入性,就要解决两个问题:
通过上述对独占锁的学习,我们来复习非公平的RenntrantLock加锁是如何实现的?
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//look!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//若被占有,检查占有线程是否是当前线程 如果是状态+1 返回true 表示可以再次获取成功
//每次重新获取都会对同步状态进行加一的操作
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
我们再来观察一下解锁:
protected final boolean tryRelease(int releases) {
//同步状态-1
int c = getState() - releases;
//如果不是持有线程调用tryRelease抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//只有当同步状态为0时,才锁成功释放
free = true;
setExclusiveOwnerThread(null);
}
//锁未被完全释放 返回false 状态-1
setState(c);
return free;
}
ReentrantLock支持两种锁:公平锁和非公平锁。
什么是公平?如果一个锁是公平的,锁的获取顺序就应该符合时间上的顺序即等待时间最长的线程最先获取锁。
然而一般的我们这样使用ReentrantLockLock lock = new ReentrantLock();
,在使用ReentrantLock并没有指定公平与不公平,那我们使用的是什么锁?观察无参构造我们会发现ReentrantLock默认使用的是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
如果想要使用公平锁调用ReentrantLock的有参构造传入true
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
java提供的关键字synchronized或者concurrent包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性,而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
//获取写锁当前的同步状态
int c = getState();
//获取写锁获取的次数
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//当锁已经被读线程获取或者当前线程不是已经获取写锁的线程 返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//当前线程获取写锁,并支持重入
setState(c + acquires);
return true;
}
//写锁未被其他任何线程获取、当前线程获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
其中exclusiveCount(acquires)
的源码为:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1 即EXCLUSIVE_MASK = 0x 0000FFFF
static final int SHARED_SHIFT = 16;
exclusiveCount方法是将同步状态与0x0000FFFF相与,即取同步状态的低16位那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数
同时还有一个方法值得我们注意:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static final int SHARED_SHIFT = 16;
该方法是获取读锁被获取的次数,是将同步状态右移16次,即取同步状态的高16位.我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数
写锁获取逻辑:
当读锁已被读线程获取或者写锁已被其他线程获取,则写线程获取失败
否则,当前同步状态没有被任何读写线程获取
当前线程获取写锁成功并支持重入
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 同步状态减去写状态
int nextc = getState() - releases;
// 当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
// 不为0则更新同步状态
setState(nextc);
return free;
}
同一时刻该锁可以被多个读线程获取也就是一种共享式锁。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
//如果写锁已经被获取 且 获取写锁的线程不是当前线程
//线程获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//当前线程获取读锁
//新增关于读锁的一些功能
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
当写锁被其他线程获取后,读锁获取失败,否则获取成功利用CAS更新同步状态。另外,当前同步状态需要加上SHARED_UNIT( (1 << SHARED_SHIFT) 即0x00010000)
的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时,是靠fullTryAcquireShared方法实现的
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
//读锁释放,将同步状态减去读状态即可
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
void await() throws InterruptedException;//同Object的await()、直到被中断或者唤醒
void awaitUninterruptibly(); //不响应中断、直到被唤醒
boolean await(long time, TimeUnit unit) throws InterruptedException; //同Object.wait(long timeout),多了自定义时间单位 中断、超时、被唤醒
boolean awaitUntil(Date deadline) throws InterruptedException; //支持设置截止时间;
void signal(); //唤醒一个等待在Condition上的线程,将该线程由等待队列转移到同步队列中
void signalAll();//将所有等待在condition上的线程全部转移到同步队列中
创建一个condition对象是通过 lock.newCondition() ,而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类
注意到ConditionObject中有两个成员变量:
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列
Node类复用了在AQS中的Node类,Node类有这样一个属性:Node nextWaiter
等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列。