java类库的阅读笔记_jdk1.7.0_40_java.util.concurrent.locks.AbstractQueuedSynchronizer

2013 1107:

低仿ReentrantLock~

package lock;

public class MyLock {
	private Thread currentThread = null;

	public void lock() {
		while (true) {
			if (tryAcquire()) {
				break;
			}
		}
	}

	public void unlock() {
		tryRelease();
	}

	private synchronized boolean tryAcquire() {
		if (currentThread == null) {
			currentThread = Thread.currentThread();
			return true;
		}
		return false;
	}

	private synchronized void tryRelease() {
		if (currentThread == null || currentThread != Thread.currentThread()) {
			throw new IllegalMonitorStateException();
		}
		currentThread = null;
	}

	public static void main(String[] args) {
		final MyLock a = new MyLock();
		Runnable run = new Runnable() {
			@Override
			public void run() {
				a.lock();
				for (int i = 0; i < 10; i++) {
					System.out.println("i'm running : "
							+ Thread.currentThread());
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				a.unlock();
			}
		};
		Thread t1 = new Thread(run);
		Thread t2 = new Thread(run);
		t1.start();
		t2.start();
	}
}

考虑的第一种方式,是直接用synchronized关键字实现,因为这个关键字可以让等待锁的线程挂起。

但是问题是,synchonized关键字只能在一个方法内生效,没办法跨越lock和unlock方法,任何写在lock方法里面的synchronized代码块,都会在lock方法里面结束,释放掉它占有的全部锁资源。没有办法维持到unlock方法执行。

所以接下来自然而然就想到,是不是可以用一个变量代表资源,某个状态代表上锁了,某个状态代表空闲。因为类变量的生命周期是比方法长的,所以可以在lock方法中填上上锁标志,在unlock方法中填上空闲标示。

高仿ReentrantLock~

package lock;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.LockSupport;

public class MyLock {
	private Thread currentThread = null;

	private Queue<Thread> qt = new LinkedList<Thread>();

	public void lock() {
		if (!tryAcquire()) {
			LockSupport.park();
		}
	}

	public void unlock() {
		if (tryRelease()) {
			LockSupport.unpark(currentThread);
		}
	}

	private synchronized boolean tryAcquire() {
		if (currentThread == null) {
			currentThread = Thread.currentThread();
			return true;
		}
		if (currentThread == Thread.currentThread()) {
			return true;
		}
		qt.offer(Thread.currentThread());
		return false;
	}

	private synchronized boolean tryRelease() {
		if (currentThread == null || currentThread != Thread.currentThread()) {
			throw new IllegalMonitorStateException();
		}
		currentThread = qt.poll();
		if (currentThread != null) {
			return true;
		}
		return false;
	}

