【Java多线程】volatile关键字

本文详细介绍了Java并发编程中的三个基本概念:原子性、可见性和有序性,并重点讨论了volatile关键字的作用。volatile保证了变量的可见性,但不保证原子性,它可以防止指令重排序,确保在多线程环境下一定的有序性。文章通过实例解释了volatile的使用场景及注意事项,指出其在标志位和单例模式中的应用,并强调了其在并发编程中的限制。
摘要由CSDN通过智能技术生成

简介

volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

并发编程的3个基本概念

1.原子性

 定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。

(2)所有引用reference的赋值操作

(3)java.concurrent.Atomic.* 包中所有类的一切操作

2.可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

使用

保证可见性,不保证原子性

  • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  • 这个写会操作会导致其他线程中的volatile变量缓存无效。
    volatile修饰变量

volatile特征

  • 保存内存可见性
  • volatile修饰的变量不会缓存到工作内存中,每一次读取获取最新volatile变量

禁止指令重排序

  • Java内存不会对volatile指令进行重排序,从而保证对volatile的执行顺序永远 是按照书写顺序执行的
    重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。
    比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
    比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

    重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

    public class TestVolatile{
    int a = 1;
    boolean status = false;
    
    //状态切换为true
    public void changeStatus{
    	a = 2;   //1
    	status = true;  //2
    }
    
    //若状态为true,则为running
    public void run(){
    	if(status){   //3
    		int b = a + 1;  //4
    		System.out.println(b);
    	}
    }
    }
    
    
    

happens-before规则:volatile字段的写入操作happen before后续同一个字段的读操作

  • volatile修饰的变量产生的汇编代码,会存在一个lock前缀,相当于一个内存屏障
  • 它确保指令重排序的时候不会将其后面的指令拍到内存屏障之前的位置,也不会
  • 前面的指令排到内存屏障之后。也就是执行到内存屏障这一指令时,在它前面的操作
  • 已经全部执行完成
  • 它会强制性的将工作内存的修改立即写入主内存中
  • 如果是写操作,它会导致其他线程对应的工作内存中的值是无效值

volatile使用场景

  • boolean标志位
  • 单例模式双重检测锁

volatile无法保证原子性

  • 例:创建10个线程,分别执行1000次i++操作,期望程序最终的结果是10000
    小于10000的结果 volatile保证i的可见性,i++不具备原子性,volatile不保证i++原子性
public class TestDemo9 {
   //public volatile static String[] array;  -> new String[10];
   //array -> new String[];
   public volatile static int i = 0;
   private static CountDownLatch countDownLatch = new CountDownLatch(10);
   public  static void increase(){
       i++;
   }
   public static void main(String[] args) {
       for(int i=0; i<10; i++){
           new Thread(){
               @Override
               public void run() {
                   for(int j=0; j<1000; j++){
                       increase();
                   }
                   countDownLatch.countDown(); //countDownLatch中计数器-1
               }
           }.start();
       }
       try {
           //调用await的线程阻塞,直到计数器为0才会继续执行
           countDownLatch.await();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println(i);

CountDownLatch的使用

  • 有一个任务想要继续执行,但是必须要等待其他任务执行完

volatile使用注意点

  • volatile对应基本数据类型才有用
  • volatile保证对象引用,保证对象内部的数据的可见性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值