Java多线程:单变量同步问题,以及验证方法

2018-10-10  本文已影响5人  拾识物者

Java多线程是很重要的基础,然而平时工作中却不是特别关心,很容易写出线程不安全的代码,但实际上代码还能正确的工作,这是为什么呢?

保证线程安全是一个特别繁复的问题,以上两种看起来不怎么正经的方案其实也是解决多线程同步问题的两种方法:

当然,多线程问题还是要靠复杂手段解决的,本文只讨论单个变量同步的问题,以及验证的方法。这份代码是《Java并发编程实践》一书内的示例代码,有一些补全和小改动。

这个小功能是分解因数,书中使用的是servlet做例子,其实用一个简单的类也是可以的。提供一个计算因数分解的类 Factorizer,方法 calculateFactor ,记录计算的次数。calculateFactor方法会被多个线程调用,检查最后的调用次数变量和实际调用次数是否一致。如果不一致说明是线程不安全的,如果一致虽然不能百分之百证明是安全的,这个时候加大样本数量基本能得到和理论一致的结果。

是的,就像实际项目中一样,多线程产生的问题大多数都不是必现的,不然早在开发过程中就解决了。

先上验证的代码,详细解释请看代码中注释:

public class Factor {
    private static final int CALCULATION_COUNT = 10; // 每个线程计算多少次分解因素计算
    public static void main(String[] args) {
        // 获取cpu数量,用过多的线程验证没有意义
        int n = Runtime.getRuntime().availableProcessors();
        Factorizer factorizer = new Factorizer();
        Thread[] threads = new Thread[n];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread() {
                public void run() { // 每个线程执行多次计算
                    for (int j = 0; j < CALCULATION_COUNT; j++) {
                        int number = new Random().nextInt(1000);
                        factorizer.calculateFactor(number);
                    }
                }
            };
            threads[i].start();
        }

        try {
            for (int i = 0; i < threads.length; i++) {
                threads[i].join(); // 主线程等待子线程执行完毕再结束
            }

            int correctCount = threads.length * CALCULATION_COUNT;
            System.out.println("final count=" + factorizer.getCount() + ", supposed to be " + correctCount);
            System.out.println(factorizer.getCount() == correctCount ? "Correct, but cannot prove it is thread safe." : "Wrong, it is not thread safe.");
        } catch (Throwable e) {
        }
    }
}

下面是第一个版本的 Factorizer,使用 long 类型计数:

class Factorizer {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public int[] calculateFactor(int n) {
        int[] result = factor(n);
        count++;
        return result;
    }
}

以上代码没有做任何的同步处理,执行失败比例:2/10,10次中2次最终的计数不对,少于预期的数量,这是为什么呢?

原因在于 count++ 并不是一个原子操作,其实有好几个步骤:

按照书中的说法,这是一个典型的“读-改-写”过程。

多个线程执行这些操作,就可能会发生两个线程同时读取 count 的值,得到一样的值,然后分别修改这个值+1,最后将同样的值写回 count 中,就造成了 count 中值的错误。

下面是使用 AtomicLong 定义 count 来计数的代码:

class Factorizer {
    private AtomicLong count = new AtomicLong(0);

    public AtomicLong getCount() {
        return count;
    }

    public int[] calculateFactor(int n) {
        int[] result = factor(n);
        count.incrementAndGet();
        return result;
    }
}

经过多次执行,结果全部是正确的。原因就在于 AtomicLong.incrementAndGet() 是一个原子操作,同时只能有一个线程执行“读-改-写”的流程,这样每个线程读取到的 count 值都是另一个线程修改过的结果,于是最终的结果就是正确的。

还有另外一种加锁写法:

class Factorizer {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public int[] calculateFactor(int n) {
        int[] result = factor(n);
        synchronized (this) {
            count++;
        }
        return result;
    }
}

原理是一样的,synchronized 同步块也能限制 count++; 只有一个线程访问。

结论:“读-改-写”单个变量的操作序列需要锁保护才能保证多线程条件下的正确性。

完整代码:GitHub传送门

上一篇 下一篇

猜你喜欢

热点阅读