这是Cert Java中的一段源代码,如果是在多线程中环境中,该段代码的执行结果可能不是我们想要的结果。
final class ControlledStop implements Runnable {
private boolean done = false;
@Override public void run() {
while (!done) {
try {
// ...
Thread.currentThread().sleep(1000); // Do something
} catch(InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public void shutdown() {
done = true;
}
}
程序中run方法的目的是在while循环条件中判断变量done的值为false时,执行相关的处理后,休眠1秒钟。但是在另一个方法shutdown中会修改done的值,如果是上述代码运行在多线程环境下,则while循环条件的值被修改,则可能出现不是想要达到的目的,所以需要把控制循环的变量done进行读写同步控制。
那么为什么会出现这种情况下?这跟多线程的运行机制有关。多线程程序运行时,线程在进程的堆中创建栈空间后,线程会在自己的栈空间内执行。通过判断done的真假来决定是否只需要循环体。如果另一个线程修改了done的值,这个修改的值何时同步到主内存是未知的,可能由cpu调度执行,或可能在堆空间不够时释放前写入到主内存等。每个线程在创建时,线程栈中的变量取值是在线程创建时从主内存拷贝过来的值。上述程序中,线程对done值的改变是不可见的,换句话说,就是done的值改变了,当前线程进行判断时,不知道done的值已经被改变,这是否是程序想要实现的功能是未知的。
在多线程编程中,采用volatile变量、锁、原子变量等机制进行同步的目的是保证对同步代码块的执行是可见的。线程同步的目的,是让每个线程都清楚其它线程对变量的改变,所以对上述代码要进行同步处理。
第一种修复方案是,使用volatile修饰符声明done变量,代码如下:
final class ControlledStop implements Runnable {
private volatile boolean done = false;
@Override public void run() {
while (!done) {
try {
// ...
Thread.currentThread().sleep(1000); // Do something
} catch(InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public void shutdown() {
done = true;
}
}
volatile修饰done变量后,done值的改变都会写回主内存,这样相当于对其它线程可见。线程读取done值时,要去主内存取当前最新值,这样基本上保证了同步。但是如果volatile修饰的是long或double类型,则执行起来不一定是程序想要的结果。因为上面类型都是64位的,无法在一个时钟周期内完成。如果是在两个时钟周期内完成修改值,则可能要等到下一个cpu执行时间片才能执行,这样导致结果也是未知的。所以最好使用锁的方法进行同步。代码如下:
final class ControlledStop implements Runnable {
private boolean done = false;
@Override public void run() {
while (!isDone()) {
try {
// ...
Thread.currentThread().sleep(1000); // Do something
} catch(InterruptedException ie) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public synchronized boolean isDone() {
return done;
}
public synchronized void shutdown() {
done = true;
}
}
对done的修改和取值全部添加锁,在循环判断中通过获取锁的形式,保证获取到最新的done值。保证对done修改及时可见。这种修改后,当前只有一个线程的一个方法获取锁,保证修改或读取done的值同步。因为线程获取锁时,JMM会把该线程对应的栈工作空间内存设置为无效,从而使得监视器保护的临界区代码要从主内存中去读取共享变量。当线程释放锁时,也就是退出锁下面的可执行代码时,都会把线程栈工作空间的共享变量刷新到主内存,保证了数据同步可见。
在多线程编程中,编写代码和调试代码给程序员带来了诸多挑战,由于线程执行调度的复杂性,导致一些问题可能需要调试多次,也不一定能够复现。还好,我们可以借助一款源代码安全漏洞检测工具-CoBOT,通过CoBOT检测,找到上面代码中的潜在风险,根据报告的问题,我们再去判断和分析,这样对于编写和调试多线程程序可以节省大量时间。
(完)