并发编程以内存可见性java
在上篇线程安全中,咱们已经知道须要使用锁来同步管理对可变状态的访问操做。今天咱们来看下并发编程的内存可见性问题。编程
同步代码块除了实现原子性或者临界区以外,其还保证了内存可见性,即保证其余线程能够看到状态的变化结果。缓存
private static boolean stop =false; private static int number = 0; public static class ReaderThread extends Thread { public void run() { while(!stop) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number=42; stop=true; }
在代码中,主线程和读线程都将访问stop和number。主线程启动读线程并将number设置为42、stop设置为true。读线程一直循环知道发现stop为true中止安全
并输出number的值。因为代码没有足够的同步机制,以及可能会存在指令重排序,因此最终可能会输出42,也可能会输出0,也有可能会一直循环没法停下来。多线程
注意:并发
在没有同步的状况下,编译器、处理器、运行时等均可能对操做执行顺序进行调整,所以没法对内存的操做顺序进行断定。this
因为cpu多级缓存的存在,多线程共享状态的修改涉及到各级缓存、内存之间的同步问题。spa
1、失效数据线程
在上边的例子中,读线程可能会读取stop变量失效的值,从而致使程序无限循环而没法中止下来。code
失效的数据可能致使引用失效、意外的异常、非法的数据、死循环等问题。
例以下边的 UnSafeInteger类并非线程安全的,可能致使一个线程set以后,另一个线程get的时候可能得不到最新的值。
package com.codeartist; public class UnSafeInteger { private int value; public int get() { return this.value; } public void set(int value) { this.value = value; } }
咱们能够经过对set和get使用同一个锁进行同步,这样就能够防止get到失效的值。
package com.codeartist; public class SyncInteger { private int value; public synchronized int get() { return this.value; } public synchronized void set(int value) { this.value = value; } }
2、非原子的64位操做和Volatile
失效的数据最终读取的仍是以前线程设置的值,因为变量的读取和写入操做都是原子性的,因此绝大多数的变量均可以保证这个最低安全性。
读取和写入64位的long和double变量的操做会分解为两个32位的操做。在多个线程写入和读取时可能致使读取某个值的高32位和另一个值的低32位。
java和.NET都提供了一种弱的同步机制,即volatile变量,访问volatile变量不会加锁致使阻塞。
volatile经过确保不会对共享状态变量施加的操做进行重排序,也不会将其缓存到cpu不可见的地方,从而保证了共享状态的内存可见性。
volatile变量值确保可见性,并不能确保操做的原子性,因此其只适合做为简单的标志判断操做是否完成、中断等。
只有知足如下条件的时候,才应该考虑使用volatile
1.对该变量的操做不依赖变量的当前值,好比自增、自减都是不适合使用的。
2.该变量不会与其余状态变量一块儿做为不变性条件。
3.访问该变量不须要加锁进行同步。
3、锁定与可见性
加锁能够确保某个线程以一种可预测的方式查看其余线程的执行结果,即下图中,当线程A获取M锁执行同步代码块的时看到的变量x的值,待锁释放以后,线程B也能够看到。
只要保证读取和写入操做使用同一个锁同步,便可以保证操做的互斥性,也能够保证全部线程都能看到共享变量的最新值的内存可见性。