多线程求和,记一个 synchronized 的错误使用方式

  

  总结:

  1. 如果在单线程环境下,几个操作共享变量的方法存在数据依赖关系,那么多线程环境下它们必须是一组原子操作,且与任何修改共享变量的方法互斥。与读方法是否互斥需要看程序的设计,比如 CopyOnWrite 模式下,这些原子操作不会与读共享变量的动作互斥,可以提高读的效率,但缺点是不能保证读操作每次读到的都是最新的值。

  2. 保证多线程任务的正确性,是基于各线程对共享变量访问的正确性来保证的。

  3. 我们还必须保证,多线程环境下存在数据依赖或控制依赖的方法,操作结果对其它线程的可见性以及操作对其它线程来说的有序性。happen-before 原则便是基于这两点的。

  4. 尤其要注意单线程情况下不存在数据依赖,但与其它线程的执行有关的动作。因为单线程下不存在数据依赖,编译器很可能会进行乱序执行。比如修改共享变量并唤醒其它线程,单线程下这两个动作是不存在数据依赖的,如何乱序执行都不会影响单线程下的执行结果。但多线程环境下,被唤醒的线程可能是需要依据共享变量的值工作的,这两个动作在多线程环境下实际是存在数据依赖的。

  题目很简单,使用多线程求一亿个数的和。这篇文章主要是为了总结一下多线程编程的思路,保证多线程任务的正确性,是基于各线程对共享变量访问的安全性来保证的。

  定义共享数据:

public class ArraySource {

    //源数组
    private int[] source;
    //累加结果
    private int result = 0;
    //当前工作的线程数
    private int threadNum;

    ArraySource(int[] source, int threadNum) {
        this.source = source;
        this.threadNum = threadNum;
    }

    public int[] getSource() {
        return this.source;
    }

    public int getResult() {
        return this.result;
    }

    public void setResult(int result) {
        this.result = result;
    }

    public int getThreadNum() {
        return this.threadNum;
    }

    public void setThreadNum(int threadNum) {
        this.threadNum = threadNum;
    }

}

  定义线程任务:

public class SumThread extends Thread {
    private int begin;
    private int end;
    ArraySource source;
    Object lock = new Object();

    SumThread(int begin, int end, ArraySource source) {
        this.begin = begin;
        this.end = end;
        this.source = source;
    }

    @Override
    public void run() {
        if (this.source == null || this.begin >= this.end) {
            throw new NullPointerException("非法入参!");
        }
        int re = 0;
        int[] sourceArray = this.source.getSource();
        for (int i = begin; i <= end; i++) {
            re += sourceArray[i];
        }
        synchronized (lock) {
            source.setResult(source.getResult() + re);
            source.setThreadNum(source.getThreadNum() - 1);
        }
    }
}

  主函数:

public class Test {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int re = test();
            if (re != 100000000) {
                System.out.println("第 " + i + " 发生了错误,结果为" + re);
                break;
            }
        }
        System.out.println("测试结束");
    }

    public static int test() throws InterruptedException {
        int[] source = new int[100000000];
        for (int i = 0; i < 100000000; i++) {
            source[i] = 1;
        }
        long beginTime = System.currentTimeMillis();
        int re = 0;
        for (int i = 0; i < 100000000; i++) {
            re += source[i];
        }
        System.out.println("单线程用时为 :" + (System.currentTimeMillis() - beginTime));
        System.out.println("单线程结果为 :" + re);

        ArraySource arraySource = new ArraySource(source, 4);
        SumThread thread0 = new SumThread(0, 20000000, arraySource);
        SumThread thread1 = new SumThread(20000001, 40000000, arraySource);
        SumThread thread2 = new SumThread(40000001, 60000000, arraySource);
        SumThread thread3 = new SumThread(60000001, 99999999, arraySource);
//        SumThread thread4 = new SumThread(80000001, 99999999, arraySource);
        beginTime = System.currentTimeMillis();
        thread0.start();
        thread1.start();
        thread2.start();
        thread3.start();
//        thread4.start();
        while (arraySource.getThreadNum() != 0) {
//            Thread.sleep(500);
//            System.out.println("还有 : " + arraySource.getThreadNum() + "  个线程在工作;当前和为: " + arraySource.getResult());
        }
        System.out.println("多线程用时为 :" + (System.currentTimeMillis() - beginTime));
        System.out.println("多线程结果为 :" + arraySource.getResult());
        return arraySource.getResult();
    }
}

  主函数中执行了一千次代码以验证程序的正确性,最终证实程序是可靠的。

  记一个错误的写法:

public class ArraySource {

    //源数组
    private int[] source;
    //累加结果
    private int result = 0;
    //当前工作的线程数
    private int threadNum;

    ArraySource(int[] source, int threadNum) {
        this.source = source;
        this.threadNum = threadNum;
    }

    public int[] getSource() {
        return this.source;
    }
public  int getResult() {
synchronized(this) {
return this.result;
}
}

public void setResult(int result) {
synchronized(this) {
this.result = result;
}
}

public int getThreadNum() {
synchronized(this) {
return this.threadNum;
}
}

public void setThreadNum(int threadNum) {
synchronized(this) {
this.threadNum = threadNum;
}
}
 

  这种写法等同于:

    @Override
    public void run() {
        if (this.source == null || this.begin >= this.end) {
            throw new NullPointerException("非法入参!");
        }
        int re = 0;
        int[] sourceArray = this.source.getSource();
        for (int i = begin; i <= end; i++) {
            re += sourceArray[i];
        }
        int totalRe=source.getResult();
        source.setResult( totalRe + re);
        int sharedThreadNum=source.getThreadNum();
        source.setThreadNum( sharedThreadNum - 1);
    }

  先获取读操作的锁,释放后去获取写操作的锁,并没有保证操作的原子性。

posted @ 2020-03-05 19:48  牛有肉  阅读(349)  评论(0编辑  收藏  举报