什么是单例模式?

单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
八种单例模式:

  1. **1.饿汉式(静态常量)**
  1. public class SingletonTest01 {
  2. public static void main(String[] args) {
  3. Singleton instance1=Singleton.getInstance();
  4. Singleton instance2=Singleton.getInstance();
  5. System.out.println(instance1==instance2);
  6. }
  7. }
  8. //饿汉式(静态变量)
  9. class Singleton{
  10. //构造函数定义成private,禁止外部创建Singleton实例(下面的例子省略定义私有构造方法)
  11. private Singleton1(){}
  12. //1.本类内部创建对象实例
  13. public static final Singleton instance=new Singleton();
  14. //2.提供一个公有的静态方法,返回实例对象
  15. public static Singleton getInstance(){
  16. return instance;
  17. }
  18. }

优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题;
缺点:在类装载的时候就完成实例化,没有达到懒加载的效果,如果从始至终从未使用过这个实例,则会造成内存的浪费

2.饿汉式(静态代码块)

  1. public class SingletonTest02 {
  2. public static void main(String[] args) {
  3. Singleton instance1= Singleton.getInstance();
  4. Singleton instance2= Singleton.getInstance();
  5. System.out.println(instance1==instance2);
  6. }
  7. }
  8. //饿汉式(静态代码块)
  9. class Singleton{
  10. .....
  11. //1.本类内部创建对象实例
  12. public static Singleton instance;
  13. static {
  14. instance=new Singleton();
  15. }
  16. //2.提供一个公有的静态方法,返回实例对象
  17. public static Singleton getInstance(){
  18. return instance;
  19. }
  20. }

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码快中的代码,初始化类的实例

3.懒汉式(线程不安全)

  1. public class SingletonTest03 {
  2. public static void main(String[] args) {
  3. Singleton instance1= Singleton.getInstance();
  4. Singleton instance2= Singleton.getInstance();
  5. System.out.println(instance1==instance2);
  6. }
  7. }
  8. class Singleton{
  9. private static Singleton instance;
  10. ...
  11. //提供一个静态的公有方法,当使用到该方法时,才会创建instance
  12. //懒汉式写法,线程不安全
  13. public static Singleton getInstance(){
  14. if (instance==null){
  15. instance=new Singleton();
  16. }
  17. return instance;
  18. }
  19. }

起到了懒加载的效果,但是只能在单线程下使用

4.懒汉式(线程安全)

  1. public class SingletonTest04 {
  2. public static void main(String[] args) {
  3. Singleton instance1= Singleton.getInstance();
  4. Singleton instance2= Singleton.getInstance();
  5. System.out.println(instance1==instance2);
  6. }
  7. }
  8. class Singleton{
  9. private static Singleton instance;
  10. ...
  11. //懒汉式线程安全写法
  12. public static synchronized Singleton getInstance(){
  13. if (instance==null){
  14. instance=new Singleton();
  15. }
  16. return instance;
  17. }
  18. }

解决了线程安全问题;
效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低

接下来介绍推荐使用的方法

5.双重检查

  1. public class SingletonTest05 {
  2. public static void main(String[] args) {
  3. Singleton instance1= Singleton.getInstance();
  4. Singleton instance2= Singleton.getInstance();
  5. System.out.println(instance1==instance2);
  6. }
  7. }
  8. class Singleton{
  9. private static Singleton singleton;
  10. ...
  11. public static Singleton getInstance(){
  12. //双重检查 适合多线程+有懒加载机制
  13. if(singleton==null){
  14. synchronized (Singleton.class){
  15. if(singleton==null){
  16. singleton=new Singleton();
  17. }
  18. }
  19. }
  20. return singleton;
  21. }
  22. }

当还没有实例 且 多线程的情况下 都进来第一层if判断—>通过, 然后第一个抢占到锁的线程经过第二层if判断—>通过,然后创建了实例 ;第二个线程经过第二次if判断—>发现有实例—->退出,以后任何线程都直接返回第一次创建的实例

6.静态内部类

  1. class Singleton{
  2. private static Singleton singleton;
  3. ...
  4. public static class SingletonInstance{
  5. //静态内部类实现 线程安全+懒加载
  6. public static final Singleton INSTANCE =new Singleton();
  7. }
  8. //提供一个公有的方法 返回该类实例
  9. public static Singleton getInstance(){
  10. return SingletonInstance.INSTANCE;
  11. }
  12. }

为什么要使用内部类?

1.如果一个类A只为另一个类B服务,那把A嵌套在B中,就相当于B的一个服务类,代码可读性更高,方便维护。
2.如果一个类A要调用另一个类B,但B又不想被其他类所引用,B就要申明为private,这样A就调用不了B,于是把A嵌套在B中,就可以同时满足前面的两个要求了,代码的封装性更好了。

