单例设计模式介绍
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够,这是就会使用到单例模式
单例模式有八种方式:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程安全,同步代码块)
- 双重检查
- 静态内部类
- 枚举
1.饿汉式(静态常量)应用实例
- 构造器私有化 (防止 new )
- 类的内部创建对象
- 向外暴露一个静态的公共方法。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;
}
}
优缺点说明:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果
- 结论:这种单例模式可用,可能造成内存浪费
2. 饿汉式(静态代码块)
//饿汉式(静态变量)
class Singleton {
//1. 构造器私有化, 外部能new
private Singleton() {
}
//2.本类内部创建对象实例
private static Singleton instance;
//代码块不能被调用自动执行,用来初始化类或对象
//静态代码快随着类的加载而执行(不是加载是执行),并且只执行一次
//非静态代码块随着对象的创建而执行,每创建一个对象执行一次、
//静态方法随类的加载而加载,不执行
//静态属性会随着类的加载而初始化(赋值)
//在静态代码块中,创建单例对象
static {
instance = new Singleton();
}
//3. 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优缺点说明:
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
- 结论:这种单例模式可用,但是可能造成内存浪费
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;
}
}
优缺点说明:
- 起到了Lazy Loading的效果,但是只能在单线程下使用。
- 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
- 结论:在实际开发中,不要使用这种方式
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;
}
}
优缺点说明:
- 解决了线程不安全问题
- 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
- 结论:在实际开发中,不推荐使用这种方式
5. 懒汉式(线程安全,同步代码块)
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
优缺点说明:
- 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
- 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
- 结论:在实际开发中,不能使用这种方式
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;
}
}
优缺点说明:
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
- 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.
- 线程安全;延迟加载;效率较高
- 结论:在实际开发中,推荐使用这种单例设计模式
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;
}
}
优缺点说明:
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用.
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~");
}
}
优缺点说明:
- 这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
- 这种方式是Effective Java作者Josh Bloch 提倡的方式
- 结论:推荐使用
单例模式注意事项和细节说明
- 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
- 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
- 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)
JDK中,java.lang.Runtime就是经典的单例模式(饿汉式)
补充:
单例模式的优点:
由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
应用场景
- 网站的计数器,一般也是单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
- Application 也是单例的典型应用
- Windows的Task Manager (任务管理器)就是很典型的单例模式
- 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枚举类
必须在枚举类的第一行声明枚举类对象。
枚举类和普通类的区别:
- 使用 enum 定义的枚举类默认继承了 java.lang.Enum 类
- 枚举类的构造器只能使用 private 访问控制符,默认的就是private 访问控制符
- 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾).
- 列出的实例系统会自动添加 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("这是一个季节");
// }
}