广州市越秀区建设局官方网站,最新的网络营销方式,wordpress大数据,陶瓷网站建设中企动力1. 三大性质简介
在并发编程中分析线程安全的问题时往往需要切入点#xff0c;那就是两大核心#xff1a;JMM抽象内存模型以及happens-before规则#xff08;在这篇文章中已经经过了#xff09;#xff0c;三条性质#xff1a;原子性#xff0c;有序性和可见性。关于sy…1. 三大性质简介
在并发编程中分析线程安全的问题时往往需要切入点那就是两大核心JMM抽象内存模型以及happens-before规则在这篇文章中已经经过了三条性质原子性有序性和可见性。关于synchronized和volatile已经讨论过了就想着将并发编程中这两大神器在 原子性有序性和可见性上做一个比较当然这也是面试中的高频考点值得注意。
2. 原子性
原子性是指一个操作是不可中断的要么全部执行成功要么全部执行失败有着“同生共死”的感觉。及时在多个线程一起执行的时候一个操作一旦开始就不会被其他线程所干扰。我们先来看看哪些是原子操作哪些不是原子操作有一个直观的印象
int a 10; //1
a; //2
int ba; //3
a a1; //4
上面这四个语句中只有第1个语句是原子操作将10赋值给线程工作内存的变量a,而语句2a实际上包含了三个操作1. 读取变量a的值2对a进行加一的操作3.将计算后的值再赋值给变量a而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然java内存模型中定义了8中操作都是原子的不可再分的。
lock(锁定)作用于主内存中的变量它把一个变量标识为一个线程独占的状态unlock(解锁):作用于主内存中的变量它把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定read读取作用于主内存的变量它把一个变量的值从主内存传输到线程的工作内存中以便后面的load动作使用load载入作用于工作内存中的变量它把read操作从主内存中得到的变量值放入工作内存中的变量副本use使用作用于工作内存中的变量它把工作内存中一个变量的值传递给执行引擎每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作assign赋值作用于工作内存中的变量它把一个从执行引擎接收到的值赋给工作内存的变量每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作store存储作用于工作内存的变量它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用write操作作用于主内存的变量它把store操作从工作内存中得到的变量的值放入主内存的变量中。
上面的这些指令操作是相当底层的可以作为扩展知识面掌握下。那么如何理解这些指令了?比如把一个变量从主内存中复制到工作内存中就需要执行read,load操作将工作内存同步到主内存中就需要执行store,write操作。注意的是java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的。也就是说read和load之间可以插入其他指令store和writer可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序read a,read b, load b,load a。
由原子性变量操作read,load,use,assign,store,write可以大致认为基本数据类型的访问读写具备原子性例外就是long和double的非原子性协定
synchronized
上面一共有八条原子操作其中六条可以满足基本数据类型的访问读写具备原子性还剩下lock和unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用反应到java代码中就是—synchronized关键字也就是说synchronized满足原子性。
volatile 我们先来看这样一个例子
public class VolatileExample {private static volatile int counter 0;public static void main(String[] args) {for (int i 0; i 10; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {for (int i 0; i 10000; i)counter;}});thread.start();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(counter);}
}开启10个线程每个线程都自加10000次如果不出现线程安全的问题最终的结果应该就是10*10000 100000;可是运行多次都是小于100000的结果问题在于 volatile并不能保证原子性在前面说过counter这并不是一个原子操作包含了三个步骤1.读取变量counter的值2.对counter加一3.将新值赋值给变量counter。如果线程A读取counter到工作内存后其他线程对这个值已经做了自增操作后那么线程A的这个值自然而然就是一个过期的值因此总结果必然会是小于100000的。
如果让volatile保证原子性必须符合以下两条规则
运算结果并不依赖于变量的当前值或者能够确保只有一个线程修改变量的值变量不需要与其他的状态变量共同参与不变约束
3. 有序性
synchronized
synchronized语义表示锁在同一时刻只能由一个线程进行获取当锁被占用后其他线程只能等待。因此synchronized语义就要求线程在访问读写共享变量时只能“串行”执行因此synchronized具有有序性。
volatile
在java内存模型中说过为了性能优化编译器和处理器会进行指令重排序也就是说java程序天然的有序性可以总结为如果在本线程内观察所有的操作都是有序的如果在一个线程观察另一个线程所有的操作都是无序的。在单例模式的实现上有一种双重检验锁定的方式Double-checked Locking。代码如下
public class Singleton {private Singleton() { }private volatile static Singleton instance;public Singleton getInstance(){if(instancenull){synchronized (Singleton.class){if(instancenull){instance new Singleton();}}}return instance;}
}这里为什么要加volatile了我们先来分析一下不加volatile的情况有问题的语句是这条
instance new Singleton();
这条语句实际上包含了三个操作1.分配对象的内存空间2.初始化对象3.设置instance指向刚分配的内存地址。但由于存在重排序的问题可能有以下的执行顺序 如果2和3进行了重排序的话线程B进行判断if(instancenull)时就会为true而实际上这个instance并没有初始化成功显而易见对线程B来说之后的操作就会是错得。而用volatile修饰的话就可以禁止2和3操作重排序从而避免这种情况。volatile包含禁止指令重排序的语义其具有有序性。
4. 可见性
可见性是指当一个线程修改了共享变量后其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析当线程获取锁时会从主内存中获取共享变量的最新值释放锁的时候会将共享变量同步到主内存中。从而synchronized具有可见性。同样的在volatile分析中会通过在指令中添加lock指令以实现内存可见性。因此, volatile具有可见性
5. 总结
通过这篇文章主要是比较了synchronized和volatile在三条性质原子性可见性以及有序性的情况归纳如下
synchronized: 具有原子性有序性和可见性 volatile具有有序性和可见性
参考文献
《java并发编程的艺术》 《深入理解java虚拟机》