Java中关键字volatile的作用

简介:
用在多线程,同步变量。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A)
  在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。
  一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。以下例子展现了volatile的作用:
public class StoppableTask extends Thread {
private volatile boolean pleaseStop;
public void run() {
while (!pleaseStop) {
// do some stuff...
}
}
public void tellMeToStop() {
pleaseStop = true;
}
}
public class StoppableTask extends Thread {
private volatile boolean pleaseStop;
public void run() {
while (!pleaseStop) {
// do some stuff...
}
}
public void tellMeToStop() {
pleaseStop = true;
}
}
  假如pleaseStop没有被声明为volatile,线程执行run的时候检查的是自己的副本,就不能及时得知其他线程已经调用tellMeToStop()修改了pleaseStop的值。
  Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。如果配合Java 5增加的atomic wrapper classes,对它们的increase之类的操作就不需要sychronized。
 恐怕比较一下volatile和synchronized的不同是最容易解释清楚的。volatile是变量修饰符,而synchronized则作用于一段代码或方法;看如下三句get代码:
int i1;
int geti1() {return i1;}
volatile int i2;
int geti2()
{return i2;}
int i3;
synchronized int geti3() {return i3;}
geti1()
int i1;
int geti1() {return i1;}
volatile int i2;
int geti2()
{return i2;}
int i3;
synchronized int geti3() {return i3;}
geti1()
  得到存储在当前线程中i1的数值。多个线程有多个i1变量拷贝,而且这些i1之间可以互不相同。换句话说,另一个线程可能已经改变了它线程内的i1值,而这个值可以和当前线程中的i1值不相同。事实上,Java有个思想叫“主”内存区域,这里存放了变量目前的“准确值”。每个线程可以有它自己的变量拷贝,而这个变量拷贝值可以和“主”内存区域里存放的不同。因此实际上存在一种可能:“主”内存区域里的i1值是1,线程1里的i1值是2,线程2里的i1值是3——这在线程1和线程2都改变了它们各自的i1值,而且这个改变还没来得及传递给“主”内存区域或其他线程时就会发生。
  而 geti2()得到的是“主”内存区域的i2数值。用volatile修饰后的变量不允许有不同于“主”内存区域的变量拷贝。换句话说,一个变量经 volatile修饰后在所有线程中必须是同步的;任何线程中改变了它的值,所有其他线程立即获取到了相同的值。理所当然的,volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。
  既然volatile关键字已经实现了线程间数据同步,又要 synchronized干什么呢?呵呵,它们之间有两点不同。首先,synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“ 主”内存区域同步整个线程的内存。因此,执行geti3()方法做了如下几步:
  1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放)
  2. 线程内存的数据被消除,从“主”内存区域中读入(Java虚拟机能优化此步。。。[后面的不知道怎么表达,汗])
  3. 代码块被执行
  4. 对于变量的任何改变现在可以安全地写到“主”内存区域中(不过geti3()方法不会改变变量值)
  5. 线程释放监视this对象的对象锁
  因此volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
  =========================分割线3=================================
  volatile关键字相信了解Java多线程的读者都很清楚它的作用。volatile关键字用于声明简单类型变量,如int、float、 boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:
