1 何谓“竞态”
之前在学习一篇文章的时候,就看到“竞态”,但是不知道什么意思,文章中也没有对“竞态”做更多的解释,后来经过一番的探索,终于弄的差不多明白了,今天写点总结。 首先,我们要明白“竞态”是什么。先说我的结论吧,“竞态”就是在多线程的编程中,你在同一段代码里输入了相同的条件,但是会输出不确定的结果的情况。我不知道这个解释是不是够清楚,我们接着往下看,下面我们用一段代码来解释一下啊。 出现竞态条件的代码:
public class MineRaceConditionDemo {
private int sharedValue = 0;
private final static int MAX = 1000;
private int raceCondition() {
if (sharedValue < MAX) {
sharedValue++;
} else {
sharedValue = 0;
}
return sharedValue;
}
public static void main(String[] args) {
MineRaceConditionDemo m = new MineRaceConditionDemo();
ExecutorService es = new ThreadPoolExecutor(10,
10,
5,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
es.execute(() -> {
try {
// 这是精髓所在啊,如果没有这个,那么要跑好几次才会出现竞态条件。
// 这个用来模拟程序中别的代码的处理时间。
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
int num = m.raceCondition();
if (map.get(num) != null) {
System.out.println("the repeat num: " + num);
System.out.println("happen.");
} else {
map.put(num, 0);
}
});
}
es.shutdown();
}
}
以上的代码是我自己的设计的一段会出现竞态条件的代码,比较简陋,但是可以说明问题了,你只要运行上面这段代码,每次的输出的结果级大概率都是不同的,但是也有以外,比如你的电脑的性能很强,这段代码也会出现执行正确的情况,也就是啥也不输出。 比如有的时候输出这个:
the repeat num: 78
happen.
the repeat num: 229
happen.
the repeat num: 267
happen.
the repeat num: 267
happen.
the repeat num: 498
happen.
有点时候输出这个:
the repeat num: 25
happen.
the repeat num: 157
happen.
当然,以上的是我的输出的,你们的输出肯定也是不同的。 对于上面这些,同一段代码,对于同样的输出,但是程序的输出有的时候是正确,有的时候是错误的,这种情况,我们称之为“竞态”。最要命的就是,代码每次输出不是每次都错误,而是你不知道他什么时候会正确,什么时候会错误。 当然,如果以上的代码执行的情况就是,啥都不输出,所有的值都是唯一的。
2 “竞态”为什么会发生?
“竞态”的发生主要是因为多个线程都对一个共享变量(比如上面的 sharedValue
就属于共享变量)有读取-修改的操作。在某个线程读取共享变量之后,进行相关操作的时候,别的线程把这个变量给改了,从而导致结果出现了错误。
什么样的代码模式会发生“竞态”
这部分知识主要是来自《Java多线程编程实战指南 核心篇》。 这里书中提到,会发生竞态条件就是两个模式:read-modify-write(读-改-写)和 check-than-act(检测而后行动)。 当然,这里面的都有一个相同的操作过程:某个有读取这个“共享变量”的操作,然后别的线程有个修改这个变量的操作。这里有个重点,在多个线程中,起码有一个线程有更新操作;如果所有的线程都是读操作,那么就不存在什么竞态条件。 总体来说,就是要thread1#load - thread2#update。 这种的模式,起码是是要有两个线程的,而且其中某个线程肯定是要有更新“共享变量”操作的,另一个线程不管是读取变量还是更新变量都会出现错误(要么读取脏数据、要么丢失更新结果)。
3 如何消除“竞态”?
单以上面的操作来说,一般来说有两种解法方式,
3.1 加锁
加上synchronized
关键字,保证每次只能有一个线程获取共享变量的使用权。当然这里也可以用 java.util.concurrent.locks.ReentrantLock
这样的显式锁进行加锁操作,思想都是一样。
private synchronized int raceCondition() {
if (sharedValue < MAX) {
sharedValue++;
} else {
sharedValue = 0;
}
return sharedValue;
}
3.2 利用原子操作
利用java的工具包里的 AtomicInteger
,代替int
,利用原子操作消除“竞态”。
private AtomicInteger sharedValue = new AtomicInteger(0);
private final static int MAX = 1000;
private int raceCondition() {
if (sharedValue.get() < MAX) {
return sharedValue.getAndIncrement();
} else {
sharedValue.set(0);
return sharedValue.get();
}
}
以上两种方法的要义就是保证每个线程在操作“共享变量”的都是原子操作。