美文网首页
Java多线程:单变量同步问题,以及验证方法

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

作者: 拾识物者 | 来源:发表于2018-10-10 16:11 被阅读5次

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

  • 回避多线程问题,只用单线程写代码,肯定是线程安全的。没错,这是最简单的解决方法,但解决不了所有问题。
  • 规定不用多线程,规定某些方法某些类不能用于多线程场景。但这种规定即使形成文档也不见得一定会被遵守,尤其是维护的时候。

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

  • 限制多线程的使用:比如大多数图形界面都将UI操作限制在单线程中,swing如此,android也是这样。这是简化问题的一种好方法,但满足不了所有的需求,只能在特定框架下使用。
  • 设定类的多线程使用规范:明确写出在多线程环境下使用类的方法和注意事项,如果你不遵守,那就自己承担后果吧,咩哈哈。其实大部分只有两个标签:安全和不安全,并没有复杂的中间情况,去看看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 的值,得到一样的值,然后分别修改这个值+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传送门

相关文章

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

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

  • 5月份第一周学习安排

    学习内容: java多线程及线程同步的方法(使用) java多线程各种同步方法的原理和优缺点 java多线程设计模...

  • Java多线程是如何解决同步的?

    同步资源 同步资源,是对资源(类、方法、代码块、变量)进行同步控制。在java中的多线程操作(修改)同一个共享变量...

  • java中的Atomic类

    java中的Atomic类 问题背景 在多线程环境中,我们最常遇到的问题就是变量的值进行同步。因为变量需要在多线程...

  • 线程间通信之等待通知机制

    等待/通知机制 前面部分介绍了Java语言中多线程的使用,以及方法及变量在同步情况下的处理方式,本节将介绍多个线程...

  • 2.安全性

    java中多线程同步包括: synchronized 显示锁 volatile 原子变量 之所以要使用同步,是因为...

  • Java并发之synchronized

    Java多线程同步关键词是常用的多线程同步手段。它可以修饰静态类方法,实例方法,或代码块。修饰static静态方法...

  • Java多线程(一)多线程基础

    前言 本文主要讲解java多线程的基础,以及一些常用方法。关于线程同步、ExecutorService框架我会放到...

  • Java多线程同步2——同步方法

    java多线程同步除了上文说到的同步代码块,还可以使用同步方法,还是银行取钱的那个问题,代码如下 public c...

  • 并发编程-锁与线程同步

    多线程同步 为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改...

网友评论

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

      本文链接:https://www.haomeiwen.com/subject/diypaftx.html