近些天,学习模拟实现线程池,仅仅听到线程两个字,就让人不寒而栗。刚开始接触多线程编程,确实很难,多线程编程要结合很多很多的计算机底层知识,如操作系统,计算机组成原理等。这篇博客,是我对volatile关键字的一个初探。
首先,开始的代码是这样的(代码很简单,很短,所以,可以认真看看)
ThreadOperate类,我自己定义的一个类,实现了Runnable接口
public class ThreadOperate implements Runnable {
private static int id;
private boolean goon;
private int currentId;
public ThreadOperate() {
goon = true;
currentId = ++id;
new Thread(this, "Thread-" + currentId).start();
}
public void closeThread() {
System.out.println("收到结束线程【" + Thread.currentThread().getName() + "】的指令");
goon = false;
}
@Override
public void run() {
System.out.println("线程【" + Thread.currentThread().getName() + "】开始执行");
while (goon) {
int sum = 0;
int i;
for (i = 0; i < 10; i++) {
sum += i;
}
i = sum;
}
System.out.println("线程【" + Thread.currentThread().getName() + "】已经结束!");
}
}
Test类:
public class Test {
public static void main(String[] args) {
// 创建三个线程
ThreadOperate[] to = new ThreadOperate[3];
for (int i = 0; i < to.length; i++) {
to[i] = new ThreadOperate();
System.out.println(i + "---" + to[i].hashCode());
}
// 结束线程
for (int i = 0; i < to.length; i++) {
try {
Thread.sleep(2000);
System.out.println("准备结束【" + i + "---" + to[i].hashCode() + "】线程");
to[i].closeThread();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
看吧,看完代码后,不妨结合自己对线程的理解,猜一猜程序的执行结果是什么样的?我刚开始认为,创建3个线程,令goon等于false,然后线程结束。可是,事实并不是这样, 来一起看一看结果。
注意看,左边的停止按钮一直处于红色状态,说明程序一直在运行,如果你的电脑配置不是很好,你也可以明显的听到电脑风扇加速转动的声音。。。
我的理解:程序开始创建了三个线程,接下来的时间里,三个线程开始并发执行。run()方法里面的while(goon)一直死循环执行,通过new ThreadOperate()操作,使goon为true,但goon没有volatile关键字,会被编译器优化,goon的值被存到cpu缓存里面去。即每个线程读取goon变量的值时,都会从cpu缓存里面读取,所以,读取到的goon值永远都是true,所以会一直死循环,线程不会结束。
那么,到底如何解决这个问题呢,说实话,很简单,只需要给goon加上volatile关键字即可。所有,我们一起来看一看volatile关键字,以下内容为我百度整理而来。
volatile关键字
1、volatile定义的变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
2、一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,可见性是指,在多线程环境,共享变量的操作对于每个线程来说,都是内存可见的,也就是每个线程获取的volatile变量都是最新值;并且每个线程对volatile变量的修改,都直接刷新到主存。
2)禁止进行指令重排序。
3、在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
4、当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。如下图
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
5、volatile
会在生成的字节码中加入一条lock
指令,这个指令有两个作用:
- 将当前处理器缓存写回到内存
- 使其它线程的cpu缓存失效
这样其它线程再次读取该变量的时候就会重新去内存中获取,得到最新值。
经过以上阅读,我们就可以对程序进行修改了。给goon增加volatile关键字,使goon的值从cpu缓存写回到内存中(写回到内存,读取速度当然没有在高速cache的读取速度快)。这样,当给goon赋值为true时,就是把内存中的goon值改为true,要关闭线程时,就给goon赋值为false时,即把内存中的goon改为false,这样,在while循环执行的时候,是从内存中读取goon的值,自然就不会造成死循环了。
来看结果:
可以清楚的看到,小红按钮变灰了!即线程结束了!!
以下是从汇编角度进行分析:
while (goon) {
int sum = 0;
int i;
for (i = 0; i < 10; i++) {
sum += i;
}
i = sum;
}
假设i这个局部变量的偏移量为-4,i的首地址应该是edp[-4]。
i++对应的汇编语言:
mov cx, edp[-4]
loop:
inc cx
cmp cx, 10
jl loop:
可以看出,每次都是i的首地址对应的值和10比较。i这个局部变量并没有做任何的操作,寄存器cx只是接收了i的首地址,然后就自己玩去了,根本不管i的死活。
如果对变量i增加volitale关键字,则,将使用下面的汇编:
loop:
mov ecx, edp[-4]
inc ecx
mov edp[-4], ecx
cmp ecx, 10
jl loop
以上代码可以理解为这样的:
ecx = i;
++ecx;
i = ecx;
if (ecx < 10) goto loop:
从汇编的角度,可以对volatile关键字进行更深一步的理解。
volatile的本质就是,禁止变量的寄存器优化!
如有错误,还请指点或评论。