首页 > 编程语言 >Java单例模式,看这一篇就够了

Java单例模式,看这一篇就够了

时间:2022-11-03 16:49:02浏览次数:52  
标签:Singleton Java getInstance 对象 就够 instance 单例 public

在创建型设计模式中,我们第一个学习的是单例模式(Singleton Pattern),这是设计模式中最简单的模式之一。

 

单例是什么意思呢?

 

单例就是单实例的意思,即在系统全局,一个类只创建一个对象,并且在系统全局都可以访问这个对象而不用重新创建。

 

一、单例模式的基本写法

 

单例模式示例代码:

 

public class Singleton {
 
  //	Singleton类自己持有这个单例对象
   private static Singleton instance = new Singleton();
 
   //	构造方法设置为私有,避免在Singleton类外部创建Singleton对象
   private Singleton() {}
 
   //	提供获取单例对象的静态方法
   public static Singleton getInstance() {
      return instance;
   }
 
   public void hello() {
      System.out.println("Hello!");
   }
}

 

使用:

 

Singleton obj = Singleton.getInstance();
obj.hello();

 

分析SingleObject类的特征:

 

  1. SingleObject类的构造方法是私有的,这样可以保证只能在SingleObject类内部才能创建对象,而无法在类外部创建SingleObject对象。
  2. SingleObject类中有一个instance成员属性,它用来持有这个SingleObject对象。
  3. SingleObject类提供了一个静态方法getInstance,它可以让我们在任何可以访问到SingleObject类的地方,都可以使用SingleObject.getInstance()来获取到这个SingleObject对象。

 

二、单例模式的作用

 

单例模式有什么用呢?

 

1. 控制对象的数量

 

当你编写了一个类提供给其他人调用时,对方看到是一个类,很有可能第一反应是尝试new一下。

 

你自己编写的类你自己是清楚如何使用的,在整个系统内这个类只需要创建一个对象就够了,但对方可能并不清楚。

 

这时候你可以把这个类编写为单例形式,把构造方法私有化,让对方无法通过new来创建对象,只能使用getInstance来获取。

 

这个模式可以帮助你有效的控制对象的数量,毕竟,有的类其内部实现复杂,如果频繁创建销毁对象,可能还是很耗费服务器资源的。

 

2.全局访问

 

单例模式的特点是单例类自己持有这个单例对象,并且提供一个静态方法可在全局获取到这个单例对象。

 

如果没有单例模式的情况下,我们一般是在代码A处创建这个对象,在代码B处如果也要使用这个对象,就需要将这个对象进行参数传递。为了避免传来传去,我们可能会写个Holder类,把这个对象放在Holder的成员变量中。

 

而单例模式的这个优点是,我们可以避免这样的困扰,直接从单例类中获取。

 

三、单例模式的变种

 

上面介绍的是单例模式的一种基本写法,实际我们还可以对其进行优化和变种。

 

1. 饿汉式

 

基本写法中,对象的创建是直接写在Singleton类的成员属性上的,因此当Singleton类被加载时,就会立即创建Singleton对象,这个写法比较简单,但我们可能并不会马上使用到这个Singleton对象,过早的创建会造成内存资源浪费。

 

这种一加载类就急于创建对象的写法,我们称之为饿汉式

 

如果对内存资源不在意,那么其实饿汉式这个写法也就没什么大的缺点,而且写起来还简单,还是可以用的。

 

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

 

此变种仅是介绍,不要使用。

 

既然饿汉式在类加载时就创建对象会造成内存浪费,那么我们把创建对象这个步骤挪到要用时再创建不就好了?

 

我们要使用对象时,都是通过getInstance方法先获取对象,我们可以在getInstance方法中完成对象创建。

 

这种需要时再创建的写法,我们称之为懒汉式

 

示例代码:

 

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

 

分析懒汉式(线程不安全)写法的特点:

 

  1. 创建对象的时机修改为了在getInstance内部,需要时再创建,这可以节约系统资源
  2. getInstance方法在多个线程并发调用时,有可能会出现创建了多个实例,所以这算是一个不好的单例变种示范

 

饿汉式没有多线程并发问题吗?

 

确实没有,因为饿汉式是在类加载时进行创建对象,类加载classloader是单线程的,不存在这个问题。

 

3. 懒汉式(线程安全)

 

此变种仅是介绍,不要使用。

 

懒汉式(线程不安全)有可能存在并发问题,导致创建多个实例,那么我们给他加上锁不就好了吗?

 

示例代码:

 

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

 

分析懒汉式写法的特点:

 

由于调用getInstance时如果instance为null会创建对象,如果多个线程同时调用getInstance方法,有可能出现同步问题导致创建多个实例,所以getInstance方法使用了synchronized加锁来保障并发情况下也只会创建一个实例,不过synchronized的粒度较大,如果每次请求都经过getInstance方法,性能影响较大。

 

4. 双检锁/双重校验锁(DCL,double-checked locking)

 

懒汉式(线程安全)已经可以达到节省资源的目的,也达到了线程安全的目的,但是使用synchronized加锁对性能有较大影响,双检锁的方式,则是把锁的粒度尽可能降低,减少加锁对性能的影响。

 

示例代码:

 

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

 

分析双检锁的写法:

 

  1. 在成员属性instance上,我们增加了volatile关键字,保障多线程对instance值的可见性以及禁止指令重排。
  2. 通过双重检查的方式,在内部再进行synchronized加锁,可以降低锁的粒度,有效避免每次调用getInstance都加锁,因为getInstance在创建对象之后,instance一直都是非null的。

 

双检锁这个方式,既可以保障不浪费资源,又可以保障在多线程的环境下保持高性能。

 

