手写一个排他锁
要求当多线程运行时只有线程名字为指定名字的线程能获取到锁
步骤
- 实现 lock 接口
- 写一个静态类,继承 AbstractQueuedSynchronizer
- 重写 aqs 里面的方法,完成需求
- aqs 里只有五个方法可以被重写
- tryAcquire tryRelease 排他锁 加锁、解锁
- tryAcquireShared tryReleaseShared 共享锁 加锁 解锁
- isHeldExclusively 当前线程是否被线程独占的方法
private static class Sync extends AbstractQueuedSynchronizer {
// 加锁
protected boolean tryAcquire(int arg) {
// 线程名字必须是 AQS-Lock 才能加锁
if (!Thread.currentThread().getName().equals("AQS-Lock")) {
return false;
}
if (compareAndSetState(0, arg)) {
// 设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 解锁
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalStateException();
}
// 此处不用cas的原因是,既然要调用解锁,那自然是自身持有锁,有锁,代表自己有一个单独的私人线程
// 所以不涉及多线程问题,无需cas
setState(0);
return true;
}
Condition getCondition(){
return new ConditionObject();
}
}
测试用例
private static CreateLock createLock = new CreateLock();
public static void main(String[] args) {
Thread A = new Thread(() -> {
testLock();
while (true) {
}
});
Thread B = new Thread(() -> {
testLock();
});
// 只有 A 能拿到锁,因为加锁限制了线程名字
A.setName("AQS-Lock");
B.setName("Lock");
A.start();
B.start();
}
private static void testLock() {
createLock.lock();
try {
System.out.println("当前获取到锁的线程名称是:"
+ Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
createLock.unlock();
}
}
输出结果
当前获取到锁的线程名称是:AQS-Lock
然后一直等待。。。(因为线程A的 while true )
q2 说一下 多线程情况下 ReentrantLock 公平锁加锁过程?
先定义一下公平锁和非公平锁
公平锁
现在有ABC三条线程, A线程获取到了锁,BC线程获取失败,BC线程会加入到同步队列的尾端。
A线程释放锁后, B线程收到唤醒通知,进行锁的争抢。
就这在这时,一个新来的线程D也来争抢了,D线程看到B正在排队(D线程其实是调用AQS源码里边的 hasQueuedPredecessors 方法) ,就放弃争抢,直接加入同步队列尾端。继续等待
非公平锁
现在有ABC三条线程,A线程获取到了锁,BC线程获取失败,BC线程会加入到同步队列的微端。A线程释放锁后,B线程收到唤醒通知,进行锁的争抢。
就这在这时候,一个新来的线程D也来争抢了
D线程很不讲武德,直接和B线程进行争抢。如果D线程抢夺失败,加入同步队列。
如果D线程成功,对B线程不公平。对C也不公平,因为C也在排队。
此外 C 线程不会争抢锁,因为只有前置节点通知后续节点才会唤醒,也就是说 C 线程只能被 B 唤醒,唤醒的前提是B线程争抢锁成功,并且B线程释放锁之后才会通知 C 。
加锁成功的线程
-
先调用 FairSync 里的 lock()方法
-
使用 AQS 的 模版方法 acquire, acquire (AQS) 会调用tryAcquire (我们重写的方法)
-
抢占成功后会 setExclusiveOwnerThread(current) ,设置当前线程持有锁
加锁失败的线程
整体流程如上,简单叙述一下:
如果获取失败,当前线程就会被添加到同步队列尾端。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
添加到尾端用的是 addWaiter 方法,addWaiter 方法会进行一次cas添加,如果添加失败, 则调用enq(死循环添加,一定要成功,如果添加成功,就会再次调用 )
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果队列不为空, 使用 CAS 方式将当前节点设为尾节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 队列为空、CAS失败,将节点插入队列
enq(node);
return node;
}
在 enq 方法里判断当前节点的前置节点是不是 head 节点,如果是,并且加锁成功,就算加锁成功
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
获取锁失败后, 判断是否把当前线程挂起
调用 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 当前节点的前驱就是head节点时, 再次尝试获取锁
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 中,如果前置节点的状态是 signal,就等待,如果是 cancel 取消状态,将其移出,如果是其他状态,就
替换为 signal 状态,然后自己等待
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// SIGNAL 设置了前一个节点完结唤醒
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果还是失败,在acquireQueued 方法里,调用 selfInterrupt() 中断方法:
// 当前线程进入waite状态,且将前驱节点的转态设置为signal,用于唤醒自己,返回中断状态状态
//注意,这里会清除中断状态!!
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //如果等待过程中被中断过就将interrupted标记置为true
在acquireQueued(final Node node, int arg) 中,获取锁的方式是一个死循环,成功:会将的标志位failed(标记当前Node是否成功获取资源)置为false。
如果失败:抛出异常,则执行执行finally中cancelAcquire(node)方法,取消当前线程请求资源的操作。该方法负责将需要取消的node踢出等待队列
参考
https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantLock.html#%e5%85%ac%e5%b9%b3%e9%94%81