多线程产生问题的原因大多都是访问了共享资源。所谓共享资源就是只能被一个线程访问的内存对象(如变量、打印机、文件、输入输出端口等)。下面看一个《Thinking in java》第四版中给出的一个例子:
package org.fan.learn.thread.share;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by fan on 2016/6/12.
*/
public class EvenChecker implements Runnable {
private IntGenerator intGenerator;
private final int id;
public EvenChecker(int id, IntGenerator intGenerator) {
this.id = id;
this.intGenerator = intGenerator;
}
public void run() {
while (!intGenerator.isCanceled()) {
int val = intGenerator.next();
if (val %2 != 0) {
System.out.println(val + "not even!");
intGenerator.cancel(); //线程安全的
}
}
}
public static void test(IntGenerator intGenerator, int threadCount) {
System.out.println("Press control-c to exit");
ExecutorService exe = Executors.newCachedThreadPool();
for (int i = 0; i < threadCount; i++) {
exe.execute(new EvenChecker(i, intGenerator));
}
exe.shutdown();
}
public static void test(IntGenerator intGenerator) {
test(intGenerator, 10);
}
}
package org.fan.learn.thread.share;
/**
* Created by fan on 2016/6/12.
*/
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
public boolean isCanceled() {
return canceled;
}
public void cancel() {
this.canceled = true;
}
}
其中EvenChecker作为测试任务:检测IntGenerator产生的整数是否是偶数。
EvenChecker的test实现中默认创建了10个线程,共享一个IntGenerator对象。这产生了资源竞争。注意:IntGenerator中的canceled属性是基本类型boolean类型的,而在java中对基本类型的基本操作(如赋值、返回值)是原子的。也就是说,java中对除long和double之外的基本类型的诸如赋值、返回值的简单操作是线程安全的(但是也不要过分依赖这一点,最好使用同步机制)。但是也要注意IntGenerator中的canceled是volatile的(volatile保证某个任务对canceled修改之后,其他任务可以实时看到,也就是说其他任务读取的canceled数据不是从该任务所运行的CPU缓存中获取的,而是从内存中读取的)。
在《Thinking in java》中说,一个任务不能依赖于另一个任务(这里说的任务就是线程),因为,任务的结束顺序是无法得到保证的。在这里,EvenChecker依赖的IntGenerator是非任务的(IntGenerator不是一个线程),可以消除潜在的资源竞争。
下面写一个IntGenerator的实现类,来展示不当的资源竞争的结果:
package org.fan.learn.thread.share;
/**
* Created by fan on 2016/6/12.
*/
public class EvenGenerator extends IntGenerator {
private int evenValue;
@Override
public int next() {
++evenValue;
//快速产生不正确的结果
//Thread.yield();
++evenValue;
return evenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
在我win7 JVM1.8机器上,如果不加Thread.yield()调用,很长时间都不会产生错误,为了展示错误添加了yield调用。输出结果如下:
结果一:
Press control-c to exit
9 not even!
结果二:
Press control-c to exit
171 not even!
287 not even!
289 not even!
结果三:
Press control-c to exit
513 not even!
515 not even!
所以,在看上去运行结果正常的情况下,并不能保证代码是正确的,这就是多线程的特性,这也是多线程编码的难点。
注意:在java中,递增不是原子性的操作。