一.内存模型的相关概念

  由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存即寄存器(工作内存)。

  这样在程序运行中先将运算的数据从主存中复制一份到高速缓存中,从而对高速缓存的数据进行读取、写入避免对内存的操作,完成后在将完成的数据更新到存中。

  概念:

    1.共享变量:多个线程会操作的变量

    2.缓存不一致:一个变量在多个CPU中缓存时,当各CPU从高速缓存中更新数据到主存时。

    缓存不一致的解决方案:    

    1)通过在总线加LOCK#锁的方式(一个线程访问时上锁,其他线程访问不了,效率低)。

    2)通过缓存一致性协议(最有代表性的是Inter的MESI协议,多线程访问时,当前线程操作共享变量时会发信号通知其他线程将该变量置为无效状态,当前线程将数据更新到主存后,其他线程再从主存更新,从而达到一致)。

    这2种方式都是硬件层面上提供的方式。

二.并发编程中的三个概念

  1.原子性

  原子性:执行一个或多个操作,要么都成功,要么都失败。JVM中具有原子性的数据类型有8种基本类型中除了long和double以外的6种类型,加上64位操作系统和64位JVM中long和double是原子的,32位JVM中的long和double是非原子的。具体可参考:http://www.cnblogs.com/louiswong/p/5951895.html

  2.可见性

  可见性:多个线程操作一个变量时,其他线程能够立刻看到修改后的值。

  3.有序性

  有序性:程序执行的顺序由代码的先后顺序保证。

  指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。多线程访问时会出现重排序,单线程不会。多线程访问时如果代码前后有依赖时不会发生重排序,如:

int a = 10;    //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4

  上述例子中的语句4不会发生在语句3的前面。

  synchronized和lock能保证原子性、可见性和有序性,volatile只能保证可见性和有序性。此外除了这3种方式保证有序性外还有一种java先天有序性即先行发生原则,下面就来具体介绍下happens-before原则(先行发生原则):

    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
    • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

  这8条原则摘自《深入理解Java虚拟机》。

  这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

  要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

三.深入剖析volatile关键字

  1.volatile自身特性:

    • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

    为什么说volatile++这种复合操作不具有原子性?请看下面代码

public class Test {
public volatile int inc = 0; public void increase() {
inc++;
} public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
} while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}

  由于inc++自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入主存。那么就是说自增操作的三个子操作可能会分割开执行,假设这里有个线程1先读取了inc的值并且进行了加1操作,但是写入主存时阻塞了没有将其他线程的工作内存中的值置为无效,所以就会导致其他线程读到的还是未修改的值,最终结果就不一定是10000。

2.volatile能保证有序性?

  在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5

  volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的,但是不能保证1和2、4和5的顺序。

3.volatile的原理和实现机制

  前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

  下面通过具体事例说明:

class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2; void readAndWrite() {
int i = v1; //第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; //普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; //第二个 volatile写
} }

  针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

    

  上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

  4.volatile的使用场景

a.作为状态标志

  比如用于判断满足某个条件时执行某个事件,例如完成初始化或请求停机

volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}

b.一次性安全发布(double-check)

  在使用之前将检查这些数据是否曾经发布过

class Singleton{
private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}

c.独立观察

  安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用,该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布期间不会更改,但是会需要多次发布。

public class UserManager {
public volatile String lastUser; public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}

d.volatile bean模式

  在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。

@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age; public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; } public void setFirstName(String firstName) {
this.firstName = firstName;
} public void setLastName(String lastName) {
this.lastName = lastName;
} public void setAge(int age) {
this.age = age;
}
}

e.开销较低的读写锁策略

  当读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

参考自:http://www.cnblogs.com/longshiyVip/p/5173877.html

    http://www.cnblogs.com/jiangds/p/6251245.html

    http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

一.内存模型的相关概念

  由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存即寄存器(工作内存)。

  这样在程序运行中先将运算的数据从主存中复制一份到高速缓存中,从而对高速缓存的数据进行读取、写入避免对内存的操作,完成后在将完成的数据更新到存中。

  概念:

    1.共享变量:多个线程会操作的变量

    2.缓存不一致:一个变量在多个CPU中缓存时,当各CPU从高速缓存中更新数据到主存时。

    缓存不一致的解决方案:    

    1)通过在总线加LOCK#锁的方式(一个线程访问时上锁,其他线程访问不了,效率低)。

    2)通过缓存一致性协议(最有代表性的是Inter的MESI协议,多线程访问时,当前线程操作共享变量时会发信号通知其他线程将该变量置为无效状态,当前线程将数据更新到主存后,其他线程再从主存更新,从而达到一致)。

    这2种方式都是硬件层面上提供的方式。

二.并发编程中的三个概念

  1.原子性

  原子性:执行一个或多个操作,要么都成功,要么都失败。JVM中具有原子性的数据类型有8种基本类型中除了long和double以外的6种类型,加上64位操作系统和64位JVM中long和double是原子的,32位JVM中的long和double是非原子的。具体可参考:http://www.cnblogs.com/louiswong/p/5951895.html

  2.可见性

  可见性:多个线程操作一个变量时,其他线程能够立刻看到修改后的值。

  3.有序性

  有序性:程序执行的顺序由代码的先后顺序保证。

  指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。多线程访问时会出现重排序,单线程不会。多线程访问时如果代码前后有依赖时不会发生重排序,如:

