作者:汤圆
个人博客:javalover.cc
前言
有时候我们的类并不需要很多个实例,在程序运行期间,可能只需要一个实例就够了,多了反而会出现数据不一致的问题;
这时候我们就可以用单例模式来实现,然后程序中所有的操作都基于这个实例;
目录
单例模式有很多种,这里我们先列举下:
- 饿汉模式
- 懒汉模式-线程不安全
- 懒汉模式-线程安全
- 懒汉模式-线程不是很安全
- 懒汉模式-双重检查
- 静态内部类
- 枚举
正文
1. 饿汉模式(不推荐)
饿汉模式的核心就是第一次加载类的时候,进行数据的初始化;
而且这个数据不可被修改(final);
后续只能读,不能写。
这样一来,就保证了数据的准确性;
下面我们看下示例
package pattern.singleton;// 饿汉模式(不推荐),因为占内存public class HungryDemo { private static final HungryDemo hungryDemo = new HungryDemo(); private HungryDemo() { } public static HungryDemo getInstance(){ return hungryDemo; } public static void main(String[] args) { HungryDemo hungryDemo1 = HungryDemo.getInstance(); HungryDemo hungryDemo2 = HungryDemo.getInstance(); System.out.println(hungryDemo1); System.out.println(hungryDemo2); System.out.println(hungryDemo1 == hungryDemo2); }}
从主程序中可以看到,不管获取多少次实例,都是同一个。
优点:在类第一次加载时就创建单例,线程安全
缺点:占内存,不管用不用,都要先加载
2. 懒汉模式-线程不安全(不推荐)
懒汉模式,就是类初始化时不加载数据,等到需要的时候才加载;
下面看示例:
package pattern.singleton;// 懒汉模式-线程不安全(不推荐)public class LazyDemo1 { private static LazyDemo1 lazyDemo; private LazyDemo1(){ } public static LazyDemo1 getInstance(){ if(lazyDemo == null) lazyDemo = new LazyDemo1(); return lazyDemo; } public static void main(String[] args) { LazyDemo1 l1 = LazyDemo1.getInstance(); LazyDemo1 l2 = LazyDemo1.getInstance(); System.out.println(l1); System.out.println(l2); System.out.println(l1 == l2); }}
这样做的好处就是节省资源,只在需要的时候才加载;
但是会导致一个问题,就是线程的安全性;
比如两个线程同时获取,有可能获取到不同的实例;
优点:懒加载,使用时才加载,节省资源
缺点:线程不安全,多线程有可能创建多个单例
3. 懒汉模式-线程安全(不推荐)
上面的懒汉模式,最大的缺点就是线程不安全;
所以我们可以升级一下,通过加锁来解决,如下所示
package pattern.singleton;// 懒汉模式-线程安全(不推荐)public class LazyDemo2 { private static LazyDemo2 lazyDemo; private LazyDemo2(){ } // 给方法加锁,线程安全了,但是效率低 public static synchronized LazyDemo2 getInstance(){ if(lazyDemo == null) lazyDemo = new LazyDemo2(); return lazyDemo; } public static void main(String[] args) { LazyDemo2 l1 = LazyDemo2.getInstance(); LazyDemo2 l2 = LazyDemo2.getInstance(); System.out.println(l1); System.out.println(l2); System.out.println(l1 == l2); }}
这样一来,不管多少个线程去获取实例,都只会获取到同一个;
但是缺点也很明显,就是效率低;
比如现在已经遗弃的vector类,就是通过给方法上锁,来解决安全问题
优点:线程安全,懒加载
缺点:效率低,每次获取单例都要操作锁
4. 懒汉模式-线程不是很安全(不推荐)
这一次,我们又升级了上面的懒汉模式,把方法锁改为代码块锁,减小了锁的范围;
package pattern.singleton;import java.lang.management.ThreadInfo;// 懒汉模式-线程不安全(不推荐)// 解释:虽然加了代码同步块,但是还是存在线程不安全的情况public class LazyDemo3 { private static LazyDemo3 lazyDemo; private static int count = 0; private LazyDemo3(){ } public static LazyDemo3 getInstance(){ if(lazyDemo == null){ // 1. 所有的线程会先执行下面的打印,然后第一个线程先获得锁,其他线程依次排队等待解锁 System.out.println(Thread.currentThread().getName()); synchronized (LazyDemo3.class){ try { System.out.println(Thread.currentThread().getName()+"等待中"); // 2. 当第一个进来的线程在这里休眠时,其他外面的线程是获取不到锁的,就会一直等待 Thread.sleep(1000); lazyDemo = new LazyDemo3(); System.out.println(Thread.currentThread().getName()+"等待结束"); // 3. 此时第一个线程释放锁,第二个线程因为已经通过了if(lazyDemo == null)的判断 // 所以会直接获取锁,然后重复刚才的步骤2,这样就会导致实例 lazyDemo 被创建多次 } catch (InterruptedException e) { e.printStackTrace(); } } } return lazyDemo; } public static void main(String[] args) { for (int i = 0; i < 2; i++) { new Thread(new Runnable() { @Override public void run() { LazyDemo3 l1 = LazyDemo3.getInstance(); System.out.println(l1); } }).start(); } }}/** * 下面是输出 * * Thread-0 * Thread-0等待中 * Thread-1 // 此时Thread-1已经通过了if()校验 * Thread-0等待结束 // Thread-0 释放锁 * Thread-1等待中 // Thread-1 获取锁 * pattern.singleton.LazyDemo3@568acee4 // 这是 Thread-0 创建的单例 * Thread-1等待结束 // Thread-1 释放锁 * pattern.singleton.LazyDemo3@3f580216 // 这是 Thread-1 创建的单例,此时就有了两个单例,就出问题了 * * */
通过例子可以看到,这两个线程交替执行去获取实例,虽然效率有所提高,但是结果却创建了两个实例,因小失大
所以这种方式也不推荐
优点:懒加载,比上面的 LazyDemo2 效率高
缺点:有可能导致线程不安全,详情见代码(需亲测看到效果,才好理解)
5. 懒汉模式-双重检查(推荐)
前面的几种懒汉模式,都是各有各的不足;
所以这里来个大招,将上面的不足都解决掉;
也就是双重检查模式。
package pattern.singleton;// 懒汉模式-双重检查(推荐)public class LazyDemo4 { // 保证可见性,即在多线程时,一个线程修改了这个变量,则其他线程立马就可以看到变化 private static volatile LazyDemo4 lazyDemo; private LazyDemo4(){ } public static LazyDemo4 getInstance(){ if(lazyDemo == null) // 加同步代码块,保证当前只有一个线程在修改 lazyDemo synchronized (LazyDemo4.class){ // 加双重检查,其他后面进来的线程,如果看到 lazyDemo 已经创建了,则不再创建,直接返回 if(lazyDemo == null) lazyDemo = new LazyDemo4(); } return lazyDemo; } public static void main(String[] args) { LazyDemo4 l1 = LazyDemo4.getInstance(); LazyDemo4 l2 = LazyDemo4.getInstance(); System.out.println(l1); System.out.println(l2); System.out.println(l1 == l2); }}
可以看到,这里在获取到锁之后,又加了一个null判断,这样就可以保证在创建实例之前,确保实例真的是null
优点:懒加载、线程安全、效率高
6. 静态内部类(推荐)
这个就比较简单了,不需要加锁,也不需要考虑null判断,直接将实例封装到内部类中,再用final修饰为不可变;
从而保证了这个实例的唯一性;
这个其实就是结合了前面的 饿汉模式 和 懒汉模式-双重检查。
package pattern.singleton;// 静态内部类(推荐)public class StaticInnerDemo { private StaticInnerDemo(){ }; // 静态内部类 // 1. 当 StaticInnerDemo 加载时,下面的 InnerInstace 并没有加载 // 2. 当 调用getInstance()时,下面的静态内部类才会加载,且只会加载一次(因为final常量) private static class InnerInstance{ private static final StaticInnerDemo staticInnerDemo = new StaticInnerDemo(); } public static StaticInnerDemo getInstance(){ return InnerInstance.staticInnerDemo; } public static void main(String[] args) { StaticInnerDemo staticInnerDemo1 = getInstance(); StaticInnerDemo staticInnerDemo2 = getInstance(); System.out.println(staticInnerDemo1); System.out.println(staticInnerDemo2); System.out.println(staticInnerDemo1 == staticInnerDemo2); }}
优点:懒加载,线程安全(详情见代码)
7. 枚举(推荐)
最后来个压轴的,通过枚举来实现单例模式;
这个可以说是极简主义风格,自带单例效果;
因为不需要过多的修饰,只是单纯的定义一个枚举,然后创建一个实例,后面程序直接用这个实例就可以了。
package pattern.singleton;// 枚举(推荐)public enum EnumDemo { INSTANCE; public static void main(String[] args) { EnumDemo instance1 = EnumDemo.INSTANCE; EnumDemo instance2 = EnumDemo.INSTANCE; System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); }}
优点:懒加载,线程安全,效率高,大牛推荐(Effective Java作者推荐)
总结
关于单例模式的实现方式,首推的就是枚举,其次是懒汉模式-双重检查,最后是静态内部类