使用易失性关键字

几乎所有应用程序都将数据读写到计算机的主存中,考虑到性能因素,这些操作不会直接在内存中执行。CPU提供缓存内存系统,应用程序在缓存中写入数据,然后将数据从缓存移到主内存。

在多线程应用中,并发线程在不同的CPU或CPU中不同的内核中运行。当线程修改存储在内存中的变量时,是在缓存或正在运行的CPU或内核中进行修改,但无法保证修改会在何时到达主内存。如果其它线程想要读取数据值,因为此数据不在计算机主存中,所以可能不会读取修改后的值。

为了解决这个问题(还有其它解决方案,例如synchronized关键字),Java语言提供volatile关键字。此关键字为修饰符,指定变量必须始终从主内存而不是CPU的缓存中读取并存储。当其它线程对变量的实际值具有可见性更重要时,应该使用volatile关键字,但访问此变量的顺序并不重要。在此场景中,volatile关键字提供更好的性能,因为它不需要任何监视器或锁来访问变量。与之相反,如果变量的访问顺序很重要,则必须使用另一种同步机制。

本节将学习如何使用易失性关键字及使用效果。

准备工作

本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。

实现过程

通过如下步骤实现范例:

  1. 创建名为Flag的类,包含名为flag的公有Boolean属性,初始值为true:

    public class Flag {
    	public boolean flag = true;
    }
    
  2. 创建名为VolatileFlag的类,包含名为flag的公有Boolean属性,初始值为true。在属性声明上添加volatile修饰符:

    public class VolatileFlag {
    	public volatile boolean flag = true;
    }
    
  3. 创建名为Task的类,指定其实现Runnable接口。包含私有Flag属性以及初始化此属性的构造函数:

    public class Task implements Runnable{
    	private  Flag flag;
    	public Task(Flag flag) {
    		this.flag = flag;
    	}
    
  4. 实现此任务的run()方法。当flag属性值为true时,int变量加1。然后输出变量的最终结果到控制台:

    	@Override
    	public void run() {
    		int i = 0 ;
    		while (flag.flag) {
    			i++;
    		}
    		System.out.printf("Task: Stopped %d - %s\n", i, new Date());
    	}
    }
    
  5. 创建名为VolatileTask的类,指定其实现Runnable接口。包含私有VolatileFlag属性以及初始化此属性的构造函数:

    public class VolatileTask implements Runnable{
    	private VolatileFlag flag;
    	public VolatileTask(VolatileFlag flag) {
    		this.flag = flag;
    	}
    
  6. 实现此任务的run()方法,代码与Task类相同,不在这里列出:

    	@Override
    	public void run() {
    		int i = 0 ;
    		while (flag.flag) {
    			i++;
    		}
    		System.out.printf("VolatileTask: Stopped %d - %s\n", i, new Date());
    	}
    }
    
  7. 实现包含main()方法的Main类。首先,创建VolatileFlag、Flag、VolatileTask和Task类的四个对象:

    public class Main {
    	public static void main(String[] args) {
    		VolatileFlag volatileFlag=new VolatileFlag();
    		Flag flag=new Flag();
    		VolatileTask vt=new VolatileTask(volatileFlag);
    		Task t=new Task(flag);
    
  8. 然后,创建两个线程执行这些任务,启动线程,休眠主线程1秒钟:

    		Thread thread=new Thread(vt);
    		thread.start();
    		thread=new Thread(t);
    		thread.start();
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    
  9. 接下来改变volatileFlag变量值来停止volatileTask执行,并且休眠主线程1秒钟 :

    		System.out.printf("Main: Going to stop volatile task: %s\n",new Date());
    		volatileFlag.flag=false;
    		System.out.printf("Main: Volatile task stoped: %s\n", new Date());
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    
  10. 最后,改变task对象值来停止任务执行,并且休眠主线出1秒钟:

    		System.out.printf("Main: Going to stop task: %s\n", new Date());
    		flag.flag=false;
    		System.out.printf("Main: Volatile stop flag changed: %s\n", new Date());
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }
    

工作原理

下图显示本范例的输出结果:

pics/07_05.jpg

因为task线程还没有结束,所以此应用并没有完成执行。当改变volatileFlag值时,因为flag属性标记为volatile,所以新值被写入到主内存中,且VolatileTask立即访问这个值并结束执行。与之相反,当改变flag对象值时,因为flag属性未标记为volatile,所以新值存储到主线程的缓存中,并且任务对象无法看到新值,始终不会结束执行。volatile关键字非常重要,不仅因为它需要刷新写操作,而且确保读取不会被缓存,并且从主内存中获取最新的值。volatile非常重要,但也经常被忽视。

考虑到volatile关键字保证修改是在主内存中写入的,但其相反情况并不总是正确的。 例如,处理被多个线程共享且做出大量修改的非易失性整型值,可能会看到其他线程所做的修改,因为它们是在主内存中写入的。但是,不能保证这些更改是从缓存传递到主内存中的。

扩展学习

只有当共享变量的值仅被一个线程修改时,volatile关键字才能正常工作。如果变量被多个线程修改,则volatile关键字无法在数据竞争条件下提供保护。 它也对操作如+或-、原子操作起作用,例如,作用在非易失性变量上的++操作符是线程不安全的。

自从Java 5,Java内存模型用volatile关键字建立了前置保证,包含两个特性:

  • 当修改易失性变量时,变量值发送到主内存。也将发送之前由同一线程修改的所有变量值。
  • 编译器不能重新排序那些为了优化目的而修改volatile变量的语句。它可以重新排序以前和之后的操作,但是不能修改volatile变量,这些修改之前发生的更改将对这些指令可见。

更多关注

  • 本章“使用原子变量”和“使用原子数组”小节
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值