赞
踩
volatile是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。
Java语言规范对volatile的定义:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
操作系统语义
计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。 CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。 有了CPU高速缓存虽然解决了效率问题, 但是它会带来一个新的问题:数据一致性。
在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中, 在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。
举一个简单的例子:i++
当线程运行这段代码时,首先会从主存中读取i( i = 1),然后复制一份到CPU高速缓存中, 然后CPU执行 + 1 (i = 2)的操作,然后将数据(i = 2)写入到缓存中,最后刷新到主存中。 其实这样做在单线程中是没有问题的,有问题的是在多线程中。
如下: 假如有两个线程A、B 都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3, 但事实是这样么?
分析如下:
两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中, 最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。 这种现象就是缓存一致性问题。
解决缓存一致性方案有两种:
1.通过在总线加LOCK#锁的方式
2.通过缓存一致性协议
但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行, 其他CPU都得阻塞,效率较为低下。
第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。
其核心思想如下:
当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的, 因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
在并发编程中我们一般都会遇到这三个基本概念:原子性、可见性、有序性。
1.原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性就像数据库里面的事务一样,他们是一个团队,同生共死。
示例: (1)i=0; (2)j=i; (3)i++; (4)i=j+1; 这四个操作,那些是原子性?
(1)在Java中,对基本数据类型的变量和赋值操作都是原子性操作;
(2)包含了两个操作:读取i,将i值赋值给j;
(3)(4)包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;
在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同, Java只保证了基本数据类型的变量和赋值操作才是原子性的 (注:在32位的JDK环境下,对64位数据的读取不是原子性操作,如long、double)。 要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。 注意: volatile是无法保证复合操作的原子性
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。 Java提供了volatile来保证可见性。 当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中, 当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。
3.有序性
即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果, 但是对多线程会有影响。 Java提供volatile来保证一定的有序性。
最著名的例子就是单例模式里面的DCL(双重检查锁)。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。
在JVM底层volatile是采用“内存屏障”来实现的。
1.保证可见性、不保证原子性
2.禁止指令重排序
在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:
1.编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2.处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。
原则:happens-before
happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before 原则中推导出来,那么他们就不能保证有序性,可以随意进行重排序。
定义:
1.同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是, 在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说, 这一是规则无法保证编译重排和指令重排)。
2.监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
3.对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
4.线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
5.线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
6.如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。
JVM是如何禁止重排序?
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时, 会多出一个lock前缀指令。 lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。
volatile的底层就是通过内存屏障来实现的。
1.对变量的写操作不依赖当前值;
2.该变量没有包含在具有其他变量的不变式中。
volatile经常用于两个两个场景:状态标记两、double check
- package com.yl.springboottest.consurrency.jmm.volatileT;
-
-
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Semaphore;
-
- /**
- * 描述: Volatile
- *
- * @author: yanglin
- * @Date: 2020-12-03-8:31
- * @Version: 1.0
- */
- public class VolatileT1 {
-
- // 线程并发数量
- private static int threadTotal = 200;
- // 请求总数
- private static int clientTotal = 5000;
-
- private static long count = 0;
-
- public static void add(){
- count++;
- }
-
- /**
- * test运行时,使用两独立的变量来保存时间,避免因使用同步而对t1,t2造成影响
- */
- long time1;
- long time2;
- volatile boolean boolValue = true;
-
-
- public static void main(String[] args) throws InterruptedException {
- /**
- * Java只保证了基本数据类型的变量和赋值操作才是原子性的
- *
- * 64位JVM只有server模式(server模式会进行更多的优化),
- * 32位JVM默认使用client模式,我将32位JVM设置为server模式后,问题同样出现
- */
- ExecutorService executorService = Executors.newCachedThreadPool();
- // 信号
- final Semaphore semaphore = new Semaphore(threadTotal);
- // 每次固定数量的线程获取许可
- final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
- for (int index = 0; index < clientTotal ; index++) {
- executorService.execute(new Runnable() {
- @Override
- public void run() {
- try {
- // 获取一个许可
- semaphore.acquire();
- add();
- // 释放一个许可
- semaphore.release();
- } catch (Exception e) {
- e.printStackTrace();
-
- }// 计数减一
- countDownLatch.countDown();
- }
- });
-
- }
- // 阻塞当前线程,直到计时器的值为0
- countDownLatch.await();
- executorService.shutdown();
- System.out.println("计数 num : " +count);
-
- System.out.println("----------------------------抢票场景中-------------");
- /** 创建线程 */
- TicketRunnable ticketRunnable=new TicketRunnable();
- // 第1个人
- Thread t1=new Thread(ticketRunnable,"张三");
- // 第2个人
- Thread t2=new Thread(ticketRunnable,"李四");
- // 第3个人
- Thread t3=new Thread(ticketRunnable,"王五");
- // 第4个人
- Thread t4=new Thread(ticketRunnable,"赵六");
- // 第5个人
- Thread t5=new Thread(ticketRunnable,"田七");
- /** 启动线程 */
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- t5.start();
-
- /**
- * Java虚拟机JVM是64位JVM还是32位
- */
- System.out.println("Java虚拟机JVM是64位JVM还是32位::"+System.getProperty("sun.arch.data.model"));
-
- /**
- * boolValue 变量是我们要验证的 volatile 变量。一开始 boolValue 初始化为 true,
- * 其次启动 t2 线程让其进入死循环;接着,t1 线程启动,并且让 t1 线程先执行,
- * 将 boolValue 改为 false。理论上来讲,此时 t2 线程应该跳出死循环,但是实际上并没有。
- * 此时 t2 线程读到的 boolValue 的值仍然为 true。所以这段程序一直没有打印出结果。
- * 这便是多线程间的不可见性问题,官方话术为: 线程 t1 修改后的值对线程 t2 来说并不可见。
- */
- // 测试个数
- int size = 5000;
- VolatileT1 vs[] = new VolatileT1[size];
- long timeSum = 0;
- for(int n = 0; n < size; n++){
- (vs[n] = new VolatileT1()).test();
- }
- /**
- * 统计出,所有线程从boolValue变为false到while(boolValue)跳出所花时间的总和
- */
- for(int n = 0; n < size; n++){
- timeSum += vs[n].time2 - vs[n].time1;
- System.out.print(n+"\t"+vs[n].time2 +'\t' + vs[n].time1+'\t'+(vs[n].time2 - vs[n].time1)+'\n');
- }
- System.out.println("响应时间总和(毫微秒):"+timeSum);
- long time1,time2;
- time1 = System.nanoTime();
- time2 = System.nanoTime();
- // 顺序执行两条语句的时间间隔,供参考
- System.out.println(time2-time1);
- }
-
- /**
- * 假设在抢票场景中,我们一共只有10张火车票,在最后一刻,我们已经卖出了9张火车票,仅剩最后一张。
- * 这个时候,系统发来多个并发请求,这些并发请求都同时读取到火车票剩余数量为1,然后都通过了这一个
- * 余量判断,最终导致超发。也就是说,本来我们只卖10张火车票,最多只生成10个订单,但因为线程不安全,
- * 用户的并发请求,导致抢票成功的用户订单超过了10个,这就是线程不安全。
- */
- static class TicketRunnable implements Runnable {
- // 剩余的票数
- static volatile int count = 10;
- // 抢到第几张票
- static int num = 0;
- // 是否售完票
- boolean flag = false;
- @Override
- public void run() {
- // TODO Auto-generated method stub
- // 票没有售完的情况下,继续抢票
- while (!flag) {
- sale();
- }
- }
- /** 售票 */
- private synchronized void sale() {
- if(count <= 0){
- flag = true;
- return;
- }
- // 剩余的票数 减1
- count--;
- // 抢到第几张票 加1
- num++;
- System.out.println(Thread.currentThread().getName()+"抢到第"+num+"张票,剩余"+count+"张票。");
- }
- }
-
- public void test() throws InterruptedException{
- Thread t2 = new Thread(){
- @Override
- public void run(){
- while(boolValue)
- ;
- time2 = System.nanoTime();
- }
- };
- Thread t1 = new Thread(){
- @Override
- public void run(){
- time1 = System.nanoTime();
- boolValue=false;
- }
- };
- t2.start();
- Thread.yield();
- t1.start();
- /**
- * 保证一次只运行一个测试,以此减少其它线程的调度对 t2对boolValue的响应时间 的影响
- */
- t1.join();
- t2.join();
- }
-
- }
以上
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。