java 多线程中实现锁的类型
主要有两种 一种是使用synchronize
第二种是自己实现Lock接口或者直接使用系统实现Lock接口的ReentantLock。
下面主要讨论第二种方式:实现Lock接口
下面是一个实现了Lock接口的自定义锁
public class MyLock implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new
IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
我们可以看到最终是通过继承AbstractQueuedSynchronizer(AQS)类的来实现加锁和获取锁的。
那么AQS是怎样实现多线程下的加锁呢?
首先结构上它是一个FIFO (First-in, First-out,先进先出)队列,图像如下图所示:
每次添加节点时
1.将新节点的prev指向tail节点
2.将 tail指向新节点。
3.将原tail节点的next指向新节点。
AQS中独占式锁的实现
首先acquire方法
首先源码如下
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
* 以独占模式获取,忽略中断,实现由至少执行一次tryAcquire,成功时才返回。否则这个 * 线程会在排队,可能会重复的加锁和解锁,会一直执行tryAcquire直到成功。
* 这个方法可以被使用到实现Lock.lock方法
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
方法注释的翻译:
以独占模式获取,忽略中断,实现由至少执行一次tryAcquire,成功时才返回。否则这个线程会在排队,
可能会重复的加锁和解锁,会一直执行tryAcquire直到成功。
这个方法可以被使用到实现Lock.lock方法
解释:
可以看到accquire方法先执行 tryAcquire如果成功就继续了,没有成功的话就会执行将当前线程放到队列中执行。
下面再看看 acquire中的addWaiter方法
addWaiter方法
/**
* Creates and enqueues node for current thread and given mode.
*
* @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);
// 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;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果队列为空初始化队列
if (t == null) { // Must initialize
//这里设置的头结点可以认为是当前正在执行线程的节点 对于AQS来说必须让头结点为正在执行的节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上述的代码,主要还是通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。(compareAndSetTail这个具体的实现是JNI调用操作系统的原生程序,这里就不深入了.), 最后 enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。
当节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),这个实现就是acquireQueued方法
acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过
//这是一个假的“自旋”!
for (;;) {
final Node p = node.predecessor();//拿到前驱
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
// head是一个Lock对象的全局变量 private transient volatile Node head;
if (p == head && tryAcquire(arg)) {
setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
p.next = null; // 说明前一个锁线程已经不再队列中了 setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; // 成功获取资源
return interrupted;//返回等待过程中是否被中断过
}
//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
// 这里应该是解决 tryAcquire 成功但 failed未修改的情况吧,具体没看懂为什么这么做
if (failed) //或者如这里所说 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
可以看到 acquireQueued的主要逻辑是:判断当前节点的前节点是否是头节点,如果是就尝试获取同步状态,获取成功就将自己设置为头节点。
顺便如果当前节点的前驱不是头节点,当前线程会被中断,等待执行节点的release,来唤醒所有的排队节点进行执行节点的检查。
下面是AQS整个独占式状态的获取流程: