一.内存模型的相关概念

  由于计算机在执行程序时都是在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. day03-CSS(1)

    一 .Css概念 CSS 指层叠样式表 (Cascading Style Sheets)(级联样式表) Css是用来美化html标签的,相当于页面化妆. ◆样式表书写位置 二. 选择器 1. 写法 选 ...

  2. 使用HTML辅助方法载入分部视图

    在webform中我们用过user control可以减少重复代码也利于将页面模组化, 在mvc中 叫分部视图 Partial View.   也就是一个片段的view.可以利用Partial vie ...

  3. centos7及以上安装git服务

    检查git是否安装或者是版本 whereis git等命令来检查是否已经安装了git版本的 git --version检测到我的环境自带的git版本 已安装但不是想要的版本需要卸载 yum remov ...

  4. 基于ZFAKA二次开发,添加PayJS支付渠道

    项目地址:https://github.com/hiyouli/payjs-for-zfaka 关于ZFAKA,请移步:ZFAKA 免费.安全.稳定.高效的发卡系统,值得拥有! 演示地址:http:/ ...

  5. 数学补天 By cellur925

    质数 bool prime(int q) { ||q==) ; ) ; !=||q%!=) ; int cnt=sqrt(q); ;i<=cnt;i+=) !=||q%(i+)!=) ; ; } ...

  6. Django + Vue cli 3.0 访问静态资源问题

    [问题背景] 用Vue clie 3.0的搭建得框架把我坑死了,在打包后,调用不到静态资源js,css,mp3等 [问题原因] vue cli 3.0打包后,dist目录下没有static目录,而Dj ...

  7. 01.html5+phonegap跨平台移动应用开发

    一.html5+PhoneGap基础知识 (1)html5介绍 HTML5是用于取代1999年所制定的 HTML 4.01 和 XHTML 1.0 标准的 HTML 标准版本,现在仍处于发展阶段,但大 ...

  8. Linux下FTP用户的使用配置

    1.创建本地账户,用户名为自己的姓名拼音,使用本地用户测试,从以下几个方面来观察,账户 .密码.访问目录(可否查看上级目录) .上传下载的权限.

  9. Codeforces Round #562 (Div. 2) B. Pairs

    链接:https://codeforces.com/contest/1169/problem/B 题意: Toad Ivan has mm pairs of integers, each intege ...

  10. NET Core 2.0使用Cookie认证实现SSO单点登录

    NET Core 2.0使用Cookie认证实现SSO单点登录 之前写了一个使用ASP.NET MVC实现SSO登录的Demo,https://github.com/bidianqing/SSO.Sa ...