前言
JDK 1.5中提供的锁的接口java.util.concurrent.locks.Lock,其提供了一些ReentrantLock, ReentrantReadWriteLock实现类。
参考JDK文档:Java Platform SE 6
目录
Condition与Object中的wati,notify,notifyAll区别:
Lock接口
Lock实现提供了比使用Synchronized方法和语句更广泛的搜定操作,此操作允许更灵活的结构,可以具有很大的属性,可以支持多个相关的Condition对象
Lock接口的提供的方法有:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
//todo
//加锁 未抢到锁的阻塞下来,直到抢到锁才会执行后续的逻辑
reentrantLock.lock();
try {
//可中断的加锁
reentrantLock.lockInterruptibly();
//尝试性加锁 如果加锁成功 返回true 加锁失败会返回false 立即返回
reentrantLock.tryLock(1000,TimeUnit.MILLISECONDS);
reentrantLock.tryLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
//todo
//释放锁
reentrantLock.unlock();
Lock对比Synchronized加锁操作
- Lock实现类的锁必须是显性的调用加锁、释放锁,且必须成对出现
- 加锁操作可以滴方法内部的核心代码片段,加锁粒度更细,意味着并发性更高
- 可以通过lockInterruptibly方式添加可中断锁
- 可以尝试性加锁,未抢到锁可以立即返回
ReentrantLock
公平性和非公平性
如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是因为它得不到CPU运行的机会。解决饥饿的方案被称之为“公平性”,即所有线程均能公平地获得运行机会。通俗讲,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。
线程饥饿的原因:
- 高优先级的线程会比低优先级的线程优先执行
- 线程被阻塞在一个等待进入同步块的状态
公平性锁:
在ReentrantLock中通过FairSync实现公平性锁,其实现是基于一个队列来实现的。
非公平锁:
NonfairSync:在锁中保持一个获取锁的线程信息,当释放锁之后再次抢锁时,通过比较正在抢锁的线程和队列头的线程,如果是上一次获取锁的线程,那么该线程具有优先执行权。
ReentrantLock实现了公平锁和非公平锁。
public ReentrantLock(boolean fair) //参数是Boolean true:公平性锁 false:非公平性锁
public ReentrantLock () //默认值是false
公平锁与非公平锁的使用示例
在while循环中进行显性加锁lock,通过打印看是哪个线程抢到了cpu的执行机会,然后再释放锁。
public class NonFairAndFairDemo implements Runnable {
private static Integer num = 0;
private ReentrantLock rtl;
public NonFairAndFairDemo(ReentrantLock rtl) {
this.rtl = rtl;
}
@Override
public void run() {
while (true) {
//显性加锁
rtl.lock();
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
rtl.unlock();
}
}
public static void main(String[] args) {
//非公平性锁的实现
ReentrantLock nonFairLock = new ReentrantLock(true);
new Thread(new NonFairAndFairDemo(nonFairLock),"A").start();
new Thread(new NonFairAndFairDemo(nonFairLock),"B").start();
}
}
非公平性锁运行,传入false就是非公平锁,true就是公平锁:
ReentrantLock nonFairLock = new ReentrantLock(false);
由运行结果可以看出两个线程抢到资源后会一直抢占,经过一段时间后才被另一个线程抢到,是不公平的。
公平性锁运行结果:
ReentrantLock nonFairLock = new ReentrantLock(true);
公平性锁机制保证了先来的锁优先级高,抢到运行机会的概率大,它的实现机制是基于队列的,因此如果A线程先到,那么等释放锁了之后A线程就会先运行,反之B先运行,这也就是说他们之间的运行时有序的,交替执行的。
AQS
实现提供了一个双向队列,将同步失败线程加入到双向队列,(Node节点包含线程信息,状态waitState),AQS中包含了state. AQS类与子类层级关系如下:
AbstractQueuedSynchronizer是CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基础,因此AbstractQueuedSynchronizer是Lock/Executor实现的前提。公平锁、不公平锁、Condition、CountDownLatch、Semaphore等实现的基础。
AQS核心字段
AQS里面有三个核心字段:
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;
其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。
AQS中的state:
state=0 表示锁是空闲状态
state>0 表示锁被占用
state<0 表示溢出
head/tail加上CAS操作就构成了一个CHL的FIFO队列,下面是Node节点的属性:
static final class Node {
/** 标记表示节点正在共享模式中等待 */
static final Node SHARED = new Node();
/** 标记表示节点正在独占模式下等待 */
static final Node EXCLUSIVE = null;
/**
* 表示线程已经被取消
* 同步队列中的线程因为超时或中断,需要从同步队列中取消。被取消的节点将不会有任何改变
*/
static final int CANCELLED = 1;
/**
* 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后
* 继节点,使后继节点的线程得以运行
*/
static final int SIGNAL = -1;
/**
* 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法
* 后,该节点将会中等待队列中转移到同步队列中,加入到对同步状态的获取
*/
static final int CONDITION = -2;
/**
* 下一次共享模式同步状态获取将会无条件的被传播下去
*/
static final int PROPAGATE = -3;
/**
* 等待状态,仅接受如下状态中的一个值:
* SIGNAL: -1
* CANCELLED: 1
* CONDITION: -2
* PROPAGATE: -3
* 0: 初始化的值
*
* 对于正常的同步节点,它的初始化值为0,对于条件节点它的初始化的值是CONDITION。它使用
* CAS进行修改。
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 获取同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继节点。如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说
* 节点类型(独占和共享)和等待队列中的后继节点公用同一个字段
*/
Node nextWaiter;
/**
* 如果节点在共享模式下等待则返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 获取前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
volatile int waitStatus:节点的等待状态,一个节点可能位于以下几种状态:
CANCELLED = 1
当前的线程被取消,节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。
SIGNAL = -1
表示当前节点的后继节点包含的线程需要运行,也就是unpark.节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。
CONDITION = -2
当前节点在等待condition,也就是在condition队列中.表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。PROPAGATE=-3当前场景下后续的acquireShared能够得以执行当前节点在sync队列中,等待着获取锁
正常状态,新生的非CONDITION节点都是此状态。
非负值标识节点不需要被通知(唤醒)。
volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。
volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。
volatile Thread thread;节点绑定的线程。
Node nextWaiter;下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。
节点(Node)是构成CHL的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程会构建成一个节点并加入到同步器的尾部。CHL的基本结构如下:
AQS同步器原理
基本的思想是表现为一个同步器,支持下面两个操作:
获取锁:首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。
while(synchronization state does not allow acquire){
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。
update synchronization state;
if(state may permit a blocked thread to acquire)
unlock one or more queued threads;
要支持上面两个操作就必须有下面的条件:
- 原子性操作同步器的状态位
- 阻塞和唤醒线程
- 一个有序的队列
目标明确,要解决的问题也清晰了,那么剩下的就是解决上面三个问题。
- 状态位的原子操作
这里使用一个32位的整数来描述状态位,使用CAS操作来修改状态。事实上这里还有一个64位版本的同步器(AbstractQueuedLongSynchronizer),这里暂且不谈。
- 阻塞和唤醒线程
标准的JAVA API里面是无法挂起(阻塞)一个线程,然后在将来某个时刻再唤醒它的。JDK 1.0的API里面有Thread.suspend和Thread.resume,并且一直延续了下来。但是这些都是过时的API,而且也是不推荐的做法。
在JDK 1.5以后利用JNI在LockSupport类中实现了此特性。
LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object, long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object, long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)
上面的API中park()是在当前线程中调用,导致线程阻塞,带参数的Object是挂起的对象,这样监视的时候就能够知道此线程是因为什么资源而阻塞的。由于park()立即返回,所以通常情况下需要在循环中去检测竞争资源来决定是否进行下一次阻塞。park()返回的原因有三:
- 其他某个线程调用将当前线程作为目标调用 unpark;
- 其他某个线程中断当前线程;
- 该调用不合逻辑地(即毫无理由地)返回。
其实第三条就决定了需要循环检测了,类似于通常写的while(checkCondition()){Thread.sleep(time);}类似的功能。
- 有序队列
在AQS中采用CHL列表来解决有序的队列的问题。AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。对于入队列(enqueue):从数据结构上出发,入列是比较简单的,无非就是当前队列中的尾节点指向新节点,新节点的prev指向队列中的尾节点,然后将同步器的tail节点指向新节点。在AQS中入列的源码如下:
/**
* 为当前线程和给定的模式创建节点并计入到同步队列中
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 创建一个节点
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试添加尾节点,如果失败则调用enq(Node node)方法设置尾节点
Node pred = tail;
// 判断tail节点是否为空,不为空则添加节点到队列中
if (pred != null) {
node.prev = pred;
// CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
/**
* 插入节点到队列中
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
// 死循环 知道将节点插入到队列中为止
for (;;) {
Node t = tail;
// 如果队列为空,则首先添加一个空节点到队列中
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// tail 不为空,则CAS设置尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
从上面源码中我们可以看到,在将节点添加到CHL尾部的时候,使用了一个CAS方法(compareAndSetTail(pred, node)
),这里使用CAS的原因是防止在并发添加尾节点的时候出现线程不安全的问题(即有可能出现遗漏节点的情况)
ReentrantLock实现
公平性锁和非公平性锁父类:Sync
static abstract class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
//非公平获取,公平锁和非公平锁都需要这个方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //state == 0表示无锁
//CAS确保即使有多个线程竞争锁也是安全的
if (compareAndSetState(0, acquires)) { //加锁成功
//当前哪一个线程获取到锁,将线程信息记录到AQS里面
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;
}
//释放
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//只有锁的持有者才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //锁被释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//当前线程是否持有锁
protected final boolean isHeldExclusively() {
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;
}
//是否上锁,根据state字段可以判断
final boolean isLocked() {
return getState() != 0;
}
}
公平性锁实现:FairSync
final static class FairSync extends Sync {
//见AbstractQueuedSynchronizer.java, 4.2节有
final void lock() {
acquire(1);
}
//公平版本的tryAcquire,除非是递归调用或没有等待者或者是第一个,否则不授予访问
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//是等待队列的第一个等待者
if (isFirst(current) &&
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;
}
}
isFirst的实现,即等待队列为空或者当前线程为等待队列的第一个元素
final boolean isFirst(Thread current) {
Node h, s;
return ((h = head) == null ||
((s = h.next) != null && s.thread == current) ||
fullIsFirst(current));
}
非公平性锁实现:NonfairSync
final static class NonfairSync extends Sync {
// 执行lock,尝试立即闯入,失败就退回常规流程
final void lock() {
if (compareAndSetState(0, 1)) //比较并设置state,成功则表示获取成功
setExclusiveOwnerThread(Thread.currentThread());//锁持有者
else
acquire(1);//获取失败,进入常规流程:acquire会首先调用tryAcquire
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
acquire的实现(AbstractQueuedSynchronizer.java)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
重入锁实现:
- ReentrantLock都是把具体实现委托给内部类(Sync、NonfairSync、FairSync),
- ReentrantLock的重入计数是使用AbstractQueuedSynchronizer的state属性的,state大于0表示锁被占用、等于0表示空闲,小于0则是重入次数太多导致溢出了.
- 可重入锁需要一个重入计数变量,初始值设为0,当成功请求锁时加1,释放锁时减1,当释放锁之后计数为0则真正释放锁;
- 重入锁还必须持有对锁持有者的引用,用以判断是否可以重入;
condition
synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock同样可以实现该功能,但是要借助于Condition对象。
newCondition方法:
public Condition newCondition()
返回用来与此 Lock 实例一起使用的 Condition 实例。
public interface Condition {
//使当前线程进入休眠进行等待
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒因await进入休眠的一个线程
void signal();
//唤醒因await进入休眠的所有线程
void signalAll();
}
使用await、signal、signalAll是必须加锁的,使用重入锁的加锁释放锁await、signal要通知await的线程必须是作用于同一个Condition实例。
eg:ABC三个线程分别打印各自名称,打印结果ABCABC....A线程通知B线程(ab的Condition)、B线程通知C线程(bc的Condition)、C线程通知A线程(ca的Condition)
public class ABCThread implements Runnable {
private ReentrantLock lock;
private Condition sCondition;
private Condition aCondition;
public ABCThread(ReentrantLock lock, Condition sCondition, Condition aCondition) {
this.lock = lock;
this.sCondition = sCondition;
this.aCondition = aCondition;
}
@Override
public void run() {
int i = 0;
while (i < 10) {
//加锁
lock.lock();
try {
//接收到C线程通知继续执行,否则就阻塞
aCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName()+" ");
//通知B线程
sCondition.signal();
i++;
//释放锁
lock.unlock();
}
}
}
ReentrantLock reentrantLock = new ReentrantLock();
Condition ab = reentrantLock.newCondition();
Condition bc = reentrantLock.newCondition();
Condition ca = reentrantLock.newCondition();
new Thread(new ABCThread(reentrantLock,ab,ca),"A").start();
new Thread(new ABCThread(reentrantLock,bc,ab),"B").start();
new Thread(new ABCThread(reentrantLock,ca,bc),"C").start();
reentrantLock.lock();
ca.signal();
reentrantLock.unlock();
Condition与Object中的wati,notify,notifyAll区别:
- Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
- Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。如果采用Object类中的wait(),notify(),notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。