JVM中volatile

Java内存模型(Java Memory Model)

Java内存模型(JMM),不同于Java运行时数据区,JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取数据这样的底层细节。JMM规定了所有的变量都存储在主内存中,但每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,工作内存是线程之间独立的,线程之间变量值的传递均需要通过主内存来完成。

volatile关键字

平时在阅读jdk源码的时候,经常看到源码中有写变量被volatile关键字修饰,但是却不是十分清除这个关键字到底有什么用处,现在终于弄清楚了,那么我就来讲讲这个volatile到底有什么用吧。

当一个变量被定义为volatile之后,就可以保证此变量对所有线程的可见性,即当一个线程修改了此变量的值的时候,变量新的值对于其他线程来说是可以立即得知的。可以理解成:对volatile变量所有的写操作都能立刻被其他线程得知。但是这并不代表基于volatile变量的运算在并发下是安全的,因为volatile只能保证内存可见性,却没有保证对变量操作的原子性。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
  * 发起20个线程,每个线程对race变量进行10000次自增操作,如果代码能够正确并发,
  * 则最终race的结果应为200000,但实际的运行结果却小于200000。
  *
  * @author Colin Wang
  *
  */
public class VolatileTest {
     public static volatile int race = 0 ;
 
     public static void increase() {
         race++;
     }
 
     private static final int THREADS_COUNT = 20 ;
 
     public static void main(String[] args) {
         Thread[] threads = new Thread[THREADS_COUNT];
 
         for ( int i = 0 ; i < THREADS_COUNT; i++) {
             threads[i] = new Thread( new Runnable() {
 
                 @Override
                 public void run() {
                     for ( int i = 0 ; i < 10000 ; i++) {
                         increase();
                     }
                 }
             });
             threads[i].start();
         }
         
         while (Thread.activeCount() > 1 )
             Thread.yield();
 
         System.out.println(race);
     }
}

这便是因为race++操作不是一个原子操作,导致一些线程对变量race的修改丢失。若要使用volatale变量,一般要符合以下两种场景:

  1. 变量的运算结果并不依赖于变量的当前值,或能够保证只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束。

使用volatile变量还可以禁止JIT编译器进行指令重排序优化,这里使用单例模式来举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
  * 单例模式例程一
  *
  * @author Colin Wang
  *
  */
public class Singleton_1 {
 
     private static Singleton_1 instance = null ;
 
     private Singleton_1() {
     }
 
