4.1单例设计模式
单例模式(Singleton Patttern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种设计模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式。可以直接访问,不需要实例化该类的对象。
4.1.1单例模式的结构
单例模式的主要有以下角色:
- 单例类。只能创建一个实例的类
- 访问类。使用单例类
4.1.2单例模式的实现
单例设计模式分类两种:
饿汉式:类加载就会导致该单例对象被创建
懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象时才会创建
1.饿汉式-方式1【静态变量方式】
创建Singleton 类:
package com.kiki.pattern.singleton.demo1;
/**
* 饿汉式:静态成员变量的方式
*
* @author kiki
* @since 2020/12/29 21:03
*/
public class Singleton {
//1.私有构造方法,因为你私有了构造方法,外界就访问不到这个构造方法,访问不到构造方法就无法创建对象。
private Singleton(){}
//2.在本类中创建本类对象【外界访问不到构造方法,那么我们自己在该类中创建对象供外界使用】
private static Singleton instance = new Singleton();
//3.提供一个公共的访问方式,让外界获取该对象
/**
* 说明:此处为什么用static修饰,因为外界无法创建Singleton类的对象,也就不能访问该类的非静态方法
* 所以这里我们提供的是静态方法
*/
public static Singleton getInstance(){
return instance;
}
}
创建测试类Client :
package com.kiki.pattern.singleton.demo1;
/**
* 饿汉式测试类
*
* @author kiki
* @since 2020/12/29 21:11
*/
public class Client {
public static void main(String[] args) {
//1.创建Singleton类的对象
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//2.验证获取到的这两个是否是同一个对象
System.out.println(instance1 == instance2);//true
}
}
2.饿汉式-方式2【静态代码块方式】
创建Singleton 类:
package com.kiki.pattern.singleton.demo1;
/**
* 饿汉式:静态成员变量的方式
*
* @author kiki
* @since 2020/12/29 21:03
*/
public class Singleton {
//1.私有构造方法,因为你私有了构造方法,外界就访问不到这个构造方法,访问不到构造方法就无法创建对象。
private Singleton(){}
//2.在本类中创建本类对象【外界访问不到构造方法,那么我们自己在该类中创建对象供外界使用】
private static Singleton instance = new Singleton();
//3.提供一个公共的访问方式,让外界获取该对象
/**
* 说明:此处为什么用static修饰,因为外界无法创建Singleton类的对象,也就不能访问该类的非静态方法
* 所以这里我们提供的是静态方法
*/
public static Singleton getInstance(){
return instance;
}
}
Client 测试类:
package com.kiki.pattern.singleton.demo2;
/**
* @author kiki
* @since 2020/12/29 21:32
* 饿汉式测试类【静态代码块方式】
*/
public class Client {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断两次获取到的Singleton对象是否是同一个对象
System.out.println(instance1 == instance2);//true
}
}
说明:
该方式在成员变量位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
2.饿汉式-方式1【线程不安全】
创建Singleton类:
package com.kiki.pattern.singleton.demo3;
/**
* 懒汉式:线程不安全的方式
*
* @author kiki
* @since 2020/12/29 21:39
*/
public class Singleton {
//1.私有构造方法
private Singleton(){}
//2.声明Singleton类型的变量instance
private static Singleton instance;//此处只是声明了该类型的变量,并没有进行赋值
//3.对外提供访问方式
public static Singleton getInstance(){
/**
* 判断instance是否为null:
* 如果为null,说明还没有创建Singleton类的对象
* 如果没有,创建一个并返回,如果有,则直接返回
*/
if (instance == null){
instance = new Singleton();
}
//instance = new Singleton();//每调用一次方法都会重新创建一个对象,并把该对象返回
return instance;
}
}
创建Client类
package com.kiki.pattern.singleton.demo3;
/**
* @author kiki
* @since 2020/12/29 21:43
*/
public class Client {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断两次获取到的Singleton对象是否是同一个对象
System.out.println(instance1 == instance2);//true
}
}
但是以上代码在多线程情况下会有问题:
package com.kiki.pattern.singleton.demo3;
/**
* 懒汉式:线程不安全的方式
*
* @author kiki
* @since 2020/12/29 21:39
*/
public class Singleton {
//1.私有构造方法
private Singleton(){}
//2.声明Singleton类型的变量instance
private static Singleton instance;//此处只是声明了该类型的变量,并没有进行赋值
//3.对外提供访问方式
public static Singleton getInstance(){
/**
* 判断instance是否为null:
* 如果为null,说明还没有创建Singleton类的对象
* 如果没有,创建一个并返回,如果有,则直接返回
*/
if (instance == null){
/**
* 多线程情况下:线程1等待,线程2拿到cpu的执行权,也会进入到该判断,此时创建的对象就不是单例的了
*/
instance = new Singleton();
}
//instance = new Singleton();//每调用一次方法都会重新创建一个对象,并把该对象返回
return instance;
}
}
但是此种情况如何解决呢?给静态方法加锁,synchronized ,此时就是线程安全的
package com.kiki.pattern.singleton.demo3;
/**
* 懒汉式:线程不安全的方式
*
* @author kiki
* @since 2020/12/29 21:39
*/
public class Singleton {
//1.私有构造方法
private Singleton(){}
//2.声明Singleton类型的变量instance
private static Singleton instance;//此处只是声明了该类型的变量,并没有进行赋值
//3.对外提供访问方式
public static synchronized Singleton getInstance(){
/**
* 判断instance是否为null:
* 如果为null,说明还没有创建Singleton类的对象
* 如果没有,创建一个并返回,如果有,则直接返回
*/
if (instance == null){
/**
* 多线程情况下:线程1等待,线程2拿到cpu的执行权,也会进入到该判断,此时创建的对象就不是单例的了
*/
instance = new Singleton();
}
//instance = new Singleton();//每调用一次方法都会重新创建一个对象,并把该对象返回
return instance;
}
}
说明:
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别的低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
5.懒汉式-方式3【双重检查锁】
再来讨论一下懒汉模式中加锁的问题,对于getInstance()
方法来说,绝大部分操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
package com.kiki.pattern.singleton.demo4;
/**
* 双重检查锁方式
*
* @author kiki
* @since 2021/1/7 20:15
*/
public class Singleton {
//1.私有构造方法
private Singleton(){}
//2.声明Singleton类型的变量,但是并没有直接进行赋值,所以依然是懒汉式
private static Singleton instance;
//3.对外提供公共的访问方式
public static Singleton getInstance(){
//3.1第一次判断,如果instance的值不为null,不需要抢占锁,就可以直接返回对象
if (instance == null){
synchronized (Singleton.class){
//3.2第二次判断,抢占到锁之后再次进行判断是否为null
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序【有兴趣可以看并发编程】 操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile
关键字,volatile
关键字可以保证可见性和有序性。
以下是双重检查锁模式的标准代码:
package com.kiki.pattern.singleton.demo4;
/**
* 双重检查锁方式
*
* @author kiki
* @since 2021/1/7 20:15
*/
public class Singleton {
//1.私有构造方法
private Singleton(){}
//2.声明Singleton类型的变量,但是并没有直接进行赋值,所以依然是懒汉式
//volatile 保证指令是有序的
private static volatile Singleton instance;
//3.对外提供公共的访问方式
public static Singleton getInstance(){
//3.1第一次判断,如果instance的值不为null,不需要抢占锁,就可以直接返回对象
if (instance == null){
synchronized (Singleton.class){
//3.2第二次判断,抢占到锁之后再次进行判断是否为null
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
小结:
添加volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程情况下线程安全,也不会有性能问题。
6.懒汉式-方式4(静态内部类方式)
静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有静态内部类的属性或方法在被调用时才会被加载,并初始化其静态属性。静态属性由于被static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
package com.kiki.pattern.singleton.demo5;
/**
* 静态内部类方式实现单例模式
*
* @author kiki
* @since 2021/1/7 21:32
*/
public class Singleton {
//私有构造方法
private Singleton(){}
//定义一个静态内部类
private static class SingletonHolder{
//在内部类中声明并初始化外部类的对象,为了防止外界对其进行修改,加上final关键字
//当然被final修饰之后就成了常量,自然要大写
private static final Singleton INSTANCE = new Singleton();
}
//提供公共的访问方式,供外界获取Singleton对象
public static Singleton getSingleton(){
return SingletonHolder.INSTANCE;
}
}
说明:
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性。
小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
7.枚举方式
枚举实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
package com.kiki.pattern.singleton.demo6;
/**
* 枚举实现方式
*
* @author kiki
* @since 2021/1/7 22:29
*/
public enum Singleton {
INSTANCE;
}
package com.kiki.pattern.singleton.demo6;
/**
* 枚举实现方式测试类
*
* @author kiki
* @since 2021/1/7 22:30
*/
public class Client {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println(instance == instance1);//true
}
}
说明:
枚举方式是属于饿汉式方式,所以在不考虑浪费内存空间的话,首选枚举方式
4.1.3 存在问题
4.1.3.1 问题演示
破坏单例模式
使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
- 序列化反序列化
Singleton类: