最近公司的项目中发现一个编译优化导致的bug。同事叙述为“在CPU开启out-of-order execution优化时,是有bug的”。针对这个问题,比较好的优化方法如下:
private static JobManager self; private static object asyncObj = new object(); public static JobManager Instance { get { if (self == null) { lock (asyncObj) { if (self == null) { // 正确的实现方法应该为: var temp = new JobManager(); Interlocked.Exchange(ref self, temp); self = new JobManager(); } } } return self; } }
这里需要解释一下:
self = new JobManager()
这句你的本意是为 JobManager 分配内存,调用构造器初始化字段,再将引用赋给 self ,即发布出来让其他线程可见。但是,那只是你一厢情愿的想法,编译器可能这样做:为JobManager 分配内存,将引用发布到(赋给)self,再调用构造器。然而,如果在将引用发布给 self 之后,调用构造器之前,另一个线程发现 self 不为 null,便开始使用JobManager对象,这时会发生什么?这个时候对象的构造器还没有执行结束!这是一个很难追踪的bug。
internal sealed class MySingleton { private static MySingleton s_value = null; public static MySingleton GetMySingleton() { if (s_value != null) return s_value; MySingleton temp = new MySingleton(); Interlocked.CompareExchange(ref s_value, temp, null); return s_value; } }
虽然多个线程同时调用GetMySingleton,会创建2个或者更多的MySingleton对象,但没有被s_value引用的临时对象会在以后被垃圾回收。大多数应用程序很少会发生同时调用GetMySingleton的情况,所以不太可能出现创建多个MySingleton对象的情况。上述代码带来优势是很明显的,首先,它的速度是非常快,其次,它永不阻塞线程。这就解决了前面在双检锁技术中提出的问题。
public static void Main() { Lazy<string> s = new Lazy<string>(() => DateTime.Now.ToLongTimeString(), LazyThreadSafetyMode.PublicationOnly); Console.WriteLine(s.IsValueCreated); Console.WriteLine(s.Value); Console.WriteLine(s.IsValueCreated); Thread.Sleep(5000); Console.WriteLine(s.Value); Console.WriteLine(DateTime.Now.ToLongTimeString()); }
public static void Main() { string name = null; LazyInitializer.EnsureInitialized(ref name, () => "Benjamin"); Console.WriteLine(name); LazyInitializer.EnsureInitialized(ref name, () => "Yao"); Console.WriteLine(name); }
public enum LazyThreadSafetyMode { None = 0, //完全没有线程安全劫持(适合GUI应用程序) PublicationOnly = 1, //使用Interlocked.CompareExchange技术 ExecutionAndPublication = 2, //使用双检锁技术 }