通常情况下,当需要模拟多线程的时候我们会选择两种方式。第一种就是自己实现Runnable类,然后在主类中调用我们自己实现的Runnable,例如:
package concurrent;
public class MyRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("自己实现的Runnable!");
}
}
package concurrent;
public class Run {
public static void main(String[] args) {
MyRunnable myRun = new MyRunnable();
new Thread(myRun).start();
}
}
但是为了测试方便,我们更喜欢的这种姿势。凌厉干练。反手就是一个匿名内部类。
package concurrent;
public class Run {
public static void main(String[] args) {
//MyRunnable myRun = new MyRunnable();
//new Thread(myRun).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("自己实现的Runnable!");
}
}).start();;
}
}
但是,这时候,就会有一个大坑在等着你调。
通常情况下,这两种方式对测试是不会有什么影响的。但是如果模拟的是多个线程抢占资源,想要模拟多线程访问共享变量出错的问题,此时就该大大的注意了。还是举个栗子比较好。
以下代码显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。
清单 1. 非线程安全的数值范围类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
如果凑巧两个线程在同一时间使用不一致的值执行 setLower
和 setUpper
的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5)
,同一时间内,线程 A 调用 setLower(4)
并且线程 B 调用 setUpper(3)
,显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3)
—— 一个无效值。
我们想要模拟出来结果(4,3)来验证确实会出错,如果按照上方提供的模拟多线程时候的两种方式。
第一种方案,规规矩矩版。自己实现Runnable接口
业务类:
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(value+" value > upper"+upper);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(value+" value < lower"+lower);
upper = value;
}
}
public class TestSub implements Runnable{
private NumberRange v;
public TestSub(NumberRange v) {
this.v = v;
}
@Override
public void run() {
// TODO Auto-generated method stub
v.setLower(4);
}
}
public class TestSup implements Runnable{
private NumberRange v;
public TestSup(NumberRange v) {
this.v = v;
}
@Override
public void run() {
// TODO Auto-generated method stub
v.setUpper(3);
}
}
两个自己实现的Runnable线程,用来设置最大最小值。
public class VolatileLearn {
public static void main(String[] args) {
NumberRange num = new NumberRange();
num.setLower(0);
num.setUpper(5);
TestSub t = new TestSub(num);
TestSup t2 = new TestSup(num);
new Thread(t).start();
new Thread(t2).start();
}
}
最后是一个启动测试类。此时进行多次的运行,会发现确实能够出现(4,3)的错误情况。
我们再来看看第二种简约干净的实现方案。(匿名内部类)
public class VolatileLearn {
public static void main(String[] args) {
NumberRange num = new NumberRange();
num.setLower(0);
num.setUpper(5);
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
num.setUpper(3);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
num.setLower(4);
}
}).start();
}
}
代码看起来确实清爽了很多,但是却会发现再也模拟不出来错误的结果了。这是为什么呢?
实际上在这种模拟多个线程访问共享资源的时候是不能这样干的。因为匿名内部类里边访问外部的变量,实际上都必须是final类型的变量,而final修饰的变量是线程安全的。因此也就模拟不出来出错的结果了。
当然,这里边num变量没有使用final修饰,是因为jdk8中,会自动在底层加上final修饰符。
综上所述,以后想要模拟多个线程访问共享变量的情况,千万不要使用匿名内部类呀!不然就跳进一个大坑啦!