java lock包
由于Synchronized存在不少缺陷,因此jdk1.5之后,提供了Locks包,有ReentrantLock(可重入锁),ReadWriteLock(读写锁,可以获取读锁和写锁)。
简单示例
public class ReentrantLockkTest {
public static void main(String[] args) {
final ResourceReentrantLock res = new ResourceReentrantLock();
//取数据
for(int i=0;i<10;i++){
new Thread(new Runnable() {
public void run() {
res.get();
}
}).start();
}
//放数据
for(int i=0;i<10;i++){
new Thread(new Runnable() {
public void run() {
res.put("jkf");
}
}).start();
}
}
}
//这是关键: 资源,内部通过ReentrantLock对get和put进行限制,如果put可以放在list的尾,get获取头,这就是一个FIFO队列的实现了,当然了这种实现还是有问题的,例如:取的时候不能放,放的时候不能取,
class ResourceReentrantLock{
int index = 0;
private List<String> list = new ArrayList<String>();
private ReentrantLock lock = new ReentrantLock();
public void put(String name){
//消耗时间,防止set一直获取锁
int i=0;
int max = new Random().nextInt(10000);
while(i<max){
i++;
}
lock.lock();
try{
name = name+(index++);
list.add(name);
System.out.println(Thread.currentThread().getName()+" put "+name);
}finally{
lock.unlock();
}
}
public String get(){
//消耗时间,防止get一直获取锁
int i=0;
int max = new Random().nextInt(10000);
while(i<max){
i++;
}
lock.lock();
try{
if(list.size()>0){
System.out.println(Thread.currentThread().getName()+" get "+list.get(0));
return list.remove(0);
}
System.out.println(Thread.currentThread().getName()+" get ");
return "";
}finally{
lock.unlock();
}
}
}
AQS(AbstractQueuedSynchronizer)
查看ReentrantLock源码,就会发现ReentrantLock的核心就是抽象的队列式的同步器(AbstractQueueSynchronizer):主要提供通过队列形式,处理线程的等待、唤醒、顺序等待问题,子类只需要关心什么情况下可以获取锁(加锁)、什么情况下释放锁(解锁),而加锁失败、释放锁之后的处理都不需要关心,统统由AQS来处理。
通过上述分析,就可以明白了AQS的原理,就是处理未获取锁线程的排队等待和已释放锁后唤醒什么线程,因此我们可以写出一个类似的同步器。如果有看过以前的文章,会发现在自旋锁中的CLH锁和MCS锁中,已经实现了类似的AQS,例如:通过队列实现获取锁失败后的线程顺序问题;通过自选处理线程的等待;释放锁之后,某一个自旋状态的线程就可以获取锁了。
MCS锁源码
package lock.review.lock;
import java.util.concurrent.atomic.AtomicReference;
public class MSCLock {
public AtomicReference<MCSNode> tail;//尾指针
public Thread cureentThread;
public ThreadLocal<MCSNode> currentThreadLocal = new ThreadLocal<MCSNode>();
public void lock(){
MCSNode node = new MCSNode(false, null);//当前节点的初始状态:不可以获取锁
MCSNode preNode = tail.getAndSet(node);
if(preNode != null){
// 加锁失败,进入等待队列中
preNode.nextNode = node;//设置前尾节点指向node的nextNode为当前节点,建立显式链表。
currentThreadLocal.set(node);
// 加入等待队列后,线程自选
while(node.lock);
cureentThread = Thread.currentThread();
}else{
// 加锁成功,啥也不说了,直接干
cureentThread = Thread.currentThread();
}
}
public void unlock(){
// 是否可以释放锁
if(currentThread != Thread.currentThread()){
return;
}
// 可以释放,但是如何传递锁
MCSNode node = currentThreadLocal.get();
if(node.nextNode == null){
//当前线程是最后一个线程,为了让锁传递下去,而当前线程也不用一直循环等待,设置tail为null
if(tail.compareAndSet(node, null)){
node.nextNode=null;
return;
}else{
while(node.nextNode != null){
node.nextNode.lock=true;
node.nextNode=null;
}
}
}
}
}
//节点
class MCSNode{
public volatile boolean lock;//true 可以获取锁,false 不可以获取锁,volatile 保证修改的立刻可见
/**当前节点的下一节点,通过nextNode进行显示的链表表示多线程的顺序,
* 同时当线程释放锁时,需要通过nextNode获取下一节点,修改其lock状态
*/
public MCSNode nextNode;
public MCSNode(boolean lock,MCSNode preNode){
this.lock = lock;
this.nextNode = preNode;
}
}
通过上述锁的分析,我们可以直接进入AbstractQueuedSynchronizer查看源码。
AbstractQueuedSynchronizer 源码分析
1. 子类如何告知AQS加锁的结果(成功或者失败)
通过ReentrantLock的lock方法,我们发现如下入口:
通过上述可以看出,如果我们需要自定义一个锁,只需要继承AQS,同时重写方法tryAcquire(),方法中明确什么情况下加锁失败即可,这样就简简单单的自定义锁。
2. 加锁失败如何处理
通过AQS方法,我们细致分析加锁失败后,如何处理失败的线程。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
-
!tryAcquire(arg) :调用子类的方法,加锁是否成功,如果成功,AQS就什么都不做,如果加锁失败,就进入:acquireQueued(addWaiter(Node.EXCLUSIVE,arg))方法。
-
addWaiter(Node.EXCLUSIVE):把当前节点加入等待队列中。
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // 先搞个简单一次的把当前节点加入等待队列中 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 无法简单,就只能循环处理 enq(node); return node; }
Node 通过next(当前节点的下一个节点)、prev(当前节点的前一个节点)指针,串联整个等待队列。通过addWatiter把当前线程的节点加入等待线程节点中,请注意当前节点的waitStatus是0,初始值。
-
acquireQueued()
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 如果前节点是头结点,尝试获取锁 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 再次尝试获取锁失败 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
shouldParkAfterFailedAcquire:当前节点等待前,判断该节点的前一个节点是否可以值得等待:
1 如果前节点取消了,就不能直接在这个前节点等待,因为该节点的被取消了,永远不会获取锁,也就谈不上释放锁之后唤醒下一节点,所以必须找到一个非取消的节点。
2 在寻找前一个非取消节点时,如果节点没有设置状态,顺手设置状态为SIGNAL状态。 -
parkAndCheckInterrupt 当前线程等待,内部调用LockSupport.park方法
/** * Disables the current thread for thread scheduling purposes unless the * permit is available. * * <p>If the permit is available then it is consumed and the call returns * immediately; otherwise * the current thread becomes disabled for thread scheduling * purposes and lies dormant until one of three things happens: * * <ul> * <li>Some other thread invokes {@link #unpark unpark} with the * current thread as the target; or * * <li>Some other thread {@linkplain Thread#interrupt interrupts} * the current thread; or * * <li>The call spuriously (that is, for no reason) returns. * </ul> * * <p>This method does <em>not</em> report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, * for example, the interrupt status of the thread upon return. * * @param blocker the synchronization object responsible for this * thread parking * @since 1.6 */ public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null); }
LockSupport.lock:当前线程阻塞,结束阻塞的方式如下:
1.调用unpark方法,释放该线程的许可,这就是AQS唤醒下一线程使用的方式。
2.该线程被中断,当前线程被中断后,也可以结束阻塞。
3.到期时间,这个在AQS中,没有设置阻塞时间,暂时不会使用这个方法。
通过上述方法,获取锁失败的线程,就正式的阻塞了,知道取消阻塞,注意哦:当线程被打断后,先需要把当前的等待线程结束掉,才能响应打断。
释放锁成功
当判断可以释放锁了,那么AQS会如何做呢?
可以看到AQS中,有release方法,用来处理释放锁之后的事情:激活下一节点。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 加一些判断,防止先调用unlock
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
子类重写tryRelease方法,告诉AQS释放锁是否成功,如果成功,那么剩下的工作就交给AQS。
- unparkSuccessor 把下一节点唤醒
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
// 当前节点的下一节点存在,并且非取消,唤醒它
if (s == null || s.waitStatus > 0) {
s = null;
// 特殊情况:从后往前找一个非取消的。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
上述就是AQS的抽象队列同步器的大致原理,如果我们想自己写个特殊的锁,只需要写个AQS的子类,重写tryAcquire和tryRealse方法,就可以实现我们的目的,忽略获取锁失败和释放锁成功的后续操作。
为什么释放锁时,不按照当前节点的下一个节点直接唤醒,而是从
ReetrantLock 公平锁和非公平锁
通过源码发现,非公平锁的不公平表现在如下两个方面:
-
lock时的不公平
// 非公平锁lock final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // 公平锁lock final void lock() { acquire(1); }
非公平锁进入后,不会通过AQS,先去偷偷获取锁。
-
tryAcquire的非公平
// 非公平锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 啥也不管,先获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return 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 tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 已经有人排队了,那我就不获取锁了,去排队吧
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;
}
tryAcquire:公平锁,当有人排队时,我就直接去排队;非公平锁我才不管又没人排队,我先试试再说。
ReetrantLock的重入
其实在ReetrantLock的公平和非公平源码中,已经有提现了
无论公平锁和非公平锁,tryAcquire都有如下判断:
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
就是判断当前锁的独占线程是否是当前线程,如果是,就state+1