写这篇文章的缘由是因为之前师弟的一个问题,想要的实现停止线程执行的功能。
【实际需求】
点击一个按钮开始执行一个长期运行的任务,然后点击另外一个按钮则任务停止执行。
【问题分析】
看到这个问题的第一个想法就是通过线程去运行该任务,然后停止该线程,这可能是最直接的解决办法。但是如果强制一个正在运行的线程会将程序的运行置于一个不确定的状态,虽然知道这一点,但是当时我没想到其他办法,写到这里就感慨自己的学艺不精啊!最后使用thread的弃用方法stop()才强制停止线程成功,但是解决完这个问题,我想了一下,这里的线程运行是无条件的,所以才需要手动去停止线程的运行。但是如果线程的运行是有条件的,那么我们不需要去停止线程,只需要更改线程运行所需的条件即可,这样可以让线程自己安全停止。
【具体实现】
要实现线程在有条件的情况下运行并安全停止,要做到一个线程对变量的修改在另一个线程中能够及时发现这种更改。这种需求和Java 内存模型中可见性是意义相同的,换言之,我们只要实现多线程共享变量在内存中的可见性即可。这种实现方式有多种,比如synchronized
,和显式锁Lock
等这里使用的就是,但是这里想使用一个简单的方式,volatile关键字来实现,而且通过使用发现这种实现方式简直就是为这种场景设立的!
实例代码如下:
public class VolatileClient {
private volatile static boolean flag = false;
private static ExecutorService service = Executors.newCachedThreadPool();
public static void main( String[] args ) throws IOException {
service.submit((Runnable) () -> {
while(!flag){
System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
}
});
service.submit((Runnable) () -> {
try {
Thread.sleep(3000);
flag = true;
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
【深入分析】
虽然volatile非常适合上述场景,但是在多线程同时操作同一变量是并不能保证线程之间的互斥性,换言之,该关键字只能保证共享变量在内存中的可见性,不能保证互斥性,那么为什么它有这种特性呢?这需要从程序实际的运行情况考虑,
如何保证可见性?如下图所示:
对于计算机运行程序时,CPU会从内存中获取数据加载到CPU中进行运算,但是随着CPU的运行速度越来越快,但是内存的读取速度并没有跟上CPU的更新的速度,所以就会出现CPU等待数据和指令读取完成才能进行运算,这样就大大浪费了CPU,所以为了提高CPU的运行效率,在CPU和内存之间加上了多级寄存器,上面只是画出了L1 Cache和L2 Cache两级缓存。如果使用过Ehcache
或者Redis
作为缓存组件的,应该会对这个过程很明白:
对于普通的变量(这里指的是相对于volatile
变量):
- 首先CPU会到Cache中查找对应的数据或者指令,如果查到则直接返回CPU进行计算;如果没有查到,才会到内存RAM中读取,读取完成后则会将该数据缓存到缓存寄存器中,以后读取也会直接从Cache中读取;
- 如果执行写操作的时候,普通变量则是先将数据写到Cache,然后之后再通过调度任务将数据写回到内存中,当然这个时间是不确定的。这样才会在别的CPU Core第一次读取该变量的时候是从内存中获取的,而这个数据是旧的,新的数据值此时还没有被写回到内存中;
对于volatile变量:
- 首先在CPU读取数据的时候是会强制从内存中获取的,而不是从缓存中,这样从获取数据的一方保证数据是新的;
- 另外在写的时候,该变量也是强制写回到内存中,这样也保证了提供数据的一方保证数据是最新的,这里也是普通变量和volatile变量的区别;
但是上面这种机制也只是保证共享变量的可见性,但是不能保证对共享变量操作的互斥性?这是为什么?
运行在不同CPU Core的线程同时去访问一个volatile
变量,这里就以上图中左面的两个Core为例去访问内存中的数据,左边Core对应的线程为Thread1,右边的Core对应的是Thread2。在这样一个情景中:当Thread1去内存中读取数据并修改,然后写回内存,但是当Thread1还没有将该数据写回内存,Thread2已经将该变量读取到自己的Core中进行计算,而此时Thread2其实想要的是读取最新的变量数据然而获取的缺失旧的,在Thread2读取完毕后,Thread1也将该变量写回到内存中,之后Thread2计算完成后也将该变量写回到内存中,这时候就出现问题,由于Thread2获取的值是旧的,所以他的计算是不正确的,同时该值也将Thread1的值覆盖。这种情况和数据库事务中的脏读情况是类似的。
从以上的分析中可以确认volatile变量是不能单独保证线程互斥性的,其实它的最典型的用法是最来作为状态标志位,由一个线程去操作,然后其他线程根据该标志位来执行相应的动作,这种有点类似于广播的形式。