     public static Singleton_1 getInstacne() {
         /*
          * 这种实现进行了两次instance==null的判断,这便是单例模式的双检锁。
          * 第一次检查是说如果对象实例已经被创建了,则直接返回,不需要再进入同步代码。
          * 否则就开始同步线程,进入临界区后,进行的第二次检查是说:
          * 如果被同步的线程有一个创建了对象实例, 其它的线程就不必再创建实例了。
          */
         if (instance == null) {
             synchronized (Singleton_1.class) {
                 if (instance == null) {
                     /*
                      * 仍然存在的问题:下面这句代码并不是一个原子操作,JVM在执行这行代码时,会分解成如下的操作:
                      * 1.给instance分配内存,在栈中分配并初始化为null
                      * 2.调用Singleton_1的构造函数,生成对象实例,在堆中分配
                      * 3.把instance指向在堆中分配的对象
                      * 由于指令重排序优化,执行顺序可能会变成1,3,2,
                      * 那么当一个线程执行完1,3之后,被另一个线程抢占,
                      * 这时instance已经不是null了,就会直接返回。
                      * 然而2还没有执行过,也就是说这个对象实例还没有初始化过。
                      */
                     instance = new Singleton_1();
                 }
             }
         }
         return instance;
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
  * 单例模式例程二
  *
  * @author Colin Wang
  *
  */
public class Singleton_2 {
 
     /*
      * 为了避免JIT编译器对代码的指令重排序优化,可以使用volatile关键字,
      * 通过这个关键字还可以使该变量不会在多个线程中存在副本,
      * 变量可以看作是直接从主内存中读取,相当于实现了一个轻量级的锁。
      */
     private volatile static Singleton_2 instance = null ;
 
     private Singleton_2() {
     }
 
     public static Singleton_2 getInstacne() {
         if (instance == null ) {
             synchronized (Singleton_2. class ) {
                 if (instance == null ) {
                     instance = new Singleton_2();
                 }
             }
         }
         return instance;
     }
}

变量在有了volatile修饰之后,对变量的修改会有一个内存屏障的保护,使得后面的指令不能被重排序到内存屏障之前的位置。volalite变量的读性能与普通变量类似,但是写性能要低一些,因为它需要插入内存屏障指令来保证处理器不会发生乱序执行。即便如此,大多数场景下volatile的总开销仍然要比锁低,所以volatile的语义能满足需求时候,选择volatile要优于使用锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在Java,`volatile`关键字用于声明一个变量,表示它的值可能被多个线程同时访问。 当一个变量被声明为`volatile`时,Java虚拟机会禁止进行某些优化,以确保读取该变量的值时是从内存获取的最新值。这意味着,当一个线程修改了一个`volatile`变量的值,其他线程能够立即看到这个修改。 使用`volatile`关键字时,必须注意它不能用来保证复合操作的原子性,也不能用来保证锁的语义。因此,在使用`volatile`时,应该尽可能地减少对其的读写操作。 举个例子,下面的代码使用了`volatile`变量来实现一个简单的计数器: ```java public class Counter { private volatile int count = 0; public void increment() { count++; } public int getCount() { return count; } } ``` 在这段代码,`count`被声明为`volatile`,因此每次对其进行写操作时,都会立即同步到主内存,而每次对其进行读操作时,都会从主内存获取最新值。 ### 回答2: 在Javavolatile是一种关键字,用于修饰变量。它的主要作用是保证被修饰变量在多线程环境下的可见性和禁止指令重排序。 当一个变量volatile修饰后,在一个线程修改了该变量的值后,其它线程能够立即看到最新的值,而不会使用缓存的旧值。这是因为每个线程都有自己的工作内存,volatile关键字能够告知JVM不要把这个变量缓存在工作内存,而是直接从主内存读取最新的值。 此外,volatile还具有禁止指令重排序的功能。在多线程环境,为了提高程序执行效率,JVM可能会对指令进行重排序,这样可能会导致多线程执行时出现意想不到的结果。而通过使用volatile关键字修饰变量,可以保证在多线程,它的读写操作是按顺序进行的,不会出现重排序问题。 需要注意的是,尽管volatile关键字保证了可见性和禁止指令重排序,但它并不能保证原子性,即不能保证复合操作的完整性。对于一些需要保证原子性的操作,仍然需要借助其他的手段,例如使用synchronized关键字或Lock接口进行同步控制。 总之,volatile关键字在Java是一种用于修饰变量的关键字,它可以保证变量在多线程环境下的可见性和禁止指令重排序,但不能保证原子性。 ### 回答3: Javavolatile是一种关键字,用于修饰变量。它的作用是确保线程之间对被volatile修饰变量进行操作时的可见性和有序性。 首先,volatile确保了可见性。当一个变量volatile修饰时,具有该变量的线程在修改变量的值后会立即将其更新到主内存,当其他线程要读取该变量时,会从主内存获取最新的值。这样可以有效地解决多线程并发访问变量时的不一致问题。 其次,volatile保证了有序性。在没有volatile修饰的情况下,由于Java线程的优化和重排序机制,可能会导致代码执行的顺序与预期不一致。而volatile关键字能够禁止指令重排序,保证了代码的执行顺序与程序的编写顺序一致。 然而,需要注意的是,volatile只能保证单个volatile变量的原子性操作,不能保证复合操作的原子性。例如,volatile int count = 0;语句可以保证读取和写入count的原子性,但是count++不是原子性的。 总之,volatile关键字在多线程编程起到了重要的作用。它保证了被修饰变量的可见性和有序性,使得多线程间的操作能够正确地进行。但在一些复合操作上仍然需要额外的操作来保证原子性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值