设计模式-01单例模式 Singleton

单例设计模式介绍

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

比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够,这是就会使用到单例模式

单例模式有八种方式:

  1. 饿汉式(静态常量)
  2. 饿汉式(静态代码块)
  3. 懒汉式(线程不安全)
  4. 懒汉式(线程安全,同步方法)
  5. 懒汉式(线程安全,同步代码块)
  6. 双重检查
  7. 静态内部类
  8. 枚举

1.饿汉式(静态常量)应用实例

  1. 构造器私有化 (防止 new )
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法。getInstance
public class SingletonTest01 {

    public static void main(String[] args) {
        //测试
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance == instance2); // true
        System.out.println("instance.hashCode=" + instance.hashCode());
        System.out.println("instance2.hashCode=" + instance2.hashCode());
        System.out.println("index=" + instance2.getIndex());// 0
        Singleton instance3 = null;
        if (null == instance3) {
            System.out.println("单例模式可以赋值为null");
        }
    }
}

//饿汉式(静态变量)
class Singleton {

    //单例模式中属性也要是 final 类型的,保证此类实例在任何地方的属性值都一样,final 类型没有set方法
    private final String index;

    //1. 构造器私有化, 外部能new
    private Singleton(String index) {
        this.index = index;
    }

    //2.本类内部创建对象实例,静态常量必须要进行显性初始化(赋值)
    private final static Singleton instance = new Singleton("0");

    //3. 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }

    public String getIndex() {
        return index;
    }
}

优缺点说明:

  1. 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
  2. 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
  3. 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果
  4. 结论:这种单例模式可用,可能造成内存浪费

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

//饿汉式(静态变量)
class Singleton {

    //1. 构造器私有化, 外部能new
    private Singleton() {
    }

    //2.本类内部创建对象实例
    private static Singleton instance;
    
    //代码块不能被调用自动执行,用来初始化类或对象
    //静态代码快随着类的加载而执行(不是加载是执行),并且只执行一次
    //非静态代码块随着对象的创建而执行,每创建一个对象执行一次、
    //静态方法随类的加载而加载,不执行
    //静态属性会随着类的加载而初始化(赋值)
    
    //在静态代码块中,创建单例对象
    static { 
        instance = new Singleton();
    }
    //3. 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

优缺点说明:

  1. 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
  2. 结论:这种单例模式可用,但是可能造成内存浪费

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

public class SingletonTest03 {
	public static void main(String[] args) {
		System.out.println("懒汉式1 , 线程不安全~");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}
}

class Singleton {
	private static Singleton instance;
	private Singleton() {}
	
	//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
	//即懒汉式
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

优缺点说明:

  1. 起到了Lazy Loading的效果,但是只能在单线程下使用。
  2. 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
  3. 结论:在实际开发中,不要使用这种方式

4. 懒汉式(线程安全,同步方法)

public class SingletonTest04 {
	public static void main(String[] args) {
		System.out.println("懒汉式2 , 线程安全~");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}
}

// 懒汉式(线程安全,同步方法)
class Singleton {
	private static Singleton instance;
	private Singleton() {}
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	//即懒汉式
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

优缺点说明:

  1. 解决了线程不安全问题
  2. 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
  3. 结论:在实际开发中,不推荐使用这种方式

5. 懒汉式(线程安全,同步代码块)

class Singleton {
	private static Singleton instance;
	private Singleton() {}
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class){
				instance = new Singleton();
			}
		}
		return instance;
	}
}

优缺点说明:

  1. 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
  2. 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
  3. 结论:在实际开发中,不能使用这种方式

6. 双重检查

public class SingletonTest06 {

	public static void main(String[] args) {
		System.out.println("双重检查");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

class Singleton {
	//volatile 禁止指令重排,保证可见性,不保证原子性。这里主要是禁止指令重排,防止多线程指令重排,新建对象出错(新建对象指令分三步:分配地址空间,初始化对象,对象获取地址空间)
	private static volatile Singleton instance;
	
	private Singleton() {}
	//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
	//同时保证了效率, 推荐使用
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

优缺点说明:

  1. Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
  2. 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.
  3. 线程安全;延迟加载;效率较高
  4. 结论:在实际开发中,推荐使用这种单例设计模式

7. 静态内部类

public class SingletonTest07 {
	public static void main(String[] args) {
		System.out.println("使用静态内部类完成单例模式");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
		
	}
}

// 静态内部类完成, 推荐使用
class Singleton {
	private static volatile Singleton instance;
	
	//构造器私有化
	private Singleton() {}	
	//写一个静态内部类,该类中有一个静态属性 Singleton
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}	
	//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
	public static Singleton getInstance() {
		return SingletonInstance.INSTANCE;
	}
}

优缺点说明:

  1. 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  2. 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
  4. 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
  5. 结论:推荐使用.

8. 枚举

public class SingletonTest08 {
	public static void main(String[] args) {
		Singleton instance = Singleton.INSTANCE;
		Singleton instance2 = Singleton.INSTANCE;
		System.out.println(instance == instance2);
		System.out.println(instance.hashCode());
		System.out.println(instance2.hashCode());
		instance.sayOK();
	}
}

//使用枚举,可以实现单例, 推荐
enum Singleton {
	INSTANCE; //属性
	public void sayOK() {
		System.out.println("ok~");
	}
}

优缺点说明:

  1. 这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
  2. 这种方式是Effective Java作者Josh Bloch 提倡的方式
  3. 结论:推荐使用

单例模式注意事项和细节说明

  1. 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
  3. 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)

JDK中,java.lang.Runtime就是经典的单例模式(饿汉式)

补充:

单例模式的优点:

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

应用场景

