在单线程方案中,此代码执行没有问题。但是在多线程并发方案中,此代码对于从不同线程创建和获取事物有问题。问题的原因与普通的Double Checked Locking(DCL)10模式相似,也就是说,SomeThing的构造和SomeThing引用在构造中对对象变量的分配可能会重新排序。导致返回在get中构造的不完整的SomeThing对象实例。为了解决这个问题,通常的方法是使用volatile来修改对象字段。与使用同步块相比,此方法避免了重新排序,确保了内存可见性并减少了性能损失。但是,如果使用情况对对象的内存可见性不敏感(不需要线程来写入对象,则对象的新值将立即对下一个读取线程可见),在Intel 64 / IA中-32环境,有更好的解决方案。
根据上一章的内容,我们知道在Intel 64 / IA-32下的写操作之间将没有重新排序,即SomeThing对象的构造与对象中对象的分配之间的顺序。处理器的性可以得到保证。似乎仅使用volatile来避免重新排序是不必要的。但是,Java编译器可能会生成重新排序的指令。但是好消息是Oracle的JDK提供了三种方法:不安全。 putOrderedObject,不安全。 putOrderedInt和Unsafe。 putOrderedLong。执行这三种方法时,JDK将插入StoreStore内存屏障,以避免对写操作进行重新排序。在Intel 64 / IA-32架构中,不需要StoreStore障碍,并且Java编译器将删除StoreStore障碍。与写入volatile变量后执行StoreLoad barrier的巨大开销相比,该方法除了避免了因重新排序而导致的性能损失之外,不会带来其他性能开销。

我们将做一个小实验来比较两者之间的性能差异。一种是使用volatile修改对象成员变量。
1 public class Container { 2 public static class SomeThing { 3 private int status; 4 5 public SomeThing() { 6 status = 1; 7 } 8 9 public int getStatus() { 10 return status; 11 } 12 } 13 14 private volatile SomeThing object; 15 16 public void create() { 17 object = new SomeThing(); 18 } 19 20 public SomeThing get() { 21 while (object == null) { 22 Thread.yield(); //不加这句话可能会在此出现无限循环 23 } 24 return object; 25 } 26 }
一种是使用不安全。 putOrderedObject以避免在适当位置重新排序。
1 public class Container { 2 public static class SomeThing { 3 private int status; 4 5 public SomeThing() { 6 status = 1; 7 } 8 9 public int getStatus() { 10 return status; 11 } 12 } 13 14 private SomeThing object; 15 16 private Object value; 17 private static final Unsafe unsafe = getUnsafe(); 18 private static final long valueOffset; 19 static { 20 try { 21 valueOffset = unsafe.objectFieldOffset(Container.class.getDeclaredField("value")); 22 } catch (Exception ex) { throw new Error(ex); } 23 } 24 25 public void create() { 26 SomeThing temp = new SomeThing(); 27 unsafe.putOrderedObject(this, valueOffset, null); //将value赋null值只是一项无用操作,实际利用的是这条语句的内存屏障 28 object = temp; 29 } 30 31 public SomeThing get() { 32 while (object == null) { 33 Thread.yield(); 34 } 35 return object; 36 } 37 38 39 public static Unsafe getUnsafe() { 40 try { 41 Field f = Unsafe.class.getDeclaredField("theUnsafe"); 42 f.setAccessible(true); 43 return (Unsafe)f.get(null); 44 } catch (Exception e) { 45 } 46 return null; 47 } 48 }
由于直接调用Unsafe.getUnsafe()需要配置JRE以获得更高的权限,因此我们使用反射来获取Unsafe中的UnSafe以获得可用的Unsafe实例。
unsafe.putOrderedObject(this,valueOffset,null)
此句子仅是借用此句子的功能以防止重新排序,而没有其他作用。
使用以下代码测试两个程序的实际运行时间。在生产环境中长时间运行后,在运行时打开-server和-XX:CompileThreshold = 1可以模拟JIT优化效果。
1 public static void main(String[] args) throws InterruptedException { 2 final int THREADS_COUNT = 20; 3 final int LOOP_COUNT = 100000; 4 5 long sum = 0; 6 long min = Integer.MAX_VALUE; 7 long max = 0; 8 for(int n = 0;n <= 100;n++) { 9 final Container basket = new Container(); 10 ListputThreads = new ArrayList (); 11 List takeThreads = new ArrayList (); 12 for (int i = 0; i < THREADS_COUNT; i++) { 13 putThreads.add(new Thread() { 14 @Override 15 public void run() { 16 for (int j = 0; j < LOOP_COUNT; j++) { 17 basket.create(); 18 } 19 } 20 }); 21 takeThreads.add(new Thread() { 22 @Override 23 public void run() { 24 for (int j = 0; j < LOOP_COUNT; j++) { 25 basket.get().getStatus(); 26 } 27 } 28 }); 29 } 30 long start = System.nanoTime(); 31 for (int i = 0; i < THREADS_COUNT; i++) { 32 takeThreads.get(i).start(); 33 putThreads.get(i).start(); 34 } 35 for (int i = 0; i < THREADS_COUNT; i++) { 36 takeThreads.get(i).join(); 37 putThreads.get(i).join(); 38 } 39 long end = System.nanoTime(); 40 long period = end - start; 41 if(n == 0) { 42 continue; //由于JIT的编译,第一次执行需要更多时间,将此时间不计入统计 43 } 44 sum += (period); 45 System.out.println(period); 46 if(period < min) { 47 min = period; 48 } 49 if(period > max) { 50 max = period; 51 } 52 } 53 System.out.println("Average : " + sum / 100); 54 System.out.println("Max : " + max); 55 System.out.println("Min : " + min); 56 }
在作者的计算机上运行测试,可变方案的结果如下
平均值:62535770
最大:82515000
最低:45161000
unsafe.putOrderedObject方案的运行结果如下
平均值:50746230
最大:68999000
最小值:38038000
从结果可以看出,与volatile方案相比,unsafe.putOrderedObject方案使平均时间消耗减少了18.9%,最大时间消耗减少了16.4%,并且最小时间消耗减少了15.8%。另外,即使在发生写-写重新排序的其他处理器中,由于StoreStore屏障的性能损失小于StoreLoad屏障,因此该方法也是可行的解决方案。但是,再次值得注意的是,该方案不是等效替代易失性语义,而是在特定情况下进行的特殊优化。它仅避免了写-写重排序,但不能保证内存可见性。
###附加了1个实验代码以重现重新排序现象
1 public class Test { 2 private static int x = 0, y = 0; 3 private static int a = 0, b =0; 4 5 public static void main(String[] args) throws InterruptedException { 6 int i = 0; 7 for(;;) { 8 i++; 9 x = 0; y = 0; 10 a = 0; b = 0; 11 Thread one = new Thread(new Runnable() { 12 public void run() { 13 //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间. 14 shortWait(100000); 15 a = 1; 16 x = b; 17 } 18 }); 19 20 Thread other = new Thread(new Runnable() { 21 public void run() { 22 b = 1; 23 y = a; 24 } 25 }); 26 one.start();other.start(); 27 one.join();other.join(); 28 String result = "第" + i + "次 (" + x + "," + y + ")"; 29 if(x == 0 && y == 0) { 30 System.err.println(result); 31 break; 32 } else { 33 System.out.println(result); 34 } 35 } 36 } 37 38 39 public static void shortWait(long interval){ 40 long start = System.nanoTime(); 41 long end; 42 do{ 43 end = System.nanoTime(); 44 }while(start + interval >= end); 45 } 46 }
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-354009-2.html
这样的言论就应当受到处理