package  mythread;
public   class  JoinThread  extends  Thread
{
public   static volatile int  n  =   0 ;
public   void  run()
{
for  ( int  i  =   0 ; i  <   10 ; i ++ )
try
{
n  =  n  +   1 ;
sleep( 3 );  //  为了使运行结果更随机,延迟3毫秒
}
catch  (Exception e)
{
}
}
public   static   void  main(String[] args)  throws  Exception
{
Thread threads[]  =   new  Thread[ 100 ];
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  建立100个线程
threads[i]  =   new  JoinThread();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  运行刚才建立的100个线程
threads[i].start();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  100个线程都执行完后继续
threads[i].join();
System.out.println( " n= "   +  JoinThread.n);
}
}
package  mythread;
public   class  JoinThread  extends  Thread
{
public   static volatile int  n  =   0 ;
public   void  run()
{
for  ( int  i  =   0 ; i  <   10 ; i ++ )
try
{
n  =  n  +   1 ;
sleep( 3 );  //  为了使运行结果更随机,延迟3毫秒
}
catch  (Exception e)
{
}
}
public   static   void  main(String[] args)  throws  Exception
{
Thread threads[]  =   new  Thread[ 100 ];
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  建立100个线程
threads[i]  =   new  JoinThread();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  运行刚才建立的100个线程
threads[i].start();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  100个线程都执行完后继续
threads[i].join();
System.out.println( " n= "   +  JoinThread.n);
}
}
如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:
  n  =  n  +   1 ;
  n ++ ;
  如果要想使这种情况变成原子操作,需要使用synchronized关键字,如上的代码可以改成如下的形式:
package  mythread;
public   class  JoinThread  extends  Thread
{
public   static int  n  =   0 ;
public static   synchronized   void  inc()
{
n ++ ;
}
public   void  run()
{
for  ( int  i  =   0 ; i  <   10 ; i ++ )
try
{
inc();  //  n = n + 1 改成了 inc();
sleep( 3 );  //  为了使运行结果更随机,延迟3毫秒
}
catch  (Exception e)
{
}
}
public   static   void  main(String[] args)  throws  Exception
{
Thread threads[]  =   new  Thread[ 100 ];
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  建立100个线程
threads[i]  =   new  JoinThread();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  运行刚才建立的100个线程
threads[i].start();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  100个线程都执行完后继续
threads[i].join();
System.out.println( " n= "   +  JoinThread.n);
}
}
package  mythread;
public   class  JoinThread  extends  Thread
{
public   static int  n  =   0 ;
public static   synchronized   void  inc()
{
n ++ ;
}
public   void  run()
{
for  ( int  i  =   0 ; i  <   10 ; i ++ )
try
{
inc();  //  n = n + 1 改成了 inc();
sleep( 3 );  //  为了使运行结果更随机,延迟3毫秒
}
catch  (Exception e)
{
}
}
public   static   void  main(String[] args)  throws  Exception
{
Thread threads[]  =   new  Thread[ 100 ];
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  建立100个线程
threads[i]  =   new  JoinThread();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  运行刚才建立的100个线程
threads[i].start();
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
//  100个线程都执行完后继续
threads[i].join();
System.out.println( " n= "   +  JoinThread.n);
}
}
  上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
最新内容请见作者的GitHub页:http://qaseven.github.io/

相关文章
|
20天前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
【4月更文挑战第6天】Java中的`synchronized`关键字用于处理多线程并发,确保共享资源的线程安全。它可以修饰方法或代码块,实现互斥访问。当用于方法时,锁定对象实例或类对象;用于代码块时,锁定指定对象。过度使用可能导致性能问题,应注意避免锁持有时间过长、死锁,并考虑使用`java.util.concurrent`包中的高级工具。正确理解和使用`synchronized`是编写线程安全程序的关键。
|
2天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
15 0
|
2天前
|
Java
两千字讲明白java中instanceof关键字的使用!
两千字讲明白java中instanceof关键字的使用!
11 0
|
2天前
|
Java 开发者
Java基础知识整理,注释、关键字、运算符
在日常的工作中,总会遇到很多大段的代码,逻辑复杂,看得人云山雾绕,这时候若能言简意赅的加上注释,会让阅读者豁然开朗,这就是注释的魅力!
37 11
|
7天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
9天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
24 0
|
11天前
|
Java
Java关键字(1)
Java关键字(1)
|
1月前
|
安全 Java 编译器
Java 中的关键字
Java 中的关键字
75 0
|
1月前
|
存储 Java
【Java】深入理解Java中的static关键字
【Java】深入理解Java中的static关键字
25 0
|
1月前
|
算法 Java C++
【Java】深入理解Java中的Native关键字
【Java】深入理解Java中的Native关键字
45 0