多线程求和,记一个 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); }
先获取读操作的锁,释放后去获取写操作的锁,并没有保证操作的原子性。
当你看清人们的真相,于是你知道了,你可以忍受孤独