	public static void main(String[] args) {
		final MyLock a = new MyLock();
		Runnable run = new Runnable() {
			@Override
			public void run() {
				a.lock();
				for (int i = 0; i < 5; i++) {
					System.out.println("i'm running : "
							+ Thread.currentThread());
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				a.unlock();
			}
		};
		Thread t1 = new Thread(run);
		Thread t2 = new Thread(run);
		t1.start();
		t2.start();
	}
}
上面一种实现显然很低效,使用while循环进行线程等待,占用了过多CPU。

下面的实现就是要用LockSupport.park和unpark方法,来消除掉等待过程中,耗费的计算资源。

一开始写了一份代码,直接synchronized锁LockSupport的两个方法,然后就死锁了。。脑残果然无解。

然后想着把park和unpark方法提出来,不能锁,但是之前的判断还是要锁的,一时间没有好的思维方式来确认会不会有问题。

怎么看一个锁会不会有问题,这是一个很深的坑。

对于这份代码,锁住了currentThread和qt,基于这两个变量的基本操作就不会有问题,一定会按着剧本走。至于lock和unlock的逻辑对不对,看起来也没有别的规律可循,还是要排列组合,演示每一种时序来确定正确与否。

2013 1106:

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject

笔记:

AbstractQueuedSynchronizer类中管理的Node队列,实现了synchronized级别的锁管理。暂且把这个Node队列称为A_Node_List。acquire方法将线程包装成Node,放到队尾,release方法将队首的线程唤醒。

而内部类ConditionObject中同样管理着Node队列,只是这个Node队列的层次比较低,实现的是wait、notify作用。暂且把这个Node队列成为C_Node_List。await方法将线程包装成Node,放到C_Node_List的队尾,signal方法将队首的Node,放入A_Node_List的队尾。

也就是说,ConditionObject的await方法,是一个非常委屈的方法。它能够执行的时候,说明它一定是在A_Node_List中胜出了,作为一个胜利者。但是苦于资源没有到位,不得不放弃执行的机会,把锁传递给A_Node_List的后继,并且把自己放在一个更低优先级的队列C_Node_List中排队。而当它的资源到位以后(在C_Node_List中胜出以后),它还得排到A_Node_List中,重新去竞争执行的机会。

好比一位斗牛士,本来已经轮到他出场了(A_Node_List队首),但是发现手上没有剑,于是只能放弃这次机会,让后面的人先上场,自己去幕后排队领剑(C_Node_List)。这个时候,及时拿到了剑,也得老老实实去出场的队列里面(A_Node_List),继续排队。

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject

方法:

public final void await() throws InterruptedException

笔记:

这个方法可以被其他线程中断,如果一开始就被中断了,就抛出异常;如果在后面等待资源时被中断,就必须等到线程得到锁以后,这个方法才会抛出异常。

首先调用addConditionWaiter方法构造一个Node节点,然后调用fullyRelease方法尝试释放占用的全部锁,如果不能全部释放,就会抛出异常。这对应着线程没有取得锁(没有AbstractQueuedSynchronizer.aqcuire)的情况。类似调用Object.wait方法,但是之前没有用synchronized关键字取得Object对象的锁。

然后进入一个循环,循环退出条件是1、该Node已经在A_Node_List里面了,2、线程被打断了。第一种条件,意味着这个ConditionObject的signal方法被调用了,并且成功施放了锁,并且这个Node在C_Node_List中占据了靠前的位置,被signal方法唤醒了。第二种条件,意味着线程被打断,于是唤醒了LockSupport.park方法,检查中断标志位以后,退出循环。(checkInterruptWhileWaiting方法会保证Node节点插入A_Node_List中去,通过两条路径,1是正常的中断情况,2是signal方法没有及时上报Node时,在该方法中等待Node上报,具体意义参考signal方法上报Node的代码)。

循环结束以后,Node一定是在A_Node_List中了,这样,就可以由AbstractQueuedSynchronizer的锁逻辑进行控制了。当AbstractQueuedSynchronizer.acquireQueued方法执行完成以后,就代表着线程又重新获得了锁,可以重新运行。

当然,await方法会判断是否有被线程中断过。而awaitUninterruptibly方法,则跳过了这一步。

2013 1104:

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer

方法:

public final void acquire(int arg)、public final boolean release(int arg)

笔记:

总结一下这个类最基本的使用方法。使用acquire方法获取锁,使用release方法释放锁。参数可以理解成信号量。

acquire方法首先调用tryAcquire,尝试去直接拿到锁,如果能直接拿到,说明首先其他线程没有占据锁,其次在锁竞争中,它先到胜出了。

如果没能拿到锁,那么就要进入Node队列等待唤醒了。调用addWaiter方法,生成Node节点,并放到Node队列中去。再调用acquireQueued方法,挂住线程,等待唤醒。

release方法首先调用tryRelease,如果没有释放锁的能力(没有先acquire),直接抛出异常;如果拥有的信号量不够,则释放成功,但方法返回false。如果释放成功,那么接下来就从Node队列中,从头到尾扫描,找到第一个标记为SIGNAL的线程,并唤醒。

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject

笔记:

打个比方:烧饼店老板是个锁男,他有一个仓库放面团,有一个店面现做现卖,可是没有助手,拿面团和卖烧饼都是一个人。

1、锁男到店面。AbstractQueuedSynchronizer.acquire  或 synchronized

2、锁男到店面发现没有面团了,退出店面。ConditionObject.await 或 wait,即把锁男放出店面,店面暂时不营业。

3、锁男到仓库,取一些面团放到店面。AbstractQueuedSynchronizer.acquire-->ConditionObject.signal 或 notify,即把面团放到店面。锁男可以回身继续取面团,也可以去店面(AbstractQueuedSynchronizer.release)。

当我们扩充更多线程时,就相当于锁男有了很多仓库,很多店面。但是始终是勤劳苦逼的一个人,这么多仓库和店面,效率都比不上他一个人。

在上面的流程中,synchronized和wait、notify同样可以做到这些。

但是locks包的AbstractQueuedSynchronizer和ConditionObject可以比上面的做的更多。

继续打比喻,仓库里面,不仅存面团,还存着葱、肉馅。锁男可以从不同的仓库取出不同的食材,而在某个店面,只要面团、葱、肉馅齐备,就可以做出一个烧饼。

使用locks包,可以通过增加ConditionObject来实现,三个ConditionObject代表不同食材,锁男可以从仓库分几次带齐这些,这样就给了锁男更多的选择(也就是提高了并发)。

如果使用关键字synchronized和wait组合,那么锁男只能一次性带齐三种食材到店面才行。


2013 1103:

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer

方法:

private boolean doAcquireNanos(int arg, long nanosTimeout)

