Java关于锁/并发的一点疑惑(含示例)

  • 4
新手上路,请多包涵

事情是这样的,我在做一点锁的小练习,模拟多线程去做库存扣减。容我先贴上代码

static class Stock {

        private int value = 10000000;

        private int version;

        private final ReentrantReadWriteLock stockLock = new ReentrantReadWriteLock();

        public int getVersion() {
            return version;
        }

        public int getStockValue() {
            stockLock.readLock().lock();
            try {
                return value;
            } finally {
                stockLock.readLock().unlock();
            }
        }

        public void reduceStock(int reduceCount, int version) {
            stockLock.writeLock().lock();
            try {
                if (this.version == version) {
                    value = value - reduceCount;
                    this.version++;
                }
            } finally {
                stockLock.writeLock().unlock();
            }
        }

    }

    static class StockMonitor implements Runnable {

        final Stock stock;

        final long frequency;

        StockMonitor(Stock stock, long frequency) {
            this.stock = stock;
            this.frequency = frequency;
        }

        @Override
        public void run() {
            for (int value = stock.getStockValue(); value > 0; value = stock.getStockValue()) {
                System.out.format("%s: stock:%s version:%s%n", Thread.currentThread().getName(), value, stock.getVersion());
                try {
                    Thread.sleep(frequency);
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.format("%s: 最终stock: %s%n", Thread.currentThread().getName(), stock.getStockValue());
        }

    }

    static class StockReducer implements Runnable {

        final Stock stock;

        final int reduceAmount;

        StockReducer(Stock stock, int reduceAmount) {
            this.stock = stock;
            this.reduceAmount = reduceAmount;
        }

        @Override
        public void run() {
            while (stock.getStockValue() >= reduceAmount && !Thread.currentThread().isInterrupted()) {
                int version = stock.getVersion();
                stock.reduceStock(reduceAmount, version);
            }
            if (Thread.interrupted()) {
                System.out.format("%s: 我被中断了!%n", Thread.currentThread().getName());
            }
        }

    }

    private static void runTask(int count) {
        final Stock stock = new Stock();
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                2,
                200,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2)
        );
        StockMonitor monitor1 = new StockMonitor(stock, 100);
        Thread m1 = new Thread(monitor1);
        m1.start();

        for (int i = 0; i < 3; i++) {
            poolExecutor.execute(new StockReducer(stock, (int)(Math.random() * 10 + 1)));
        }
        poolExecutor.execute(new StockReducer(stock, 1));

        long start = System.currentTimeMillis();
        while (m1.isAlive()) {}
        long end = System.currentTimeMillis();
        poolExecutor.shutdownNow();
        while (!poolExecutor.isTerminated()) {}
        System.out.format("第%s次耗时: %ss%n",  count, ((end - start) / 1000.0));
    }

    public static void main(String[] args) {
        for (int i = 1; i < 101; i++) {
            runTask(i);
        }
    }

代码中定义了一个共享资源Stock,一个StockReducer,一个StockMonitor。StockMonitor作用就是打印Stock状态,StockReducer作用是扣减库存。
人菜瘾大,我试图模拟了一下CAS,想测一下随着线程地增加性能会有多少递减来着。
我在准备运行代码前还感觉良好,然而现实立刻给予了我重击。现在咱们来看看输出结果:

Thread-0: stock:10000000 version:0
Thread-0: stock:4644660 version:891567
Thread-0: 最终stock: -8
第1次耗时: 0.241s
Thread-1: stock:10000000 version:0
Thread-1: stock:4965258 version:718427
Thread-1: 最终stock: 0
第2次耗时: 0.221s
Thread-2: stock:10000000 version:0
Thread-2: stock:4173631 version:896154
Thread-2: 最终stock: 0
第3次耗时: 0.223s
Thread-3: stock:10000000 version:0
Thread-3: stock:6127375 version:979329
Thread-3: stock:2475406 version:1884079
Thread-3: 最终stock: 0
第4次耗时: 0.336s
Thread-4: stock:10000000 version:0
Thread-4: stock:3816026 version:880387
Thread-4: 最终stock: 0
第5次耗时: 0.223s
Thread-5: stock:10000000 version:0
Thread-5: stock:2955524 version:883127
Thread-5: 最终stock: 0
......

跑了好几次,抓到了一个第一次就库存超卖的结果。
我疑惑的是,在reduceStock方法内我加了写锁,怎么还是会有线程跑进if条件呢。我尝试过给version加volitial,试过在if条件加上value > 0,都还是会出现这个问题。
还有一个有意思的现象,当我把线程数+1,也就是在runTask方法里将for的i < 4。这时运行一次循环的时间平均是i < 3的十倍左右,以下是线程+1后的输出结果:

