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传送门
网友评论