volatile关键字

1.volatile简介:
volatile 是 JVM 提供的轻量级的同步机制。volatile 关键字可以保证并发编程三大特征(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性。

2.三大特性
1>.保证可见性:
加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。

volatile语义实现原理:

先来看两个与CPU相关的专业术语:

内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。
volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

写volatile时处理器会将缓存写回到主内存。
一个处理器的缓存写回到内存,会导致其他处理器的缓存失效

验证保证可见性

package com.gao;

/**
 * @ClassName:
 * @Description:
 * @Author: Gaoyubo
 * @Date
 * @Version: 1.0
 */
public class Test1 {
//这里直接加上了volatile关键字,可以看倒程序是可以正常执行的,但是如果去掉,main线程感知不到number值的变化,程序将会卡住
    volatile int  number = 0;

    public void add(){
        this.number = 10;
    }

    public static void main(String[] args) {
        Test1 test1 = new Test1();

        //创建第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test1.number);

            try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
            test1.add();//暂停3秒后,修改number的值。
            System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test1.number);

        },"Thread_One").start();


        //第二个是main线程
        while (test1.number == 0){
            //如果第二个main线程 可以监测到number值的改变,就会跳出当前循环,执行后续程序。
        }

        System.out.println(Thread.currentThread().getName()+"程序结束!");

    }
}

 ​ 原子性

​ 原子性指的是,当某个线程正在执行某件事情的过程中,是不允许被外来线程打断的。也就是说,原子性的特点是要么不执行,一旦执行就必须全部执行完毕。而volatile是不能保证原子性的,即执行过程中是可以被其他线程打断甚至是加塞的。

​ 所以,volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作的。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。

验证不保证原子性 

package com.gao;

import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @ClassName:
 * @Description:
 * @Author: Gaoyubo
 * @Date
 * @Version: 1.0
 */
@SpringBootTest
public class CASTest {
// volatile int number = 0;
private volatile static AtomicInteger number=new AtomicInteger(0);
//synchronized
    public void add(){
        //使用自旋锁加cas实现   unsafe是java用来操作内存的
       number.getAndIncrement();
//       number++;  //不是原子性操作
    }
    @Test
    public void test(){
        CASTest casTest=new CASTest();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    casTest.add();
                }
            },"Thread"+i).start();
        }
        //如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
        while(Thread.activeCount() >2){
            Thread.yield();//礼让其他线程,暂不执行后续程序
        }
        System.out.println(casTest.number
        );
    }
}

正常使用volatile是不保证原子性的,不过可以使用在方法上加上synchronized关键字来加锁保证原子性。或者使用原子类来保证原子性 

Thread.tield()

Thread.yield()方法是Thread类中的静态方法,直接由类名调用。

 yield表示让步、放弃的意思。

  • Thread.yield() 方法,使当前线程由执行状态,变成为就绪状态,让出CPU,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。

  • 《Java编程思想》中这样描述:Thread,yield()方法的调用,是对线程调度器(Java线程计制的一部分,可以将CPU从一个线程转移到另一个线程)的建议,它在声明:“我已经执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机。”这完全是选择性的。

  • 《JDK11中文手册》中这样描述:向调度程序提示当前线程愿意让出其当前使用的处理器。 调度程序可以忽略此提示。

  Yield是一种启发式尝试,用于改善线程之间的相对进展,否则会过度利用CPU。 它的使用应与详细的分析和基准测试相结合,以确保它实际上具有所需的效果。

  使用此方法很少合适。 它可能对调试或测试目的很有用,它可能有助于重现因竞争条件而产生的错误。 在设计并发控制结构(例如java.util.concurrent.locks包中的结构)时,它也可能很有用。

class YieldThread implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
            if (i == 3) {
                Thread.yield();
            }
        }
    }
}
public class YieldDemo {
    public static void main(String[] args) {
        YieldThread yThield = new YieldThread();
        new Thread(yThield,"A").start();
        new Thread(yThield,"B").start();
    }
}
  • 根据运行情况得出结论:

    通过对比使用Thread.yield()方法和未使用的运行结果,可以发现使用yield()方法后,很大概率上出现让出CPU给其它线程执行的情况。

  class YieldThread implements Runnable {
      public void run() {
          for (int i = 0; i < 5; i++) {
              System.out.println(Thread.currentThread().getName() + "-" + i);
              if (i == 3) {
                  Thread.yield();
             }
         }
     }
}
 public class YieldDemo {
     public static void main(String[] args) {
         YieldThread yThield = new YieldThread();
         Thread t1 = new Thread(yThield,"t1");
         Thread t2 = new Thread(yThield,"t2");
         t1.setPriority(1);
        t2.setPriority(10);
        System.out.println("t1.getPriority()"+t1.getPriority());
         System.out.println("t2.getPriority()"+t2.getPriority());
        t1.start();
        t2.start();
     }
 }
  • 根据运行情况得出结论:

    实际运行中发现,Thread.yield()方法并不是仅仅选择让步于同等或者更高优先级的线程。高优先级的线程也会让步与低优先级的线程。因此高优先级仅仅是线程获得的CPU时间片更多一些,相对执行到的机会更大,并不是一定先执行。

2>.保证有序性(禁止指令重排序)
简单说明:

计算机在执行程序时,为了提高计算性能,编译器和处理器常常会对指令进行重排序,一般分为如下3种:

源代码 ——> 编译器优化的重排 ——> 指令并行的重排 ——>内存系统的重排 ——> 最终执行的指令


解释说明:

·单线程环境下,可以确保程序最终执行结果和代码顺序执行结果的一致性(单线程环境下不用关注指令重排,因为是否重排都不会出错)。处理器在进行重排序时,必须要考虑指令之间的数据依赖性。

·多线程环境中,线程交替执行。由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也就无法预测。而用volatile关键字修饰的变量,可以禁止指令重排序,从而避免多线程环境下,程序出现乱序执行的现象。

有序性的实现原理:
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。

volatile通过加内存屏障来实现禁止指令重排序。JMM为volatile加内存屏障有以下4种情况:

在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。

在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。

在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。

在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。

volatile写是在前面和后面分别插入内存屏障,而volatile 读操作是在后面插入两个内存屏障。

内存屏障    解释说明

StoreStore屏障  禁止上面的普通写和下面的volatile 写重排序。
StoreLoad屏障 防止上面的volatile '写与下面可能存在的volatile 读/写重排序。
LoadLoad屏障    禁止下面所有的普通读操作和上面的volatile读重排序。
LoadStore屏障    禁止下面所有的普通写操作和上面的volatile读重排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值