Thread-0: stock:10000000 version:0
Thread-0: stock:9720622 version:106648
Thread-0: stock:9308132 version:267112
Thread-0: stock:8879434 version:431788
Thread-0: stock:8446031 version:593737
Thread-0: stock:8017534 version:756205
Thread-0: stock:7589054 version:921154
Thread-0: stock:7149642 version:1087798
Thread-0: stock:6698148 version:1254978
Thread-0: stock:6252264 version:1424765
Thread-0: stock:5807580 version:1593240
Thread-0: stock:5357400 version:1760675
Thread-0: stock:4894778 version:1927998
Thread-0: stock:4446009 version:2094625
Thread-0: stock:3995518 version:2263108
Thread-0: stock:3531879 version:2430541
Thread-0: stock:3082959 version:2597376
Thread-0: stock:2633458 version:2762137
Thread-0: stock:2188267 version:2927416
Thread-0: stock:1733464 version:3095106
Thread-0: stock:1269690 version:3265381
Thread-0: stock:807281 version:3435110
Thread-0: stock:322541 version:3607455
Thread-0: 最终stock: 0
第1次耗时: 2.583s
Thread-1: stock:10000000 version:0
Thread-1: stock:9344872 version:156057
Thread-1: stock:8685019 version:333065
Thread-1: stock:7988600 version:509706
Thread-1: stock:7367318 version:685235
Thread-1: stock:6738699 version:861870
Thread-1: stock:6117508 version:1037418
Thread-1: stock:5438947 version:1214298
Thread-1: stock:4860224 version:1391463
Thread-1: stock:4206301 version:1568004
Thread-1: stock:3610213 version:1744932
Thread-1: stock:2996725 version:1921401
Thread-1: stock:2355161 version:2097362
Thread-1: stock:1705939 version:2274564
Thread-1: stock:1096618 version:2448457
Thread-1: stock:523388 version:2623930
Thread-1: stock:20806 version:2800975
Thread-1: 最终stock: 0
第2次耗时: 1.901s

对于这个现象我也是想不明白,而且随着线程数增加,出现库存超卖的频率变少了(据我多次观察,绝不是时间长的我懒得看下去.jpg)
在此请教各位大佬指教!

回复
阅读 422
2 个回答

还是会出现负数,那么问题应该还是在reduceStock方法中,虽然前面while循环中有stock.getStockValue() >= reduceAmount这个判断,但是不保证在执行时不会发生变化,所以还需要在reduceStock方法里面获取锁之后再进行一次判断。
可以试试下面的这个

public void reduceStock(int reduceCount, int version) {
    stockLock.writeLock().lock();
    try {
        if (getVersion() == version && getStockValue() >= reduceCount) {
            value = getStockValue() - reduceCount;
            this.version++;
        }
    } finally {
        stockLock.writeLock().unlock();
    }
}

还可以在获取锁之前加判断

        public void reduceStock(int reduceCount, int version) {
            if (getVersion() == version && getStockValue() >= reduceCount) {
                stockLock.writeLock().lock();
                try {
                    if (getVersion() == version && getStockValue() >= reduceCount) {
                        value = getStockValue() - reduceCount;
                        this.version++;
                    }
                } finally {
                    stockLock.writeLock().unlock();
                }
            }
        }

我这边跑完了100次,没有出现负数

我也是人菜瘾大,既然你想测试cas,我觉得你的写法很矛盾,又是读写锁又是cas,很难抓住重点,所以我改造了下,更加清晰,明了。


    private static void runTask(int count) throws InterruptedException {
        final Stock stock = new Stock();
        ExecutorService poolExecutor = Executors.newFixedThreadPool(4);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 3; i++) {
            poolExecutor.execute(new StockReducer(stock, (int) (Math.random() * 10 + 1)));
        }

        poolExecutor.execute(new StockReducer(stock, 1));

        poolExecutor.shutdown();
        boolean b = poolExecutor.awaitTermination(10, TimeUnit.SECONDS);
        long end = System.currentTimeMillis();

        System.out.format("第%s次耗时: %ss,是否正常终止:%b,最终stock:%s%n", count, ((end - start) / 1000.0), b, stock.getStockValue());
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i < 101; i++) {
            runTask(i);
        }
    }

    static class Stock {

        private volatile int value = 10000000;

        public int getStockValue() {
            return value;
        }

        public synchronized void update(int value, int expect) {
            if (this.value == expect)
                this.value = value;
        }

    }

    static class StockReducer implements Runnable {

        final Stock stock;

        final int reduceAmount;

        StockReducer(Stock stock, int reduceAmount) {
            this.stock = stock;
            this.reduceAmount = reduceAmount;
        }

        @Override
        public void run() {
            while (stock.getStockValue() >= reduceAmount && !Thread.currentThread().isInterrupted()) {
                int value = stock.getStockValue();
                int update = value - reduceAmount;
                if (update >= 0)
                    stock.update(update, value);
                else
                    break;
            }
        }

    }
你知道吗?

宣传栏