java中关键字volatile的作用

本文深入解析Java中volatile关键字的作用原理,对比synchronized关键字的区别,并通过示例代码展示如何正确使用volatile保证多线程环境下变量的可见性和原子性。

     用在多线程,同步变量。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A)

 

=========================分割线1=================================

在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;
  }
}
假如pleaseStop没有被声明为volatile,线程执行run的时候检查的是自己的副本,就不能及时得知其他线程已经调用tellMeToStop()修改了pleaseStop的值。
 
Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。如果配合Java 5增加的atomic wrapper classes,对它们的increase之类的操作就不需要sychronized。
 
Reference:

=========================分割线2=================================

 

  恐怕比较一下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()得到存储在当前线程中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);
    }
}


     如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别 的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子 操作:

  =   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);
    }
}


    上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字时要慎 重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。

<think> 首先,用户的问题是关于Java关键字volatile的详细介绍和使用场景。我需要基于提供的引用内容来回答,并遵循系统级指令。 系统级指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 - 参考站内引用:[^1]、[^2]、[^3]、[^4]。 从引用内容: - [^1]:讨论volatile关键字Java内存模型相关概念,实现原理和使用场景。 - [^2]:讲解volatile作用、工作原理、应用场景,与其他同步机制的对比。 - [^3]:强调volatile保证可见性,避免脏读现象。 - [^4]:提到使用volatile替代锁的条件:写操作不依赖当前值,变量不包含在其他变量的不变式中。 用户的问题是:"请问Java关键字volatile的详细介绍和使用场景"。所以,我需要提供volatile的详细解释、工作原理和使用场景。 结构回答: 1. **介绍volatile关键字**:简要概述它的作用。 2. **工作原理**:解释它如何工作,涉及Java内存模型。 3. **使用场景**:列出常见的使用场景。 4. **注意事项**:讨论其限制和与其他同步机制的对比。 5. **生成相关问题**:在最后生成相关问题。 确保使用中文回答。 逐点结构化: - **步骤1: 概述**:定义volatile关键字及其主要目的——保证可见性和有序性。 - **步骤2: 基本原理**:解释Java内存模型中的可见性问题,volatile如何解决它。 - 提到可见性(visibility)和内存屏障(memory barriers)。 - **步骤3: 详细特性**: - 可见性:volatile修饰的变量,修改后立即对其他线程可见。 - 有序性:volatile变量防止指令重排序。 - **步骤4: 使用场景**: - 状态标志:如引用[^3]的例子,isShutDown标志。 - 双重检查锁定(Double-Checked Locking)在单例模式中。 - 当变量写操作不依赖当前值时(引用[^4])。 - 其他场景如共享变量的简单读写。 - **步骤5: 限制和注意事项**: - volatile不能保证原子性(例如,i++操作不是原子的)。 - 不能用于复合操作或依赖于当前值的操作。 - 与synchronized、Atomic类对比。 - **步骤6: 总结**:简要总结适用场景。 在回答中自然地添加引用标识。例如: - 当提到可见性时,引用[^3]。 - 当提到使用条件时,引用[^4]。 - 其他部分引用[^1]或[^2]。 最后,生成相关问题: - §§相关问题§§ - 例如: 1. volatile关键字如何保证可见性和有序性? 2. volatile与synchronized的区别? 3. 在哪些情况下不应该使用volatile? 4. 如何用volatile实现线程安全的单例模式? 数学表达式:在这个上下文中,可能不需要数学表达式,因为主题是编程。但如果有必要,比如讨论原子操作时,可以用行内数学格式,但尽量避免,除非相关。系统指令说必须使用格式,如果没有数学内容,不需要强行添加。 确保LaTeX语法正确:如果有变量或代码,用行内代码格式(用户层指令中有代码块示例),但数学表达式用$...$。这里可能用不到太多数学。 回答结构: 1. **概述**:介绍volatile关键字。 2. **作用**:详细解释其作用。 3. **工作原理**:基于Java内存模型。 4. **使用场景**:列出示例。 5. **限制**:讨论条件。 6. **相关问题**:生成。 现在,草拟回答。 </think> ### Java关键字volatile的详细介绍和使用场景 volatileJava 中的一个重要关键字,主要用于在多线程环境下保证变量的可见性和有序性。它通过解决 Java 内存模型(JMM)中的问题来实现线程安全,但并非万能,需要谨慎使用以避免常见的并发错误。下面我将基于核心概念和引用内容,逐步为您解析 volatile 的详细信息。 #### 1. **volatile 关键字的概述** volatile 关键字修饰的变量,可以确保对该变量的读写操作对所有线程是“可见的”(visibility)。当一个线程修改了 volatile 变量的值,这个变化会立即刷新到主内存中,其他线程读取时也能获取最新值,避免了“脏读”(dirty read)问题。例如,在引用[^3]中描述的示例:如果 `isShutDown` 变量未用 volatile 修饰,线程可能读取到过期的值,导致逻辑错误;而使用 volatile 后,修改后的值对所有线程立即可见[^3]。volatile 还提供了一定的“有序性”(ordering),防止编译器和处理器对指令进行重排序优化[^1][^2]。 #### 2. **volatile 的工作原理** volatile 的工作原理基于 Java 内存模型(JMM)的核心机制: - **可见性保证**:在 JMM 中,每个线程都有自己的工作内存(缓存),普通变量的修改可能只在工作内存中暂存,不立即同步到主内存。使用 volatile 修饰变量时,任何写操作都会: 1. 强制将变量的值写入主内存。 2. 使其他线程的工作内存中该变量的缓存失效,强制它们从主内存重新读取。 这通过内存屏障(memory barriers)实现,确保变量的修改对所有线程立即可见[^1][^2]。 - **有序性保证**:volatile 通过禁止指令重排序(reordering)来保证操作顺序。例如,在 volatile 写操作前后的指令不会被重排序到写操作之后,读操作前后的指令不会被重排序到读操作之前。这类似于插入了一个轻量级的“内存栅栏”(fence),防止多线程环境下的乱序执行导致的数据不一致问题[^1][^2]。 然而,volatile 并不保证原子性(atomicity)。例如,对于复合操作(如 `i++`,它包含读取、修改和写入),volatile 无法阻止多个线程同时执行导致的竞态条件(race condition)。如果需要原子性,应使用 `synchronized` 或 `java.util.concurrent.atomic` 包中的原子类[^4]。 #### 3. **volatile 的主要使用场景** volatile 适用于一些特定场景,特别是在满足以下两个条件时(引用[^4]): - 对变量的写操作不依赖于当前值(例如,简单的赋值而非计算)。 - 该变量没有包含在其他变量的不变式(invariant)中(即变量之间没有依赖关系)。 常见的使用场景包括: - **状态标志(status flags)**:用于控制线程的执行流。例如,一个线程安全的状态变量作为“开关”。 ```java public class ShutdownHandler { private volatile boolean isShutdown = false; // 使用 volatile 保证可见性 public void doWork() { while (!isShutdown) { // 执行任务 } } public void shutdown() { isShutdown = true; // 修改后,所有线程立即可见 } } ``` 在这个例子中,`isShutdown` 作为 volatile 变量确保线程能及时退出循环,避免无限循环问题[^3][^4]。 - **双重检查锁定(Double-Checked Locking)**:在单例模式中优化性能,确保懒加载(lazy initialization)的线程安全。 ```java public class Singleton { private static volatile Singleton instance; // 使用 volatile 防止指令重排序 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // volatile 保证初始化顺序正确 } } } return instance; } } ``` 这里,volatile 解决了 JMM 中对象初始化过程中的重排序问题,确保其他线程不会获取到未完全初始化的对象[^1][^2]。 - **一次性发布(one-time publication)**:当共享变量只需要被初始化一次且后续只读时。例如,配置参数的加载。 ```java public class ConfigLoader { private volatile Map<String, String> config; // volatile 保证可见性 public void loadConfig() { Map<String, String> temp = new HashMap<>(); // 加载配置... config = temp; // volatile 确保赋值操作对所有线程可见 } public String getConfig(String key) { return config.get(key); // 线程安全读取 } } ``` 在这个场景中,volatile 简化了同步,避免使用锁的开销[^4]。 - **简单计数器或标志位**:当变量的操作是独立的读写,且不涉及复杂计算。例如,监控线程的状态变量。 在这些场景中,volatile 提供了一种轻量级的同步机制,比 `synchronized` 锁更高效(因为它不涉及上下文切换),但仅适用于特定简单情况[^2][^4]。 #### 4. **volatile 的局限性及与其他机制的对比** - **局限性**: - 原子性问题:volatile 无法保证复合操作的原子性。例如,`volatile int count = 0; count++;` 在多线程下仍可能出错(因为 `count++` 是非原子操作)。 - 依赖条件严格:如引用[^4]强调,volatile 只能用于写操作不依赖当前值的场景。如果变量需要与其他变量保持一致(如不变式),应使用更强的同步机制[^4]。 - 性能开销:虽然轻量,但 volatile 读写涉及内存屏障,可能引入轻微性能损耗(通常优于锁,但不如普通变量)。 - **与 synchronized 和原子类的对比**: - **synchronized**:提供互斥锁,保证原子性、可见性和有序性,但开销较大。适合复杂同步块。 - **Atomic 类**(如 `AtomicInteger`):使用 CAS(Compare-And-Swap)操作保证原子性,适用于计数器等场景。 - volatile 在简单可见性和有序性需求下更高效,但需权衡原子性需求[^2][^4]。 总之,volatileJava 并发工具中的重要组成部分,它在特定场景下简化了线程安全编程,但开发者必须理解其适用边界,避免误用导致的并发错误[^1][^2][^3][^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值