从汇编角度分析volatile关键字

        近些天,学习模拟实现线程池,仅仅听到线程两个字,就让人不寒而栗。刚开始接触多线程编程,确实很难,多线程编程要结合很多很多的计算机底层知识,如操作系统,计算机组成原理等。这篇博客,是我对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指令,这个指令有两个作用:

  1. 将当前处理器缓存写回到内存
  2. 使其它线程的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的本质就是,禁止变量的寄存器优化!

 

如有错误,还请指点或评论。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值