静态内部类和非静态内部类的区别:

其实我们可以把静态内部类看成和外部类平级的一个类,我们在调用静态内部类时甚至都不用初始化外部类。
静态内部类和非静态内部类一样,都不会因为外部类的加载而加载。
静态内部类的加载不同于非静态内部类,静态内部类使用时就会加载,但如果想实例化一个非静态内部类,则必须先实例化外部类,然后根据外部类的实例就可以创建非静态内部类实例

该方式的优点:
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存—->懒加载。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

补充(类的加载在JVM篇):方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的方法在多线程环境下被正确的加锁同步,这里的静态变量的赋值操作进行编译之后实际上就是一个代码,当我们执行getInstance方法的时候,会导致SingletonInstance类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。

7.枚举(最推荐)

  1. public class SingletonTest07 {
  2. public static void main(String[] args) {
  3. Singleton instance = Singleton.INSTANCE;
  4. }
  5. }
  6. enum Singleton{
  7. INSTANCE;
  8. Singleton(){
  9. }
  10. }

这借助了JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步问题,代码简洁,而且还能防止反序列化重新创建新的对象

补充:什么是枚举?

我们学习过单例模式,即一个类只有一个实例。而枚举其实就是多例,一个类有多个实例,但实例的个数不是无穷的,是有限个数的。我们称呼枚举类中实例为枚举项。一般一个枚举类的枚举项的个数不应该太多,如果一个枚举类有30个枚举项就太多了!

2.定义枚举类型
定义枚举类型需要使用enum关键字,例如:

  1. public enum Direction {
  2. FRONT, BEHIND, LEFT, RIGHT;
  3. }
  4. Direction d = Direction.FRONT;

注意,定义枚举类的关键字是enum,而不是Enum,所有关键字都是小写的!
其中FRONT、BEHIND、LEFT、RIGHT都是枚举项,它们都是本类的实例,本类一共就只有四个实例对象。

在定义枚举项时,多个枚举项之间使用逗号分隔,最后一个枚举项后需要给出分号。不能使用new来创建枚举类的对象,因为枚举类中的实例就是类中的枚举项,所以在类外只能使用类名.枚举项。

3.枚举与switch
枚举类型可以在switch中使用
Direction d = Direction.FRONT;
switch(d) {
case FRONT: System.out.println(“前面”);break;
case BEHIND:System.out.println(“后面”);break;
case LEFT: System.out.println(“左面”);break;
case RIGHT: System.out.println(“右面”);break;
default:System.out.println(“错误的方向”);
}
Direction d1 = d;
System.out.println(d1);
注意,在switch中,不能使用枚举类名称,例如:“case Direction.FRONT:”这是错误的,因为编译器会根据switch中d的类型来判定每个枚举类型,在case中必须直接给出与d相同类型的枚举选项,而不能再有类型。

4.所有枚举类都是Enum的子类
所有枚举类都默认是Enum类的子类,无需我们使用extends来继承。这说明Enum中的方法所有枚举类都拥有。

5.枚举类的构造器
枚举类也可以有构造器,构造器默认都是private修饰,而且只能是private。因为枚举类的实例不能让外界来创建!

  1. enum Direction {
  2. FRONT, BEHIND, LEFT, RIGHT;
  3. Direction()//枚举类的构造器不可以添加访问修饰符,枚举类的构造器默认是private的。但你自己不能添加private来修饰构造器
  4. {
  5. System.out.println("hello");
  6. }
  7. }

其实创建枚举项就等同于调用本类的无参构造器,所以FRONT、BEHIND、LEFT、RIGHT四个枚举项等同于调用了四次无参构造器,所以你会看到四个hello输出。

6.其实枚举类和正常的类一样,可以有实例变量,实例方法,静态方法等等

为什么说枚举可以解决线程安全问题?

  1. public enum T {
  2. SPRING,SUMMER,AUTUMN,WINTER;
  3. }

反编译后:

  1. public final class T extends Enum
  2. {
  3. //省略部分内容
  4. public static final T SPRING;
  5. public static final T SUMMER;
  6. public static final T AUTUMN;
  7. public static final T WINTER;
  8. private static final T ENUM$VALUES[];
  9. static
  10. {
  11. SPRING = new T("SPRING", 0);
  12. SUMMER = new T("SUMMER", 1);
  13. AUTUMN = new T("AUTUMN", 2);
  14. WINTER = new T("WINTER", 3);
  15. ENUM$VALUES = (new T[] {
  16. SPRING, SUMMER, AUTUMN, WINTER
  17. });
  18. }
  19. }

线程安全原理同方法加载机制。

那么枚举类的防止反序列化创建实例是为什么?

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。