Java中对于volatile变量,通俗点说可以把它看做多线程之间分享的共享内存,可见性是立即的。
实际上它分成了两部分,volatile write和volatile read。由于Unsafe提供了getXXXVolatile和putXXXVolatile接口。所以这样一来Java中对于能够共享的变量,至少有四种访问方式:
普通写、普通读、putXXXVolatile、getXXXVolatile。
另一方面,像是数组元素Object[] objs,我们仅能将objs声明为volatile,而这样的话对于其中的元素 objs[0]、objs[1]是完全没有效用的,也就是说,两种声明方式:Object[] objs和volatile Object[] objs,对于其中的元素是一样的。此种情况下只能使用Unsafe提供的接口来保证内存可见性。
所以此文来探索下类似于volatile写 + 普通读, 普通写 + volatile读, 这样的情况下是不是真的无法保证可见性。
volatile写 + 普通读
首先来看一个简单粗暴的例子我们往一个初始化为空的长度30000的Object[]中写入数据。另一个线程早在写入数据开始前就从下标0尝试读取数据,假如读到的==null则进入while的循环。除非读到了!=null,则打印数据:
package com.psly.locksupprot;
import com.psly.testatomic.UtilUnsafe;
import sun.misc.Unsafe;
public class TestVolatileSemantics2 {
private static final Unsafe _unsafe = UtilUnsafe.getUnsafe();
private static final int _Obase = _unsafe.arrayBaseOffset(Object[].class);
private static final int _Oscale = _unsafe.arrayIndexScale(Object[].class);
private final static int N = 30000;
private final static Object[] B = new Object[N+1];
private static class Node {
public Node(int value){
this.value = value;
}
private int value;
}
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 1; i <= N; ++i){
_unsafe.putObjectVolatile(B, _Obase + i * _Oscale, new Node(1));
}
System.out.println("Done");
}
});
Thread reader = new Thread(new Runnable(){
public void run(){
for(int i = 1; i <= N; ++i){
while(B[i] == null){}
System.out.println(((Node)B[i]).value + " " + i + " first reader"); }
}
}
);
reader.start();
Thread.sleep(1000);
writer.start();
}
}
在我的电脑上执行它的输出仅有为:
Done
并且始终占据电脑的cpu资源。
尽管我们稍微修改下代码,修改读线程的方式,采取遍历整个数据组,如果不为null则输出。那么它看似是可以读到的:
package com.psly.locksupprot;
import com.psly.testatomic.UtilUnsafe;
import sun.misc.Unsafe;
public class TestVolatileSemantics2 {
private static final Unsafe _unsafe = UtilUnsafe.getUnsafe();
private static final int _Obase = _unsafe.arrayBaseOffset(Object[].class);
private static final int _Oscale = _unsafe.arrayIndexScale(Object[].class);
private final static int N = 30000;
private final static Object[] B = new Object[N+1];
private static class Node {
public Node(int value){
this.value = value;
}
private int value;
}
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 1; i <= N; ++i){
_unsafe.putObjectVolatile(B, _Obase + i * _Oscale, new Node(1));
}
System.out.println("Done");
}
});
Thread reader = new Thread(new Runnable(){
public void run(){
for(;;){
for(int i = 1; i <= N; ++i){
if(B[i] != null){
System.out.println(((Node)B[i]).value + " " + i + " first reader");
}
}
}
}
}
);
reader.start();
Thread.sleep(1000);
writer.start();
}
}
输出为:
1 23132 first reader
1 23133 first reader
1 23134 first reader
1 23135 first reader
1 23136 first reader
1 23137 first reader
1 23138 first reader
1 23139 first reader
1 23140 first reader
1 23141 first reader
1 23142 first reader
1 23143 first reader
1 23144 first reader
1 23145 first reader
1 23146 first reader
1 23147 first reader
1 23148 first reader
1 23149 first reader
1 23150 first reader
1 23151 first reader
1 23152 first reader
1 23153 first reader
1 23154 first reader
1 23155 first reader
1 23156 first reader
1 23157 first reader
1 23158 first reader
1 23159 first reader
1 23160 first reader
1 23161 first reader
1 23162 first reader
但是假如要证明一个规则成立,则必须确保所有符合假设的情况下都成立。而证明一件事情不成立,只需要举一个例子。
所以根据之前的例子,volatile写(compareAndSwapXXX也是一样的) + 普通读,无法保证后者取到更新后的数据。
(更正之前的说法,事实上是,这里的B对应的读取,因为编译器的优化导致B[i]没有读到更新后的值。)
(事实上,只要volatile写之后,无论怎么读都可以读到更新后的值,只要编译器不参与优化)(我的推测)
所以对于最上面的那个例子,我们采用在B前面添加volatile或者读取使用volatile读就可以解决了,代码如下:
package com.psly;
import sun.misc.Unsafe;
public class TestVolatileSemantics2 {
private static final Unsafe _unsafe = UtilUnsafe.getUnsafe();
private static final int _Obase = _unsafe.arrayBaseOffset(Object[].class);
private static final int _Oscale = _unsafe.arrayIndexScale(Object[].class);
private final static int N = 30000;
private volatile static Object[] B = new Object[N+1];
private static class Node {
public Node(int value){
this.value = value;
}
private int value;
}
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 1; i <= N; ++i){
_unsafe.putObjectVolatile(B, _Obase + i * _Oscale, new Node(1));
}
System.out.println("Done");
}
});
Thread reader = new Thread(new Runnable(){
public void run(){
for(int i = 1; i <= N; ++i){
while(B[i] == null){}
System.out.println(((Node)B[i]).value + " " + i + " first reader"); }
}
}
);
reader.start();
Thread.sleep(1000);
writer.start();
}
}
package com.psly;
import sun.misc.Unsafe;
public class TestVolatileSemantics2 {
private static final Unsafe _unsafe = UtilUnsafe.getUnsafe();
private static final int _Obase = _unsafe.arrayBaseOffset(Object[].class);
private static final int _Oscale = _unsafe.arrayIndexScale(Object[].class);
private final static int N = 30000;
private final static Object[] B = new Object[N+1];
private static class Node {
public Node(int value){
this.value = value;
}
private int value;
}
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 1; i <= N; ++i){
_unsafe.putObjectVolatile(B, _Obase + i * _Oscale, new Node(1));
}
System.out.println("Done");
}
});
Thread reader = new Thread(new Runnable(){
public void run(){
for(int i = 1; i <= N; ++i){
while(_unsafe.getObjectVolatile(B, _Obase + i * _Oscale) == null){}
System.out.println(((Node)B[i]).value + " " + i + " first reader"); }
}
}
);
reader.start();
Thread.sleep(1000);
writer.start();
}
}
普通写 + volatile读
我们也跟前面一样举一个反例。
但是在我构造的例子中,普通写 + volatile读都看似读到了更新后的数据。但我们依然所以无法判断究竟是否及时读取到。
为此我参考了dijkstra的Solution of a Problem in Concurrent Programming Control,其中算法截图如下:
其中的Li4,critical section之前的c[i] :=false是写入操作,c[j]为读取操作,critical section之后的c[i]也是写入操作。
好,我们用java实现这个算法,第一个c[i]采用普通写入,c[j]采用getIntVolatile读入,后一个c[i]采用putIntVolatile写入。
假设普通写能够被后面的volatile read读取。那么这里一定能够保证任意时刻只有一个线程处于critical section(根据算法保证,可参考并发控制)。
我们在临界区对变量+1,假如最后的值不符合预期,那么就说明临界区同时进入了不止一个线程,从而说明假设错误。
我们给出的代码如下:
package com.psly.testatomic;
import sun.misc.Unsafe;
public class TestVolatileDijkstraMethodWithNoBlock {
//用于内存保证:putXXVolatile/getXXVolatile
private static final Unsafe _unsafe = UtilUnsafe.getUnsafe();
private static final int _Obase = _unsafe.arrayBaseOffset(int[].class);
private static final int _Oscale = _unsafe.arrayIndexScale(int[].class);
//N:线程数,TIMES每个线程需要进入临界区的次数。
private final static int N = 3;
private final static int TIMES = 1000000;
private final static int[] B = new int[N+1];
private final static int[] C = new int[N+1];
//每个线程进入临界区++count,最终count == N * TIMES
private volatile static long count;
//k与上面的count字段类似
private static int k = 1;
private final static Object kObj;
private final static long kOffset;
static{
for(int i = 1; i <= N; ++i){
B[i] = 1;
C[i] = 1;
}
try {
kObj = _unsafe.staticFieldBase(TestVolatileDijkstraMethodWithNoBlock.class.getDeclaredField("k"));
} catch (Exception e) {
// TODO Auto-generated catch block
throw new Error(e);//e.printStackTrace();
}
try {
kOffset = _unsafe.staticFieldOffset(TestVolatileDijkstraMethodWithNoBlock.class.getDeclaredField("k"));
} catch (Exception e) {
// TODO Auto-generated catch block
throw new Error(e);//e.printStackTrace();
}
}
final static void dijkstrasConcurMethod(int pM){
int times = TIMES;
int i = pM;
L0: for(;;){
B[i] = 0;
L1: for(;;){
if( k != i ) {
//C[i] = 1;
if(B[_unsafe.getIntVolatile(kObj, kOffset)] == 1)
_unsafe.putIntVolatile(kObj, kOffset, i);//k = i;//k = i;
continue L1;
} else{
C[i] = 0;
for(int j = 1; j <= N; ++j )
if(j != i && _unsafe.getIntVolatile(C, _Obase + j * _Oscale) == 0){
//将C[i]的值更新回去,写这里效率更高
_unsafe.putIntVolatile(C, _Obase + i * _Oscale, 1);
continue L1;
}
}
break L1;
}
//临界区开始
++count;
//临界区结束
_unsafe.putIntVolatile(C, _Obase + i * _Oscale, 1);
B[i]=1;
if( --times != 0){
continue L0; //goto L0;
}
return;
}
}
public static void main(String[] args) throws InterruptedException
{
//开始时间
long start = System.currentTimeMillis();
//打印累加器初始值
System.out.println( count + " initial\n");
Thread handle[] = new Thread[N+1];
//创建线程
for (int i = 1; i <= N; ++i){
int j = i;
handle[i] = new Thread(new Runnable(){
@Override
public void run(){
dijkstrasConcurMethod(j);
}
});
}
//线程开始执行
for (int i = 1; i <= N; ++i)
handle[i].start();
//主线程等待子线程结束
for (int i = 1; i <= N; ++i)
handle[i].join();
//打印累加值,== N * TIMES
System.out.println(count);
//打印程序执行时间
System.out.println((System.currentTimeMillis() - start) / 1000.0 + " milliseconds");
}
}
3个线程,每个100000次,最后总共应该是3000000(count)。执行结果是:
0 initial
2999599
0.178 milliseconds
这说明,普通写 + volatile读也是无法保证可见性的。
这里改为volatile写 + volatile读,
_unsafe.putIntVolatile(C, _Obase + i * _Oscale, 0);//C[i] = 0;
就能够得到正确结果:
0 initial
300000
0.023 milliseconds
所以我们的结论是,volatile写 + 普通读 和 普通写 + volatile读都无法保证可见性,请大家在需要及时看见共享内存更新的场景中统一采用volatile写 + volatile读。
以上未给出的UtilUnsafe如下:
package com.psly;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class UtilUnsafe {
private UtilUnsafe() { } // dummy private constructor
/** Fetch the Unsafe. Use With Caution. */
public static Unsafe getUnsafe() {
// Not on bootclasspath
if( UtilUnsafe.class.getClassLoader() == null )
return Unsafe.getUnsafe();
try {
final Field fld = Unsafe.class.getDeclaredField("theUnsafe");
fld.setAccessible(true);
return (Unsafe) fld.get(UtilUnsafe.class);
} catch (Exception e) {
throw new RuntimeException("Could not obtain access to sun.misc.Unsafe", e);
}
}
}