本文标题大纲:
前言
在前一篇文章Java 多线程(4)—线程的同步(中) 我们看了一下如何使用 ReentrantLock
类和 synchronized
关键字来控制在多个线程并发执行的同步,并通过他们解决了我们之前留下的关于线程通过带来的一些问题。这篇是线程同步文章的最后一篇,我们来一下一些新的知识点:
volatile 关键字
首先我们来看一下 Java 中另一个和多线程有关的关键字: volatile
,这个关键字是在定义变量的时候作为变量修饰符使用的。这里有一个规律:用 volatile
修饰的变量,在线程中被修改之后会立刻同步到主内存中。用该关键字修饰的变量可以保证在任意时刻,某个线程从主内存中取该变量的值总是最新的。
这个其实就是 volatile
的第一个作用:保证其修饰变量在不同线程之间的可见性。
为什么会有这个规律呢?在这里还得提一下我们在 Java 多线程(3)— 线程的同步(上) 中提到的 Java 规定的用来完成线程工作内存和主内存数据交互的 8 种原子性
的操作。其中就包括了这几个:read
、load
、store
和 write
,我们来看一下它们的作用:
read 操作:作用于主内存中的变量,其把主内存的一个变量的值传输到线程的工作内存中,供接下来的 load 操作使用
load 操作:其把 read 操作从主内存中得到的变量值复制到当前线程工作内存的变量副本中
store 操作:作用于线程工作内存的变量,其把工作内存中的变量的值传输到主内存中,供接下来的 write 操作使用。
write 操作:作用于主内存的变量,其把 store 操作从线程工作内存得到的变量值写入主内存的变量中
这两个过程用这张图来说明是在合适不过了:
用 volatile
关键字修饰的变量,在线程的工作内存中使用之前一定会通过 load
操作来将变量的值从主内存读取到线程的工作内存中,而在修改完成之后一定会通过 store
操作来将修改后变量的值刷新到主内存中。那么之后其他线程要从主内存中取这个变量的值肯定是修改之后的值,即为最新值。这样的话就保证了用 volatile
关键字修饰的变量在每一次线程从主内存中读取的总是最新值。
volatile 关键字能保证原子性吗?
那么讲到这里可能有小伙伴会问了,既然用 `volatile` 关键字修饰的变量可以保证 Java 线程每次从主内存读取的值都是最新值,那么可不可以用 `volatile` 关键字来代替 `synchronized` 关键字和一些其他的锁来实现线程之间的同步呢? 答案是不可以,如果能代替的话 Java 干嘛还要提供那么多实现线程之间同步的手段。当然这个回答不具有说服力,我们还是来实践一下:我们继续上篇文章中的问题,用多个线程来实现某个变量的累加,不过现在我们把这个变量用 `volatile` 关键字修饰,来看看会发生什么:/**
* volatile 关键字的测试
*/
public static class VolatileTest {
private volatile static int sum = 0;
// 对 sum 变量进行 10000 次 +1 操作
public static void increase() {
for (int i = 0; i < 10000; i++) {
sum++;
}
}
public static int getSum() {
return sum;
}
public static void startTest() {
// 开启 10 个子线程进行 sum 变量的累加操作
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
increase();
}
}).start();
}
// 当前活跃的线程数大于 1 的时候,证明子线程累加未完全完成,此时主线程应该让出 CPU
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(getSum());
}
}
public static void main(String[] args) {
VolatileTest.startTest();
}
<