问题:在测试利用 AQS 框架实现一个锁中发现了一个小问题,就是 setState() 和setExclusiveOwnerThread() 这两个方法的顺序调用,顺序错了,会导致严重的 Bug,下面来简单分析下这个问题。
代码示例
定义加锁和解锁代码如下:
class MyJucLock implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 加锁
*/
@Override
protected boolean tryAcquire(int acquires) {
assert acquires == 1;
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 错误的解锁代码示例
*/
protected boolean tryRelease(int releases) {
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException("不能释放别人的锁");
}
// 先把锁给释放了,此时有很多现成就进去加锁
setState(0);
// 就是因为这一步没有放入到 cas 里面去执行,而是脱离了锁去执行导致出问题
setExclusiveOwnerThread(null);
return true;
}
@Override
protected boolean isHeldExclusively() {
System.out.println(">>>>currentThread="+Thread.currentThread()+":exclusiveThread="+getExclusiveOwnerThread());
return getExclusiveOwnerThread()==Thread.currentThread();
}
}
private static final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
然后定义测试代码如下:
public class MyJucLockDemo {
static int count = 0;
static final int SIZE_=1000;
public static void main(String[] args) throws Exception {
MyJucLock jucLock = new MyJucLock();
CountDownLatch countDownLatch = new CountDownLatch(SIZE_);
for (int i = 0; i < SIZE_; i++) {
Thread thread = new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (Exception e) {
throw new RuntimeException(e);
}
jucLock.lock();
count++;
jucLock.unlock();
}
countDownLatch.countDown();
});
thread.start();
}
countDownLatch.await();
System.out.println(">>>>>>最终结果最终结果最终结果,count="+count);
}
}
运行结果如下:
>>>>>>当前线程 Thread-91,count=1438
>>>>currentThread=Thread[Thread-167,5,main]:exclusiveThread=null
>>>>>>当前线程 Thread-143,count=1439
Exception in thread "Thread-167" java.lang.IllegalMonitorStateException: 不能释放别人的锁
at main.future.juc.MyJucLock$Sync.tryRelease(MyJucLockDemo.java:31)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
at main.future.juc.MyJucLock.unlock(MyJucLockDemo.java:98)
at main.future.juc.MyJucLockDemo.lambda$main$0(MyJucLockDemo.java:126)
at java.base/java.lang.Thread.run(Thread.java:834)
发现竟然报错了,错误原因是因为在释放锁的时候先调用 setState() ,然后再去调用 setExclusiveOwnerThread() 方法,方法调用顺序错误导致的,简单分析下:
先调用了 setState() 方法,那么就相当于把锁释放了,此时就存在这样一种情况,锁释放了,此时还没有来得及去调用 setExclusiveOwnerThread() 方法将 exclusiveOwnerThread 变量置 null,
然后时间片用完,切换到去执行尝试加锁,并且恰好其中一个线程加锁成功,调用 setExclusiveOwnerThread() 方法设置我占有了这把锁。
然后时间片又切换回上一次释放锁的线程,开始调用 setExclusiveOwnerThread() 方法将刚才加锁成功的线程给置成了 null,然后加锁成功的线程过来去释放锁,发现当前线程和占有锁的线程不相等了,exclusiveOwnerThread 变量被上一次释放锁的线程给置成了 null,那么肯定是不能够去释放这把锁的。
虽然去掉这层判断是可以释放锁成功的,但是这仅仅局限于不可重入锁,如果想要实现可重入锁,就必须保证是当前线程和持有锁的线程是一致的,否则就会出现乱释放锁的现象。
改进之后的代码如下:
/**
* 错误的解锁代码示例
*/
protected boolean tryRelease(int releases) {
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException("不能释放别人的锁");
}
// 先把锁给释放了,此时有很多现成就进去加锁
// 就是因为这一步没有放入到 cas 里面去执行,而是脱离了锁去执行导致出问题
setExclusiveOwnerThread(null);
setState(0);
return true;
}
只需要把顺序调换一下就可以了,执行结果如下:
>>>>>>当前线程 Thread-838,count=99997
>>>>currentThread=Thread[Thread-878,5,main]:exclusiveThread=Thread[Thread-878,5,main]
>>>>>>当前线程 Thread-878,count=99998
>>>>currentThread=Thread[Thread-699,5,main]:exclusiveThread=Thread[Thread-699,5,main]
>>>>>>当前线程 Thread-699,count=99999
>>>>currentThread=Thread[Thread-250,5,main]:exclusiveThread=Thread[Thread-250,5,main]
>>>>>>当前线程 Thread-250,count=100000
发现一切正常,最终值得到是 100000。
然后继续实现可重入锁,支持可重入的加锁代码:
/**
* 加锁
*/
@Override
protected boolean tryAcquire(int acquires) {
assert acquires == 1;
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(getState()+1);
System.out.println(">>>>>>锁重入啦==================");
return true;
}
return false;
}
支持可重入的锁释放代码:
@Override
protected boolean tryRelease(int releases) {
assert releases == 1;
int c = getState() - 1;
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException("不能释放别人的锁");
}
/**
* 注意 setExclusiveOwnerThread 和 setState 不能调用换顺序
* 为什么呢?因为 state 这个状态是锁,你要确保所有操作都做完了
* 再去释放这把锁才可以保证原子性,所以置空当前线程要在锁释放之前操作呀
*/
if (c == 0) {
setExclusiveOwnerThread(null);
/**
* 修改 state 的值标识要真正去释放这把锁
*/
setState(0);
return true;
}
/**
* 修改 state 的值标识要真正去释放这把锁
*/
setState(c);
return true;
}
编写测试类如下:
public class MyJucLockDemo {
static int count = 0;
static final int SIZE_=1000;
public static void main(String[] args) throws Exception {
MyJucLock jucLock = new MyJucLock();
CountDownLatch countDownLatch = new CountDownLatch(SIZE_);
for (int i = 0; i < SIZE_; i++) {
Thread thread = new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (Exception e) {
throw new RuntimeException(e);
}
jucLock.lock();
jucLock.lock();
count++;
jucLock.unlock();
jucLock.unlock();
}
countDownLatch.countDown();
});
thread.start();
}
countDownLatch.await();
System.out.println(">>>>>>最终结果最终结果最终结果,count="+count);
}
}
测试结果如下:
>>>>currentThread=Thread[Thread-444,5,main]:exclusiveThread=Thread[Thread-444,5,main]
>>>>>>锁重入啦==================
>>>>currentThread=Thread[Thread-329,5,main]:exclusiveThread=Thread[Thread-329,5,main]
>>>>currentThread=Thread[Thread-329,5,main]:exclusiveThread=Thread[Thread-329,5,main]
>>>>>>锁重入啦==================
>>>>currentThread=Thread[Thread-907,5,main]:exclusiveThread=Thread[Thread-907,5,main]
>>>>currentThread=Thread[Thread-907,5,main]:exclusiveThread=Thread[Thread-907,5,main]
>>>>>>最终结果最终结果最终结果,count=100000
所以以后注意下这个小细节问题。另外测试的时候不建议使用 join() 方法,此方法会让现场串行化执行,每次都必须等待加入进来的线程执行完才会轮到下一个线程执行,就约等于串行化执行了,所以推荐使用 CountDownLatch 工具类来测试。