Java的同步原语——volatile,synchronized和final

前言

上一篇博客写了java内存模型的基础,也就是未同步的情况,未同步的线程运行顺序不得而知,运行效果也不一定,安全性是比较低的。本篇博客将谈谈java的三个同步原语。

volatile

volatile变量自身具有下列特性
可见性:对一个volatile变量的读,总是能看到任意线程对这个变量最后的写入。
原子性:对任意单个volatile变量的读、写具有原子性,但是对于volatile++这种复合操作不具有原子性。
下面看一段程序

class VolatileExample{
int a=0;
volatile boolean flag=false;

public void writer(){
a=1;//1
flag=true;//2
}
public void reader(){
if(flag){//3
int i=a*a;//4
}
}
}

假设线程A执行writer方法之后,线程B执行reader方法,根据happens-before规则,这个过程建立的happens-before关系为:
(1)1 happens-before 2,3happens-before4
(2) 根据volatile规则:2happens-before 3
(3)根据happens-before的传递性规则:1happens-before4
所以执行顺序为可预测的,为:1-2-3-4.
分析原因:这是因为,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值刷新到主内存中,线程B读同一个volatile变量的时候,本地的变量变为无效,读取的是主内存的volatile变量。
volatile的内存语义
1.线程A写一个volatile变量,实质上是县城A向接下类将要读这个变量的某个县城发出了(其对共享变量所做修改)信息
2.线程B读一个volatile变量,实质上是县城B接受了之前某个线程发出的(在写这个变量之前低共享变量所做修改)信息
3.线程A写一个volatile变量,随后线程B读这个变量,这个过程实质上是线程A在向线程B发送消息。
总结
上面的代码中,若在旧的内存模型中,1和2之间没哟数据依赖性,所以1和2是可以被重排序的,其结果就是,读线程B执行4时,不一定能看到线程A在执行1时对共享变量的修改。增加了volatile之后,只要volatile变量于普通变量之间的重排序可能破坏volatile的内存语义,这种重排序就会被编译器和处理器禁止。

锁的内存语义

锁的内存语义于volatile的内存语义相同。但是,锁比volatile更强大,因为volatile仅仅保证对单个volatile变量的读、写具有原子性,而锁的互斥执行的特性可以确保读整个临街区域代码的执行具有原子性,所以锁的功能更加强大。但是在可伸缩性和执行性能上,volatile更加具有优势。
看下面的代码来理解锁

class MonitorExample{
int a=0;
public synchronized void writer(){//1
a++;//2
}//3
public synchronized void reader(){//4
int i=1;//5
}//6
}

同样,假设线程A执行writer方法,随后线程B执行reader方法,根据happens-before关系有:
(1)根据程序次序规则:1 happens-before 2,2happens-before3, 4happens-before5,5 happens-before6
(2)根据监视器锁规则:3happens-before4
(3)根据happens-before的传递性,2happens-before5
在这个过程中,A线程释放了锁之后,线程B获取了同一个锁。线程A在释放锁之前所有 可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。

final

对于final,编译器和处理器遵守两个重排序原则:
1.在构造函数内对一个final域的写入,于随后把这个构造对象的音乐复制给一个引用变量,这两个操作之间不能重排序。
2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
下面肯一段代码来帮助理解

public class FinalExample{
int i;
final int j;
static FinalExample obj;
public FinalExample(){
i=1;
j=2;
}
public static void writer(){
obj=new FinalExample();
}
public static void reader(){
FinalExample object=obj;
int a=object.i;
int b=object.j;
{
}

假设线程A执行writer方法,而线程B执行reader方法,假设线程B读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要有这个假设)写final域的重排序原则可以保证,在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通的变量则不具有这宗特性。也就是,对于final变量j的赋值语句j=2,不会被重排序到构造方法之外,而普通变量i的赋值语句则有可能被重排序大盘构造方法之外,这就有可能导致线程B在读取变量i的值的时候,没能正确读到1的值。
前面说到,假设线程B读对象引用与读对象的成员变量之间没有重排序,现在就说明原因。这是因为,final的另外一个重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。也就是说,线程B中,读取final变量j前看到会先读取对象object,而i的读取就不一定了,这是错误的读取方式。

总结

之前一篇博客中也有写到,不同线程之间的运行是随机的,而这三个同步原语,是为了让运行顺序可控,让线程的运行更加透明化。

你可能感兴趣的:(Java的同步原语——volatile,synchronized和final)