        throws InterruptedException

笔记:

我发现写这个包的码农,真的是非常喜欢外层大循环。

这个方法的代码还比较好理解,退出条件有三个:

1、获取到锁

2、等待超时

3、被其他线程打断,抛出异常退出

循环的主体部分,仍然是调用shouldParkAfterFailedAcquire清除不需要锁的Node节点,然后挂线程。

里面稍微特殊的是nanosTimeout > spinForTimeoutThreshold,意思是,1ms以内的线程等待,就不要调用LockSupport.park了,直接靠外层死循环度过就行了。这大概也是这一次死循环的主要原因了。

另一个死循环的原因仍然是,线程被唤醒以后,可以通过死循环,重新进入退出条件判断。不需要额外的代码。

这个方法和acquireQueued方法一样,都通过严格限制退出条件,保证了锁的效力。因释放锁而计划退出挂住状态的情况,都可以正常退出。而其他情况(其他线程打断或者unpark该线程)均无法退出挂住的情况。

从这一点看,这个包里面用到的外层死循环不是一种编程风格,而是切实必要的处理方式。

2013 1102:

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer

方法:

final boolean acquireQueued(final Node node, int arg)

笔记:

这个方法基本作用是,将node放到Node队列中的合适的位置,然后挂起当前线程。

调用shouldParkAfterFailedAcquire方法,是为了将node放到合适的位置。Node对象,可能因为取消了acquire动作,waitStatus变成了CANCELLED,从而不再需要维持在队列中。所以shouldParkAfterFailedAcquire方法会将node从队列尾部一直往前移,直到前面一个的waitStatus是SIGNAL。经过的节点,由于失去了前面的引用,就可以被回收了。

调用parkAndCheckInterrupt方法,会将当前线程挂住。一旦线程恢复,它会检测是否该线程是被打断的,如果是打断的,就设置interrupted变量为true。

这个方法里面,使用了死循环,原因就是parkAndCheckInterrupt方法使用LockSupport.park方法挂线程以后,这个线程可以有两种恢复方法,一种是LookSupport.unpark方法,一种是Thread对象的interrupt方法。如果是前者的话,说明Node队列已经轮到当前线程Node获取锁了,可以通过另一个if分支结束该方法。如果是后者的话,说明当前线程虽然被其他线程打断获得了运行机会,但是还不能获得锁,所以仍然需要继续等待。实现等待的方式,就是通过外层死循环,重新进入parkAndCheckInterrupt方法。

恢复线程的两种方法的测试代码:

package lock;

import java.util.concurrent.locks.LockSupport;

public class TestLock {
	public static void main(String[] args) throws InterruptedException {
		testPark();
		testInterrupt();
	}

	private static void testInterrupt() throws InterruptedException {
		Runnable run = new Runnable() {
			@SuppressWarnings("static-access")
			@Override
			public void run() {
				LockSupport.park();
				System.out.println(Thread.currentThread().interrupted());
			}
		};
		Thread t = new Thread(run);
		t.start();
		Thread.sleep(1000);
		t.interrupt();
	}

	private static void testPark() throws InterruptedException {
		Runnable run = new Runnable() {
			@Override
			public void run() {
				LockSupport.park();
				System.out.println("unpark");
			}
		};
		Thread t = new Thread(run);
		t.start();
		Thread.sleep(1000);
		LockSupport.unpark(t);
	}
}

2013 1031:

类:

java.util.concurrent.locks.AbstractOwnableSynchronizer

属性:

private transient Thread exclusiveOwnerThread

笔记:

这个属性记录当前持有锁的线程。ReentrantLock的设计形式就是,多个线程会依靠同一把锁,取到了锁的线程,会将线程对象放入该属性中,以便管理。

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer$Node

笔记:

AbstractQueuedSynchronizer管理线程对锁的竞争,并不是无序竞争,而是先来后到的队列形式。

它使用Node存储线程对象,并形成一个Node队列,当有新的线程请求锁时,就封装一个Node,排到队列尾部。

释放锁时,会从队列头开始往后找,找到一个需要锁的Node,并将其中保存的线程,使用LockSupport.unpark唤醒。

类:

java.util.concurrent.locks.AbstractQueuedSynchronizer

方法:

private Node enq(final Node node)

笔记:

这个方法是将node插入队列的尾部。

方法首先把tail取出来,然后调用compareAndSetTail方法——使用原子操作,比较tail是否还没有改变(可能被其他线程改变),如果没有改变,就把node作为tail插入队列。如果改变了,就继续循环,重新判断。

至于这个方法为什么里面要把队列为空的情况单独拎出来,应该是一种多线程处理。暂时记下来,以后一起分析这个包中的多线程逻辑。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值