[Java 并发编程实战] 对 volatile 变量进行实例验证(内含源码)

「 天行健,君子以自强不息。地势坤,君子以厚德载物。」———《易经》

volatile 变量,在上一篇文章中已经有简单提及相关概念和用法,这一篇主要对 Volatile 变量的特性进行源码验证。验证它的涉及到的三个特性:

  • 可见性
  • 指令重排序
  • 非原子性

volatile 之可见性验证

上一篇文章中,讲到 volatile 变量通常被当做状态标记使用。其中典型的应用是,检查标记状态,以确定是否退出循环。下面我们直接举个反例,源码如下:

 1    public class Volatile { 2 3        boolean ready=true;  //volatile 状态标志变量 4 5        private final static int SIZE = 10; //创建10个对象,可改变 6 7        public static void main(String[] args) throws InterruptedException{ 8 9            Volatile vs[]=new Volatile[SIZE];1011            for(int n=0;n<SIZE;n++)12                (vs[n]=new Volatile()).test(); 1314            System.out.println("mainThread end");//调用结束打印,死循环时不打印15        }1617        public void test() throws InterruptedException{18            Thread t2=new Thread(){19                public void run(){20                    while(ready);//变量为true时,让其死循环21                }22            };23            Thread t1=new Thread(){24                public void run(){25                    ready=false;26                }27            };28            t2.start();29            Thread.yield();30            t1.start();31            t1.join();//保证一次只运行一个测试,以此减少其它线程的调度对 t2对boolValue的响应时间 的影响32            t2.join();33        }34    }
1

其中,ready 变量是我们要验证的 volatile 变量。一开始 ready 初始化为 true,其次启动 t2 线程让其进入死循环;接着,t1 线程启动,并且让 t1 线程先执行,将 ready 改为 false。理论上来讲,此时 t2 线程应该跳出死循环,但是实际上并没有。此时 t2 线程读到的 ready 的值仍然为 true。所以这段程序一直没有打印出结果。这便是多线程间的不可见性问题,官方话术为: 线程 t1 修改后的值对线程 t2 来说并不可见。下图可以看到程序一直处于运行状态:

这里写图片描述

解决办法是:对变量 ready 声明为 volatile,再次执行者段程序,能够顺利打印出 “mainTread end”。volatile 保证了变量 ready 的可见性。

这里写图片描述

另外补充说明我这个例子用的 Java 版本:

这里写图片描述

volatile 之重排序问题说明

有序性:表示程序的执行顺序按照代码的先后顺序执行。通过下面代码,我们将更加直观的理解有序性。

1int a = 1;2int b = 2;3a = 3;     //语句A4b = 4;     //语句B

上面代码,语句 A 一定在语句 B 之前执行吗? 答案是否定的。因为这里可能发生指令重排序。语句 B 可能先于语句 A 先自行。

什么是指令重排序?处理器为了提高运行效率,可能对输入代码进行优化,他不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是他会保证程序最终执行的结果和代码顺序执行的结果是一致的。

但是下面这种情况,语句 B 一定在 语句 A 之后执行。

1int a = 1;2int b = 2;3a = 3;     //语句A4b = a + 3;     //语句B

原因是,变量 b 依赖 a 的值,重排序时处理器会考虑指令之间的依赖性。

当然,这个 volatile 有什么关系呢?
volatile 变量可以一定程度上保证有序性,volatile 关键字禁止指令重排序。

1//x、y为非volatile变量2//flag为volatile变量34x = 1;        //语句15y = 2;        //语句26flag = true;  //语句378x = 3;         //语句49y = 4;       //语句5

这里要说明的是,flag 为 volatile 变量;能保证

  1. 语句1,语句2 一定是在语句3的前面执行,但不保证语句1,语句2的执行顺序。
  2. 语句4,语句5 一定是在语句3的后面执行,但不保证语句4,语句5的执行顺序。
  3. 语句1,语句2 的执行结果,对语句3,语句4,语句5是可见的。

以上,就是关于 volatile 的禁止重排序的说明。、

volatile 之非原子性问题验证

volatile 关键字并不能保证原子性,如自增操作。下面看一个例子:

 1public class Volatile{ 2 3    private volatile int count = 0; 4 5    public static void main(String[] args) { 6 7        final Volatile v = new Volatile(); 8 9        for(int i = 0; i < 1000; i++) {10            new Thread(new Runnable() {1112                @Override13                public void run() {14                    // TODO Auto-generated method stub15                    v.count++;16                }1718            }).start();19        }2021        while(Thread.activeCount() > 1)22            Thread.yield();2324        System.out.println(v.count);25    }2627}

这个程序执行的结果并没有达到我们的期望值,1000。并且每次的运行结果可能都不一样,如下图,有可能是 997 等。

这里写图片描述

来看下面一副图,分解自增操作的步骤。

  1. read&load 从主内存复制变量到当前工作内存。
  2. use&assign 执行代码,改变共享变量的值。
  3. store&write 用工作内存数据刷新主内存相关内容。

这里写图片描述

但是,这一系列的操作并不是原子的。也就是在 read&load 之后,如果主内存 count 发生变化,线程工作内存中的值由于已经加载,不会产生对应的变化。所以计算出来的结果和我们预期不一样。

对于 volatile 修饰的变量,jvm 虚拟机只是保证从主内存加载到线程工作内存中的值是最新的。

所以,假如线程 A 和 B 在read&load 过程中,发现主内存中的值都是5,那么都会加载这个最新的值 5。线程 A 修改后写到主内存,更新主内存的值为6。线程 B 由于已经 read & load,注意到此时线程 B 工作内存中的值还是5, 所以修改后也会将6更新到主内存。

那么两个线程分别进行一次自增操作后,count 只增加了1,结果也就错了。

当然,我们可以通过并发安全类AomicInteger, 内置锁 sychronized,显示锁 ReentrantLock,来规避这个问题,让程序运行结果达到我们的期望值 1000.

1)采用并发安全类 AomicInteger 的方式:

 1import java.util.concurrent.atomic.AtomicInteger; 2 3public class Volatile{ 4 5    private AtomicInteger  count = new AtomicInteger(0); 6 7    public static void main(String[] args) { 8 9        final Volatile v = new Volatile();1011        for(int i = 0; i < 1000; i++) {12            new Thread(new Runnable() {1314                @Override15                public void run() {16                    // TODO Auto-generated method stub17                    v.count.incrementAndGet();18                }1920            }).start();21        }2223        while(Thread.activeCount() > 1)24            Thread.yield();2526        System.out.println(v.count);27    }2829}

2) 采用内置锁 synchronized 的方式:

 1public class Volatile{ 2 3    private int count = 0; 4 5    public static void main(String[] args) { 6 7        final Volatile v = new Volatile(); 8 9        for(int i = 0; i < 1000; i++) {10            new Thread(new Runnable() {1112                @Override13                public void run() {14                    // TODO Auto-generated method stub15                    synchronized  (this) {16                        v.count++;17                    }18                }1920            }).start();21        }2223        while(Thread.activeCount() > 1)24            Thread.yield();2526        System.out.println(v.count);27    }2829}

3)采用显示锁的方式

 1import java.util.concurrent.locks.Lock; 2import java.util.concurrent.locks.ReentrantLock; 3 4public class Volatile{ 5 6    private int count = 0; 7 8    Lock lock = new ReentrantLock(); 910    public static void main(String[] args) {1112        final Volatile v = new Volatile();1314        for(int i = 0; i < 1000; i++) {15            new Thread(new Runnable() {1617                @Override18                public void run() {19                    // TODO Auto-generated method stub20                        v.lock.lock();21                        try {22                            v.count++;23                        }finally {24                            v.lock.unlock();25                        }26                }2728            }).start();29        }3031        while(Thread.activeCount() > 1)32            Thread.yield();3334        System.out.println(v.count);35    }3637}

参考

http://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.csdn.net/gao_chun/article/details/45095995
https://blog.csdn.net/xilove102/article/details/52437581

原文地址:https://www.cnblogs.com/seaicelin/p/9142419.html

时间: 2024-10-24 10:35:06

[Java 并发编程实战] 对 volatile 变量进行实例验证(内含源码)的相关文章

Java并发编程与技术内幕:ArrayBlockingQueue、LinkedBlockingQueue及SynchronousQueue源码解析

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要:本文主要讲了Java中BlockingQueue的源码 一.BlockingQueue介绍与常用方法 BlockingQueue是一个阻塞队列.在高并发场景是用得非常多的,在线程池中.如果运行线程数目大于核心线程数目时,也会尝试把新加入的线程放到一个BlockingQueue中去.队列的特性就是先进先出很容易理解,在java里头它的实现类主要有下图的几种,其中最常用到的是ArrayBl

《Java并发编程实战》第十五章 原子变量与非阻塞同步机制 读书笔记

一.锁的劣势 锁定后如果未释放,再次请求锁时会造成阻塞,多线程调度通常遇到阻塞会进行上下文切换,造成更多的开销. 在挂起与恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断. 锁可能导致优先级反转,即使较高优先级的线程可以抢先执行,但仍然需要等待锁被释放,从而导致它的优先级会降至低优先级线程的级别. 二.硬件对并发的支持 处理器填写了一些特殊指令,例如:比较并交换.关联加载/条件存储. 1 比较并交换 CAS的含义是:"我认为V的值应该为A,如果是,那么将V的值更新为B,否则不需要修

《Java并发编程实战》第十一章 性能与可伸缩性 读书笔记

造成开销的操作包括: 1. 线程之间的协调(例如:锁.触发信号以及内存同步等) 2. 增加的上下文切换 3. 线程的创建和销毁 4. 线程的调度 一.对性能的思考 1 性能与可伸缩性 运行速度涉及以下两个指标: 某个指定的任务单元需要"多快"才能处理完成.计算资源一定的情况下,能完成"多少"工作. 可伸缩性: 当增加计算资源时(例如:CPU.内存.存储容器或I/O带宽),程序的吞吐量或者处理能力能相应地增加. 2 评估各种性能权衡因素 避免不成熟的优化.首先使程序正

JAVA并发编程实战 读书笔记(二)对象的共享

<java并发编程实战>读书摘要 birdhack 2015年1月2日 对象的共享 JAVA并发编程实战读书笔记 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键之synchronized只能用于实现原子性或者确定临界区.同步还有另一个重要的方面:内存可见性. 1.可见性 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程

《Java并发编程实战》要点笔记及java.util.concurrent 的结构介绍

买了<java并发编程实战>这本书,看了好几遍都不是很懂,这个还是要在实战中找取其中的要点的,后面看到一篇文章笔记做的很不错分享给大家!! 原文地址:http://blog.csdn.net/cdl2008sky/article/details/26377433 Subsections  1.线程安全(Thread safety) 2.锁(lock) 3.共享对象 4.对象组合 5.基础构建模块 6.任务执行 7.取消和关闭 8.线程池的使用 9.性能与可伸缩性 10.并发程序的测试 11.显

《Java并发编程实战》第三章 对象的共享 读书笔记

一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见性的描写叙述即多线程能够实时获取其它线程改动后的状态. *** 待补充   两个工人同一时候记录生产产品总数问题 1. 失效数据 可见性出现故障就是其它线程没有获取到改动后的状态,更直观的描写叙述就是其它线程获取到的数据是失效数据. 2. 非原子64位操作 3. 加锁与可见性 比如在一个变量的读取与

【java并发编程实战】-----线程基本概念

学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习Java并发编程,共同进步,互相指导. 在学习Java并发之前我们需要先理解一些基本的概念:共享.可变.线程安全性.线程同步.原子性.可见性.有序性. 共享和可变 要编写线程安全的代码,其核心在于对共享的和可变的状态进行访问. "共享"就意味着变量可以被多个线程同时访问.我们知道系统中的资

《Java并发编程实战》读书笔记

Subsections 线程安全(Thread safety) 锁(lock) 共享对象 对象组合 基础构建模块 任务执行 取消和关闭 线程池的使用 性能与可伸缩性 并发程序的测试 显示锁 原子变量和非阻塞同步机制 一.线程安全(Thread safety) 无论何时,只要多于一个线程访问给定的状态变量.而且其中某个线程会写入该变量,此时必须使用同步来协助线程对该变量的访问. 线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的. 线程安全的实例: (1).一个无状

《Java并发编程实战》第二章 线程安全性 读书笔记

一.什么是线程安全性 编写线程安全的代码 核心在于要对状态访问操作进行管理. 共享,可变的状态的访问 - 前者表示多个线程访问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与其规范完全一致. 多个线程同时操作共享的变量,造成线程安全性问题. * 编写线程安全性代码的三种方法: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 Java同步机制工具: synchronized volatile类型变量 显示锁(Explicit Lock