单例是较为常见的设计模式,在实现延迟加载时,会出现线程安全的问题,我们一般采用加锁的方式,不采用加显式锁的方式例如枚举、以及非延迟加载的方式之类的最终虚拟机在执行的时候会帮我们加锁。
这个其实很好理解,我们可以看下如下的代码
class Sim{ private static Sim t = new Sim();
private Sim(){ System.out.println(Thread.currentThread().getName()+"开始加载"); while ( true ); } public static Sim getInstance(){ return t; } } public class Test2 { public static void main(String[] args) { Thread thread = new Thread(()->{ System.out.println(Sim.getInstance()); }); thread.setName("test"); thread.setPriority(10); Thread thread1 = new Thread(()->{ System.out.println(Sim.getInstance()); }); thread1.setName("test111"); thread1.setPriority(1); thread.start(); thread1.start(); } }
这是一个典型的依靠jvm保证线程安全的单例模式,这儿是没有显式的加锁的。我们在Sim的初始化方法中打印了线程名字,并且执行了一个死循环。main方法执行后第一个加载的线程jvm会让其获得锁并执行构造方法,另外一个线程则会等待。打印结果则会如下
发现只有test111加载了 我们查看线程信息发现
我们的test线程处于Object.wait()阻塞状态而不是正常的runnable状态。 test111则是正常的运行状态 且只有test111执行了<clinit>方法,并自旋在了<init>也就是构造函数中
那么如何实现一个不加锁的单例呢? 可以借助cas来实现
先看下不安全的情况
class Single{ private static Single single; private static AtomicReference<Single> sing = new AtomicReference(); private Single(){ Integer i = 0; while ( i++ < 10000 ); }; private static Single getInstance(){ /********不安全********/ if ( single == null ){ single = new Single(); return single; } return single; /********安全********/ // if ( sing.get() != null ){ // return sing.get(); // } // sing.compareAndSet(null,new Single()); // return sing.get(); } public static void main(String[] args) throws InterruptedException { CyclicBarrier cyclicBarrier = new CyclicBarrier(500); CountDownLatch countDownLatch = new CountDownLatch(500); final Map<Single,String> map = new ConcurrentHashMap<>(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 500; i++) { executorService.execute(()->{ try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } Single instance = Single.getInstance(); map.put(instance,"1"); countDownLatch.countDown(); }); } executorService.shutdown(); countDownLatch.await();
System.out.println(String.format("共初始化 %d 个,分别为 %s",map.keySet().size(),map.keySet()));
} }
为了更加容易模拟,我在初始化中加入了稍微费时点儿的操作。并且借助CyclicBarrier 来模拟多个线程同一时间请求。这样在不安全的情况下,得到的结果会可能如下有多个
然后我们把不安全的步骤注释掉,执行安全的步骤时,多次试验下均只有一个
private static Single getInstance(){ /********不安全********/ // if ( single == null ){ // single = new Single(); // return single; // } // return single; /********安全********/ if ( sing.get() != null ){ return sing.get(); }
//只有一个线程会成功 sing.compareAndSet(null,new Single()); return sing.get(); }
这儿的sing.compareAndSet(null,new Single());使用cas指令来保证多个线程对原子引用赋值时确保只有一个会赋值成功。做到了不加锁的情况下实现单例
多次执行结果如下 都只有一个结果
当然了 本案例只是说明如何实现无锁化的单例,但并不推荐,因为这种方式只能保证最终我们获取到的对象都是同一个。而类的构造函数则是可能被执行很多次的。