如果大家自行编写单例类,追求节约资源和高性能,可以使用这种写法,但据《Java并发编程实践》提到不赞成这个写法,推荐静态内部类的方式(这一点我尚未验证)。

 

5. 静态内部类

 

这个变种,可以达到和双检锁一样的效果,并且写起来更加简单,推荐使用。

 

public class Singleton {  
  
    private static class SingletonHolder {  
  	  private static final Singleton INSTANCE = new Singleton();  
    }  
  
    private Singleton () {}  
  
    public static final Singleton getInstance() {  
  	  return SingletonHolder.INSTANCE;  
    }  
}

 

分析一下静态内部类的特点:

 

将instance放在了内部类SingletonHolder中,前面我们提到饿汉式是类加载时就会立即创建对象,而静态内部类不会,它只会在调用了getInstance时,才会加载内部类SingletonHolder,此时才会创建对象。

 

6. 枚举

 

这个方式,这里仅是从网上摘抄,据说是很好,但是没有试过,工作中也很少见。

 

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。

 

它更简洁,自动支持序列化机制,绝对防止多次实例化。

 

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

 

不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

 

不能通过 reflection attack 来调用私有构造方法。

 

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

 

7. 登记式

 

如果熟悉我们封装的工具包Toolbox,就会知道工具包内提供了一个登记式单例工具类Singleton。

 

单例模式是一种非常常用的设计模式,但以上介绍的各种方法,都需要为每个单例类编写一些模板式的代码,为了简化,我们可以使用Singleton工具类。

 

//    获取单例对象
//    Student类必须要具备无参构造方法
//    每个类在一个进程中只能获得一个单例对象
Student student = Singleton.get(Student.class);

//    移除单例对象
Singleton.remove(Student.class);

//    清空所有单例对象
Singleton.clear();

//    单例对象数量
int size = Singleton.size();

 

其实他就是很像是spring容器。

 

Singleton.java:

 

/**
 * 单例工具
 * @author Unicorn
 */
public final class Singleton {

    /**
     * 对象池
     */
    private static Map<String, Object> pool = new ConcurrentHashMap();

    private Singleton() {}

    public static <T> T get(Class<T> clazz) {
        Assert.notNull(clazz);
        String key = clazz.getName();
        T obj = (T) pool.get(key);
        if (null == obj) {
            synchronized(Singleton.class) {
                obj = (T) pool.get(key);
                if (null == obj) {
                    obj = ReflectUtil.newInstance(clazz);
                    pool.put(key, obj);
                }
            }
        }
        return obj;
    }

    /**
     * 移除对象
     * @param clazz
     */
    public static void remove(Class clazz) {
        if (null != clazz) {
            String key = clazz.getName();
            pool.remove(key);
        }
    }

    /**
     * 销毁,清空对象池
     */
    public static void clear() {
        pool.clear();
    }

    public static int size() {
        return pool.size();
    }
}

 

8. Spring容器

 

spring容器核心机制是IoC和DI,其本身也提供了单例对象的支持。

标签:Singleton,Java,getInstance,对象,就够,instance,单例,public
From: https://www.cnblogs.com/ladderx/p/16854962.html

相关文章

  • Java获取小数点后几位
    学习中遇见保留1小数点后位,选择有四种方法:方法:1、用format方法,语法“String.format("%.2f",数值)”;2、用DecimalFormat的format方法;3、用setScale方法进行四舍五入;4、用set......
  • java时间类型转换方法
    java时间类型转换方法1.Calendar转LocalDateTime/***Calendar转LocalDateTime*@paramcalendar*@return*/publicstaticLoca......
  • Java String常用API总结
    Stringname;用于字符串拼接StringBuildersb=newStringBuilder();获取字符串长度name.length());指定字符在此字符串中第一次出现的索引name.indexOf('z'));nam......
  • 关于java.lang.IllegalArgumentException: Unknown URL content://com.example.databa
    在学习《Android第一行代码》第八章的ProviderTest项目的时候,运行之后点击“AddToBook”按钮,出现如下问题:java.lang.IllegalArgumentException:UnknownURLcontent://......
  • Java计算文件或文件夹大小
    代码:1/**2*路人甲操作文件的工具类3*returnSizi:计算文件大小4*/5classFilesUtil{67privatestaticlongcountSize;//用于储存文件大小......
  • Java函数式编程(1):Lambda表达式(1)
    您好,我是湘王,这是我的51CTO博客,欢迎您来,欢迎您再来~Java在其技术发展周期中经历过几次比较重要的变化,如果不是这几次比较重要的变化,恐怕不会有现在这样的江湖地位。个人看来,......
  • 从柯里化讲起,一网打尽 JavaScript 重要的高阶函数
    前情回顾我们在前篇​​《✨从历史讲起,JavaScript基因里写着函数式编程》​​讲到了JavaScript的函数式基因最早可追溯到1930年的lambda运算,这个时间比第一台计算......
  • [Java基础]-- 接口、抽象类
    一直都在使用接口,没怎么用过抽象类,今天面试遇到了面试官提问:抽象类和接口有什么区别啊?下面就关于这个问题好好研究一下,希望在以后的工作中能牢记。。以下是《疯狂java讲义》......
  • [Java应用]-- 拼接多张图片
    实现代码如下importjava.awt.image.BufferedImage;importjava.io.File;importjavax.imageio.ImageIO;/****@类功能说明:java拼接多张图片,生成的格式是jpg、bmp......
  • [Java--常见排序算法]------冒泡、选择、快速排序
    java常用的排序算法(冒泡、选择、快速等)一、冒泡排序法(效率最低)直接在代码中说明,他们可以直接在程序中运行//冒泡排序@TestpublicvoidtestBublle(){/***冒泡排序的基本......