Java并发--volatile

volatile特性

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  2. 禁止进行指令重排序。(实现有序性)
  3. volatile 只能保证对单次读/写的原子性(有限的原子性)。i++ 这种操作不能保证原子性。同步64位int/long的写操作也不具有原子性。

可见性实现

volatile变量在编译后,会插入一条lock指令,其作用是将当前处理器缓存行的数据写回到系统内存。
同时写回内存的操作会使在其他 CPU 里缓存了该内存地址的额数据无效
。这样当其他处理器在读取该变量时就会从系统内存中直接读取,得到最新值。
早期处理器是通过对总线进行锁定,这样其他CPU对内存的都会被阻塞,直到锁释放。

后来通过高速缓存锁代替总线锁来处理,缓存一致性(MESI)有多种,常用的就是“嗅探(snooping)” 协议。就是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。

有序性实现

happens-before

happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

内存屏障

  1. 为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序
  2. Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
是否能重排序 第二个操作
第一个操作 - 普通读写 volatile读 volatile写
普通读写 NO
volatile读 NO NO NO
volatile写 NO NO
  1. 其中JMM针对volatile采取的策略是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。
  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止上面的普通写和下面的 volatile 写重排序)。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(防止上面的 volatile 写与下面可能有的 volatile 读/写重排序)。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止下面所有的普通读操作和上面的 volatile 读重排序)。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止下面所有的普通写操作和上面的 volatile 读重排序)。

其中StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。另外volatile写后面有StoreLoad屏障,此屏障的作用是避免volatile写与后面可能有的读或写操作进行重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)为了保证能正确实现volatile的内存语义,JMM采取了保守策略:在每个volatile写的后面插入一个StoreLoad屏障。
volatile写
下边是volatile读的顺序
volatile写

原子性

如果对volatile变量的操作只有只有简单的读和写,那么可确保原子性。比如volatile变量i进行这些操作i++、 i=b;。这是由于i++本质是i = i+ 1;其操作过程需要读取i,然后加1,然后写给i,包含多个操作步骤,同样对于i=b,先要读取b的值,然后写给i。还有一点就是64位的long/int的写操作同样不是原子性,他是分为两次32位的操作完成,也不具备原子性。还有if(i == true),也是两步操作,先是读取i,然后再做比较

使用

根据以上特性,由于是原子性的描述,正确使用volatile时,需要确保以下两点(其实就针对原子性的操作):

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

标态标志

1
2
3
4
5
6
7
8
9
10
11
volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

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

一次性安全发布(one-time safe publication)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;

public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}

public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}

这种模式有些疑问,先看下关于单例中双重检查的分析。

单例

User user=new User(“男”,26);
该语句做了几件事:

  1. 因为new用到了User.class,所以找到User.class文件并加载到内存中
  2. 执行该类的static代码块,如果有的话,给User.class类进行初始化
  3. 在堆内存中开辟空间,分配内存地址。
  4. 在堆内存中建立对象的特有属性,并进行默认初始化。
  5. 对属性进行显式初始化
  6. 对对象进行构造代码块初始化
  7. 对对象进行对应的构造函数初始化
  8. 将内存地址赋给栈内存中的p变量
    特别注意,步骤7和步骤8,先后发生顺序是随机的,或者说不确定性。

饿汉式单例

1
2
3
4
5
6
7
8
9
10
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton(){

}
public static Singleton getInstance(){
return instance;
}
}

在类加载时就创建好实例了,但是不一定使用,所以可能造成资源浪费。如果创建这个类需要很多的系统资源,则浪费更严重。

懒汉式单例

1
2
3
4
5
6
7
8
9
10
11
public class Singleton{
private static Singleton instance = null;
private Singleton(){

}
public static Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
}

这种方式存在线程安全问题,

  1. A进入if判断,此时instance为null,因此进入if内
  2. B进入if判断,此时A还没有创建instance,因此instance也为null,因此B也进入if内
  3. A创建了一个instance并返回
  4. B也创建了一个instance并返回
    如果在方法上加上synchronized,则每次获取对象时都有锁同步,影响效率
    1
    2
    3
    4
    5
    public static synchronized Singleton getInstance(){
    if (instance == null)
    instance = new Singleton();
    return instance;
    }

于是有人提出了双重检查的方式,这种方式如果在new对象时的执行顺序7和8发生变量,则会得到一个不完事的对象,所以也存在问题,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static Singleton getInstance(){
if (instance == null)
synchronized(instance){
if(instance == null)
instance = new Singleton();
}
return instance;
}

//使用内部类方式
public class Singleton{
private Singleton(){

}
private static class SingletonContainer{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonContainer.instance;
}
}

Doug Lea发表意见

Doug Lea 在他的文章中写道:“根据最新的 JSR133 的 Java 内存模型,如果将引用类型声明为 volatile,双重检查模式就可以工作了”

1
2
// 在引用类型声明加上volatile关键字
private volatile static Singleton instance = null;

所以文章中关于一次性安全发布(one-time safe publication)的示例是没问题的,虚惊一场。

独立观察(independent observation)

这种模式主要是利用了可见性,关键点在于last这个字眼,目标是获取最后的状态,只要修改了volatile变量,那他就是最新的,也就是最last的,就达到目的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

“volatile bean” 模式

JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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;
}
}

关于volatile修饰对象,String,数组,一直没找到靠的资料,有些网友写道volatile传递性,待验证,看了好几一直得不出什么结论,再研究研究

volatile 和 synchronized 实现 “开销较低的读-写锁”

如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

1
2
3
4
5
6
7
8
9
10
11
12
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;

public int getValue() { return value; }

public synchronized int increment() {
return value++;
}
}