• AQS 框架中 setState() 和 setExclusiveOwnerThread() 方法顺序问题


    问题:在测试利用 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    然后定义测试代码如下:

    
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行结果如下:

    >>>>>>当前线程 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    发现竟然报错了,错误原因是因为在释放锁的时候先调用 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;
            }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    只需要把顺序调换一下就可以了,执行结果如下:

    >>>>>>当前线程 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 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    发现一切正常,最终值得到是 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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    支持可重入的锁释放代码:

            @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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    编写测试类如下:

    
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    测试结果如下:

    >>>>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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    所以以后注意下这个小细节问题。另外测试的时候不建议使用 join() 方法,此方法会让现场串行化执行,每次都必须等待加入进来的线程执行完才会轮到下一个线程执行,就约等于串行化执行了,所以推荐使用 CountDownLatch 工具类来测试。

  • 相关阅读:
    openGauss学习笔记-241 openGauss性能调优-SQL调优-审视和修改表定义
    【OPENVX】对象基本使用之vx_matrix
    【Spring】——5、@Lazy懒加载
    PX4天大bug,上电反复重启,连不上QGC!
    学不会的线段树
    实时监控网页变化,并增加多种提示信息
    数据化管理洞悉零售及电子商务运营——零售密码
    基于51单片机霍尔汽车自行车码表测速测里程显示proteus仿真原理图PCB
    加法器与减法器verilog
    云服务器安装 redis(源码安装)
  • 原文地址:https://blog.csdn.net/qq_35971258/article/details/126944207