int a = 10;    //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4

  上述例子中的语句4不会发生在语句3的前面。

  synchronized和lock能保证原子性、可见性和有序性,volatile只能保证可见性和有序性。此外除了这3种方式保证有序性外还有一种java先天有序性即先行发生原则,下面就来具体介绍下happens-before原则(先行发生原则):

    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
    • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

  这8条原则摘自《深入理解Java虚拟机》。

  这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

  要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

三.深入剖析volatile关键字

  1.volatile自身特性:

    • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

    为什么说volatile++这种复合操作不具有原子性?请看下面代码

public class Test {
public volatile int inc = 0; public void increase() {
inc++;
} public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
} while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}

  由于inc++自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入主存。那么就是说自增操作的三个子操作可能会分割开执行,假设这里有个线程1先读取了inc的值并且进行了加1操作,但是写入主存时阻塞了没有将其他线程的工作内存中的值置为无效,所以就会导致其他线程读到的还是未修改的值,最终结果就不一定是10000。

2.volatile能保证有序性?

  在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5

  volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的,但是不能保证1和2、4和5的顺序。

3.volatile的原理和实现机制

  前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

  下面通过具体事例说明:

class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2; void readAndWrite() {
int i = v1; //第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; //普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; //第二个 volatile写
} }

  针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

    

  上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

  4.volatile的使用场景

a.作为状态标志

  比如用于判断满足某个条件时执行某个事件,例如完成初始化或请求停机

volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}

b.一次性安全发布(double-check)

  在使用之前将检查这些数据是否曾经发布过

class Singleton{
private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}

c.独立观察

  安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用,该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布期间不会更改,但是会需要多次发布。

public class UserManager {
public volatile String lastUser; public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}

d.volatile bean模式

  在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。

@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age; public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; } public void setFirstName(String firstName) {
this.firstName = firstName;
} public void setLastName(String lastName) {
this.lastName = lastName;
} public void setAge(int age) {
this.age = age;
}
}

e.开销较低的读写锁策略

  当读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

JAVA并发编程:相关概念及VOLATILE关键字解析的更多相关文章

  1. Java并发编程(六)volatile关键字解析

    由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...

  2. 【Java并发编程】6、volatile关键字解析&内存模型&并发编程中三概念

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...

  3. Java并发编程——为什么要用volatile关键字

    首发地址 https://blog.leapmie.com/archives/66ba646f/ 日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再 ...

  4. Java并发编程:JMM和volatile关键字

    转载请标明出处: http://blog.csdn.net/forezp/article/details/77580491 本文出自方志朋的博客 Java内存模型 随着计算机的CPU的飞速发展,CPU ...

  5. Java并发编程(三)volatile域

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Android多线程(一)线程池 Android多线程(二)AsyncTask源代码分析 前言 有时仅仅为了读写一个或 ...

  6. java并发编程(2) --Synchronized与Volatile区别

    Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同 ...

  7. 【Java并发编程】11、volatile的使用及其原理

    一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果 ...

  8. Java并发编程的艺术(三)——volatile

    1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行:但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 ...

  9. Java并发机制(3)--volatile关键字与内存模型

    Java并发编程:volatile关键字解析及内存模型 个人整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920373.html 1.线程内存模型: ...

随机推荐

  1. 转换为标准IPv4格式

    Insus.NET刚写了一个函数,把一个IP地址转换为标准格式,即每段位均是由3个数字组成. SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- = ...

  2. 一文带你认识Java8中接口的默认方法

    Java8是Oracle于2014年3月发布的一个重要版本,其API在现存的接口上引入了非常多的新方法. 例如,Java8的List接口新增了sort方法.在Java8之前,则每个实现了List接口的 ...

  3. STL——queue

    1.初始化 需要头文件<queue> queue<int>que; 2.成员函数 C++队列Queue类成员函数如下: back()返回最后一个元素 empty()如果队列空则 ...

  4. js 常用排序

    1. 冒泡排序 原理:从第一个元素开始,把当前元素和下一个索引元素进行比较.如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素 function bubbleSort(arr) { if ...

  5. Kbengine

    Kbengine 编辑 KBEngine是一款开源的游戏服务端引擎,使用简单的约定协议就能够使客户端与服务端进行交互, 使用KBEngine插件能够快速与(Unity3D, OGRE, Cocos2d ...

  6. 51nod1065(set.upper_bound()/sort)

    题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1065 题意:中文题诶- 思路: 解法1:set容器,将所有前 ...

  7. solidity 学习笔记(3) 函数修饰符/继承

    修饰符: 函数修饰符有 constant  view pure 其中 constant和view的功能是一样的  不会消耗gas 也就是说不会做任何存储   constant在5.0以后的版本中被废弃 ...

  8. codevs1229 数字游戏

    1229 数字游戏  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 白银 Silver 题解    

  9. 洛谷P4052 [JSOI2007]文本生成器(AC自动机)

    传送门 好像这题的确只能用AC自动机做了……Aufun大佬太强啦 正着难我们反着做,用总共单词个数减去没有一个单词都不包含的 然后考虑怎么处理一个单词都不包含的,就是跑不到单词的结尾节点 定义$f[i ...

  10. 关于HTML5用SVG画图

    SVG在HTML5中的应用 SVG(Scalable Vector Graphics)是用来绘制矢量图的HTML5标签.只要定义好XML属性就能够获得与其一致的图像元素. 使用SVG之前先将标签加入到 ...