  1. 网站的计数器,一般也是单例模式实现,否则难以同步。
  2. 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  3. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  4. 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
  5. Application 也是单例的典型应用
  6. Windows的Task Manager (任务管理器)就是很典型的单例模式
  7. Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

枚举

JDK1.5之前需要自定义枚举类
JDK 1.5 新增的 enum 关键字用于定义枚举类
若枚举只有一个成员, 则可以作为一种单例模式的实现方式

枚举类的属性

枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰
枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数

JDK1.5之前枚举类:

public class TestSeason {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        Season spring1 = null;
        System.out.println(spring);
        spring.show();
        System.out.println(spring.getSeasonName());
    }
}

//枚举类
class Season {
    //1.提供类的属性,声明为private final,只能赋值一次,在构造器中赋值
    //这里不能声明为 static final,static final修饰,属于类又只能赋值一次,必须直接赋值进行初始化
    //不能通过其他方式赋值,因为其他方式声明对象的时候也可以用到,不能保证类的属性只赋值一次
    //private static final String seasonName = "赋值";
    private final String seasonName;
    private final String seasonDesc;

    //2.声明为final的属性,在构造器中初始化。
    private Season(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    //3.通过公共的方法来调用属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
    //4.创建枚举类的对象:将类的对象声明public static final
    public static final Season SPRING = new Season("spring", "春暖花开");
    public static final Season SUMMER = new Season("summer", "夏日炎炎");
    public static final Season AUTUMN = new Season("autumn", "秋高气爽");
    public static final Season WINTER = new Season("winter", "白雪皑皑");

    @Override
    public String toString() {
        return "Season [seasonName=" + seasonName + ", seasonDesc="
                + seasonDesc + "]";
    }

    public void show() {
        System.out.println("这是一个季节");
    }
}

Enum枚举类

必须在枚举类的第一行声明枚举类对象。

枚举类和普通类的区别:

  1. 使用 enum 定义的枚举类默认继承了 java.lang.Enum 类
  2. 枚举类的构造器只能使用 private 访问控制符,默认的就是private 访问控制符
  3. 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾).
  4. 列出的实例系统会自动添加 public static final 修饰

JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定

枚举类的主要方法:
values()方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常。

和普通 Java 类一样,枚举类可以实现一个或多个接口

若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式, 则可以让每个枚举值分别来实现该方法

/*
 * 一、枚举类
 * 1.如何自定义枚举类
 * 2.如何使用enum关键字定义枚举类
 *     >常用的方法:values() valueOf(String name)
 *     >如何让枚举类实现接口:可以让不同的枚举类的对象调用被重写的抽象方法,执行的效果不同。(相当于让每个对象重写抽象方法)
 */
public class TestSeason1 {
    public static void main(String[] args) {
        Season1 spring = Season1.SPRING;
        System.out.println(spring);
        spring.show();
        System.out.println(spring.getSeasonName());

        System.out.println();
        //1.values()
        Season1[] seasons = Season1.values();
        for (int i = 0; i < seasons.length; i++) {
            System.out.println(seasons[i]);
        }
        //2.valueOf(String name):要求传入的形参name是枚举类对象的名字。
        //否则,报java.lang.IllegalArgumentException异常
        String str = "WINTER";
        Season1 sea = Season1.valueOf(str);
        System.out.println(sea);
        System.out.println();

        Thread.State[] states = Thread.State.values();
        for (int i = 0; i < states.length; i++) {
            System.out.println(states[i]);
        }

//        Season2.INS.sh();
        Season2 ins = Season2.INS;
    }
}

interface Info {
    void show();
}

//枚举类
enum Season1 implements Info {
    SPRING("spring", "春暖花开") {
        public void show() {
            System.out.println("春天在哪里?");
        }
    },
    SUMMER("summer", "夏日炎炎") {
        public void show() {
            System.out.println("生如夏花");
        }
    },
    AUTUMN("autumn", "秋高气爽") {
        public void show() {
            System.out.println("秋天是用来分手的季节");
        }
    },
    WINTER("winter", "白雪皑皑") {
        public void show() {
            System.out.println("冬天里的一把火");
        }
    };

    private final String seasonName;
    private final String seasonDesc;

    private Season1(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }

    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }

    @Override
    public String toString() {
        return "Season [seasonName=" + seasonName + ", seasonDesc="
                + seasonDesc + "]";
    }
//	public void show(){
//		System.out.println("这是一个季节");
//	}
}

enum Season2 {
    INS {
        public void show() {
            System.out.println("测试内部类方法,这个方法无效,无法调用");
        }
    };  
    //类中有这个方法,对象中的此方法才可用
 //	public void show(){
//		System.out.println("这是一个季节");
//	}  
}

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值