深入浅出Java并发包—指令重排序
前面大致提到了JDK中的一些个原子类,也提到原子类是并发的基础,更提到所谓的线程安全,其实这些类或者并发包中的这么一些类,都是为了保证系统在运行时是线程安全的,那到底怎么样才算是线程安全呢?
Java并发与实践一书中提出,当多个线程同时访问一个类的时候,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要做额外的同步以及在调用代码时不需要做其他的协调,这个类的运行仍然是正确的,那么这个类是线程安全的。
很显然只有资源竞争时才会出现线程不安全,而无状态的类将永远是线程安全的。因此我们再做分层结果的时候,Service层可以轻松的使用单例去显示,而展示层和数据层却需要每个单独的线程单独一个对象去处理。
之前说了这么一些原子类,他们都是线程安全的类,原子操作的描述是多个线程执行同一个操作时,其中一个线程要么完全执行完成这个操作,要么根本没有执行任何步骤。
在JDK中,JAVA语言为了维持顺序内部的顺序化语义,也就是为了保证程序的最终运行结果需要和在单线程严格意义的顺序化环境下执行的结果一致,程序指令的执行顺序有可能和代码的顺序不一致,这个过程就称之为指令的重排序。指令重排序的意义在于:JVM能根据处理器的特性,充分利用多级缓存,多核等进行适当的指令重排序,使程序在保证业务运行的同时,充分利用CPU的执行特点,最大的发挥机器的性能!
我们来看一组代码示例:
package com.yhj.concurrent;
/**
* @Described:Happen-Before测试
* @author YHJ create at 2013-4-13 下午05:12:36
* @ClassNmae com.yhj.concurrent.HapenBefore
*/
public class HappenBefore { static int x,y,m,n;//测试用的信号变量 public static void main(String[] args) throws InterruptedException {
int count = 10000;
for(int i=0;i<count;++i){
x=y=m=n=0;
//线程一
Thread one = new Thread(){
public void run() {
m=1;
x=n;
};
};
//线程二
Thread two = new Thread(){
public void run() {
n=1;
y=m;
};
};
//启动并等待线程执行结束
one.start();
two.start();
one.join();
two.join();
//输出结果
System.out.println("index:"+i+" {x:"+x+",y:"+y+"}");
}
}
}
这段代码循环1w次, 每次启动两个线程去修改x、y、m、n四个变量,能得到什么结果的呢?运行一下,很容易得到x=1,y=0;x=0,y=1两种结果,事实上根据JVM规范以及CPU的特性,我们很可能还能得到x=0,y=0或者x=1,y=1的情况。当然上端代码大家不一定能得到x=0,y=0或者x=1,y=1的结果,这是因为这段代码太简单了,以现在CPU 的运算速度,根本无需做线程切换就能将这些很快的执行完毕。x=1,y=1这种情况大家也许还能理解,当发生线程切换时,第一个线程第一行代码执行完毕,再次执行第二线程的第一行代码,就会发生x=1,y=1的结果。但x=0,y=0是否可能发生?按照现在的JVM和CPU特性,这种情况的确是存在的。由于线程的run方法里面的动作对结果是无关的,因此里面的指令可能发生指令重排序,即使是按照程序的顺序方法执行,数据从线程缓冲区刷新到主存也是需要时间的(之前有提到,原理可参考http://yhjhappy234.blog.163.com/blog/static/316328322011101723933875/,实践可通过下面方框中的测试代码验证)。假定是按照m=1,x=n,n=1,y=m执行的,显然x=0是很正常的,m=1虽然在y=m之前执行,但是线程one有可能还没来得及将m=1的数据从高速缓存(work memory)写入主存,线程two就从主存中取m的数据,所以还有可能是0,这样就发生了数据错误!尤其是在大并发和多核CPU的执行下,数据的结果就更无法确定了!
package com.yhj.jvm.memory.concurrent; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* @Described:并发常量测试
* @author YHJ create at 2013-04-17 下午08:54:24
* @FileNmae com.yhj.jvm.memory.concurrent.ConcurrentStaticTest.java
*/
public class ConcurrentStaticTest { public static int counter = 0;//volatile public final static int THRED_COUNT = 20; public static void plus() {
counter++;
} /**
* @param args
* @Author YHJ create at 2011-11-17 下午08:54:19
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<THRED_COUNT;++i){
executorService.execute(new Runnable() { @Override
public void run() {
for(int j = 0;j<10000;++j){
plus();
}
} });
}
//等待所有进程结束
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(counter); }
}
为了解决此类额外难题,Java存储模型引入了happens-Before发则,确保并发情况下的数据正确性!通俗的说就是如果动作B要看到动作A的执行结果(无论A/B是否在同一个线程中),那么A/B必须满足happens-before发则!
在说happens-before发则之前我们还得先看另外一个概念:在Java中还有一个概念叫JMMA(Java Memory Medel Action):Java模型动作。一个Action包含:编写读、变量写、监视器加锁、释放锁、线程启动(start)、线程等待(join)。关于锁我们后续会详细介绍。
说了这么多,那究竟什么是happens-before发则呢?完整的发则如下
(1)同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。
(2)对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。
(3)对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。
(4)Thread.start()的调用会happens-before于启动线程里面的动作。
(5)Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。
(6)一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。
(7)一个对象构造函数的结束happens-before与该对象的finalizer的开始
(8)如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。
法则中提到了一个关键字volatile,其实我们前面讲JVM的时候也多次提到这个关键字,今天我们略微扩展一点,因为他对我们后续的CAS理解有很大帮助。
Volatile相当于synchronized的一个弱实现,他实现了synchronized的语义却没有锁机制,它确保对volatile字段的更新以可预见的形式告知其他线程。
Java存储模型不对对olatile指令的操作做重排序,保证volatile的变量都能按照指令的顺序执行。
Volatile类型的变量不会被缓存在寄存器中(寄存器中的数据只有当前线程可以访问),或者其他对CPU不可见的地方,每次都需要充主存中读取对应的数据,这保证每次对变量的修改,其他线程也是可见的,而不是仅仅修改自己线程的局部变量,在happens-before发则中,对一个volatile变量进行写操作后了,此后的任何读操作都可见该次写操作的结果。
Volatile关键字主要用于以下场景
volatile boolean condition = false; public void method() {
while(!condition){
doSth();
}
}
应用volatile关键字的三个发则
(1)写入变量不依赖此变量的值,或者只有一个线程修改此变量
(2)变量的状态不需要与其它变量共同参与不变约束
(3)访问变量不需要加锁
Happens-before和volatile是后面锁和原子操作的基础,那锁操作和原子操作是怎么实现的呢?请参考后续连载的章节!
深入浅出Java并发包—指令重排序的更多相关文章
- 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则
转: http://www.blogjava.net/xylz/archive/2010/07/03/325168.html 在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到 ...
- 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则[转]
在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到锁机制,因此此小节中会适当引入锁的概念. 在Java Concurrency in Practice中是这样定义线程安全的: ...
- Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- java指令重排序的问题
转载自于:http://my.oschina.net/004/blog/222069?fromerr=ER2mp62C 指令重排序是个比较复杂.觉得有些不可思议的问题,同样是先以例子开头(建议大家跑下 ...
- Java并发编程-线程可见性&线程封闭&指令重排序
一.指令重排序 例子如下: public class Visibility1 { public static boolean ready; public static int number; } pu ...
- 【java多线程系列】java内存模型与指令重排序
在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序.很多读者可能会说这还不简 ...
- java高并发核心要点|系列4|CPU内存指令重排序(Memory Reordering)
今天,我们来学习另一个重要的概念. CPU内存指令重排序(Memory Reordering) 什么叫重排序? 重排序的背景 我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多.当CP ...
- java并发学习--第九章 指令重排序
一.happns-before happns-before是学习指令重排序前的一个必须了解的知识点,他的作用主要是就是用来判断代码的执行顺序. 1.定义 happens-before是用来指定两个操作 ...
随机推荐
- CentOS 5.8 升级php版本
一:我们都知道系统的yum源安装出来的php版本不是5.1的就是5.3 那就是说 有些程序不支持那么低的版本的呢 那我们该怎么办呢 接下来 简单的说下php的版本升级 编译升级太慢了 这里我们选择 ...
- Spring-Mybatis 异常记录(1)
Spring applicationconfig.xml如下 <?xml version="1.0" encoding="UTF-8"?> < ...
- SQL允许远程访问
1.打开sqlserver对象资源管理器 右键 方面 常规 服务器配置 RemoteAccessEnabled true RemoteDacEnabled true 2.打开SQL SERVER管理 ...
- Android 中 shape 图形的使用
转载于:http://kofi1122.blog.51cto.com/2815761/521605 Android中常常使用shape来定义控件的一些显示属性,今天看了一些shape的使用,对shap ...
- 使用PowerShell 连接Azure
除了使用门户登入外,还可以使用PowerShell的方式来连接Azure.首先要去下载组件 http://azure.microsoft.com/en-us/downloads/?rnd=1 http ...
- spring多数据源配置
项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此.多数据源让人最头痛的,不是配置多个数据源,而是如何能灵活动态的切换数据源.例如在一个spring和hibernate的框架的 ...
- Msys+Mingw在手 妙用在心!
1 缘起 平时在一些c++群面,看见很多大学十分努力的学习c++/MFC ,看见在编程语言百花争芳的时候,C/C++还是很有很有魅力.估计很多初学者使用都是window下的visual stdio 开 ...
- [WinForm]- 设置DataGridView单元格内根据不同值显示图片
首先设置要显示图片的列 DataGridViewImageColumn status = new DataGridViewImageColumn(); status.DisplayIndex = ; ...
- linux 删除某种规则命名的文件
由于android开发需要删除以IMG_开头命名的图片文件,因此用到此命令 命令格式: rm IMG_*
- lamada 表达式之神奇的groupby
少说话多干活 先定义一个测试用的实体,接下来会用字段Name进行分组的 public class TestToRun { public string Name { get; set; }//名称 pu ...