当前位置: 首页 > 科技观察

图解:volatile类和原子类的异同

时间:2023-03-16 14:30:29 科技观察

本文转载自微信公众号《JerryCodes》,作者KyleJerry。转载本文请联系JerryCodes公众号。Volatile和atomic类原子类和volatile使用场景总结volatile和atomic类,我们先看一个案例。如图所示,我们有两个线程。图中左上角可以看到有一个普通的布尔标志位,初始赋值为true。然后线程2会进入一个while循环,根据这个flag的值决定是继续执行还是退出,也就是标志位。一开始,由于flag的值为true,这里会先执行一定时间的循环。那么假设在某个时候,线程1将这个标志的值更改为false,它想要的是线程2看到这个变化后停止运行。但这样做实际上是有风险的。线程2可能不会立即停止,可能会在一段时间后停止,甚至在最极端的情况下可能永远不会停止。为了理解为什么会出现这种情况,我们先来看看CPU的内存结构。下面是一个简单的双核CPU示意图:可以看出,线程1和线程2运行在不同的CPU核上,每个核都有自己的本地内存,在其下面也有它们共享的内存。一开始都能读到flag为true,但是当线程1的值改为false时,线程2无法及时看到这个修改,因为线程2不能直接访问线程1的本地内存,这样的问题是一个非常典型的可见性问题。要解决这个问题,我们只需要在变量前面加上volatile关键字修饰即可。只要我们加上this关键字,变量每次修改,其他线程就可以看到,这样一旦线程1改变了这个值,那么线程2就可以马上看到,这样就可以退出while循环了。之所以可以在加关键字后使其可见,是因为有了这个关键字,线程1所做的修改会被flush到共享内存中,然后再刷新到线程2的本地内存中,这样线程2就可以感觉到了变化,所以主要用关键字volatile来解决可见性问题,可以在一定程度上保证线程安全。下面我们来回顾下熟悉的多线程value++场景,如图:如果初始化为每个线程加1000次,最后的结果可能不是2000。由于value++不是原子的,所以会存在线程安全问题多线程的情况。但是如果我们这里使用volatile关键字,是不是就可以解决问题呢?不幸的是,即使使用volatile,也无法保证线程安全,因为这里的问题不仅仅是可见性问题,还有原子性问题。我们有很多方法可以解决这里的问题。第一种是使用synchronized关键字,如图:这样两个线程不能同时改变value的值,保证了value++语句的原子性,同时Synchronized也保证了可见性,即,当第一个线程修改值时,第二个线程可以立即看到这次修改的结果。解决这个问题的第二种方法是使用我们的原子类,如图:比如使用一个AtomicInteger,然后每个线程调用它的incrementAndGet方法。使用原子变量后,就不需要加锁了。我们可以使用它的incrementAndGet方法。这个操作底层由CPU指令保证原子性,所以即使多个线程同时运行,也不会有线程安全问题。原子类和volatile的使用场景我们可以看到volatile和原子类的使用场景是不一样的。如果我们有可见性问题,我们可以使用volatile关键字,但是如果我们的问题是组合操作,我们需要使用同步来解决原子性问题,那么可以使用原子变量来代替volatile关键字。通常可以使用volatile来修饰boolean类型的标志位,因为对于标志位来说,直接赋值操作本身就是原子的,而volatile保证了可见性,所以是线程安全的。小结对于Counter这种会被多个线程同时操作的场景,这种场景的一个典型特点就是不只是简单的赋值操作,而是需要先读取当前值,然后在此基础上进行进行某些修改,然后将其分配回去。这样的话,我们的volatile在这种情况下是不足以保证线程安全的。我们需要使用原子类来保证线程安全。