概述
一,概念
1.1)什么是单例模式:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责自己创建自己的对象,同时确保永远只有这一个对象被创建。这个类提供了一种外界访问其唯一对象的方式,外界可以直接访问自己创建好的自身对象 ,不能实例化该类的对象。
1.2)单例模式的意图:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
二,单例模式的特性:
- 1、单例就是该类只能返回⼀个实例,即,单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
三,单例代码的特点:
单例代码所具备的特点:
- 1.私有化的构造函数
- 2.私有的静态全局变量,最终指向自己创建的自身对象
- 3.供外界访问自己创建的自身对象的、公有的静态方法
四,关键代码:
- 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 构造函数是私有的。
五,代码实现——5种方式
首先,我们将创建一个 SingleObject 类。
SingleObject 类有它的私有构造函数和本身的一个静态全局实例。
SingleObject 类提供了一个静态方法,供外界获取它的这个静态全局实例。
5.1)懒汉式,线程不安全
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
特点:这种方式 lazy loading懒加载 很明显。
缺陷:在多线程环境下不能正常工作!懒汉式存在致命的问题:当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。
public class Singleton {
//私有的静态全局变量
private static Singleton instance;
//私有的构造方法
private Singleton (){}
//供外界访问获取类对象的函数:
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
自己编写:
public class SingleObject01 {
//私有的构造函数:
private SingleObject01() {
}
//提供外界访问的全局变量:
private static SingleObject01 singleObject01;
//供外界访问的方法:
public static SingleObject01 getInstance() {
if (singleObject01 == null) {
singleObject01= new SingleObject01();
}
return singleObject01;
}
/**
* 分析:
*
这种懒汉式的严格来讲,在多线程访问的情况下,都不是单例模式了,
*
因为如果有多个线程同时访问,那么得到的将是多个SingleObject01对象。
*/
}
5.1)懒汉式--加锁优化,线程安全
描述:这种方式具备很好的 lazy loading懒加载效果,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
缺点:虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法,会严重影响效率。
public class Singleton {
//私有的静态全局变量
private static Singleton instance;
//私有的构造函数
private Singleton (){}
//加锁之后的供外界访问获取类对象的方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
自己编写:
public class SingleObject02 {
//私有构造函数:
private SingleObject02() {
}
//私有的静态全局变量:
private static SingleObject02 singleObject02;
//供外界访问上边全局变量的方法:
public static synchronized SingleObject02 getInstance() {
if (singleObject02 == null) {
singleObject02 = new SingleObject02();
}
return singleObject02;
}
}
注意:
但是同步操作的代码其实只在第一次调用时才被需要用到,因为只有在第一次的时候才创建了单例实例对象时。
对于第二次、第三次等以后的调用,直接返回第一次的时候实例化好的对象即可。这就引出了双重检验锁。
5.2)双重检验锁
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双重检验锁机制,安全且在多线程情况下能保持高性能。
public class Singleton {
//私有的静态全局变量
private static Singleton singleton;
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的方法:
public static Singleton getSingleton() {
//双重检验锁:
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,就会报错。
我们只需要将 instance 变量声明成 volatile 就可以了。
有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。
public class Singleton {
//使用volatile关键字修饰的私有的静态全局变量
private volatile static Singleton singleton;
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的静态方法
public static Singleton getSingleton() {
//双重检验锁:
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
自己编写:
public class SingleObject04 {
//私有的构造函数:
private SingleObject04() {
}
//私有的静态全局变量:
private static volatile SingleObject04 singleObject04;
//供外界获取该类对象的方法:
public static SingleObject04 getInstance() {
if (singleObject04 == null) {
synchronized (SingleObject04.class) {
if (singleObject04 == null) {
singleObject04 = new SingleObject04();
}else{
return singleObject04;
}
}
}
return singleObject04;
}
}
5.3)饿汉式
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader机制避免了多线程的同步问题,不过,类对象在类加载时就实例化,虽然导致类加载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类加载,如果有其他的方式导致了类加载、从而初始化了 类对象instance ,显然没有达到 lazy loading懒加载的效果。
public class Singleton {
//私有的静态全局变量,指向类加载时就实例化好的类对象;
private static Singleton instance = new Singleton();
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的静态方法:
public static Singleton getInstance() {
return instance;
}
}
自己编写:
public class SingleObject03 {
//私有构造函数
private SingleObject03() {
}
//私有的静态全局变量,饿汉式--直接提前实例化好
private static SingleObject03 singleObject03 = new SingleObject03();
//供外界访问的方法:
public static SingleObject03 getInstance() {
return singleObject03;
}
}
·
5.4)静态内部类
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。这种方法也是《Effective Java》上所推荐的。
这种写法由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,保证了线程安全问题,没有性能缺陷;
示例代码:
public class Singleton {
//静态内部类:
private static class SingletonHolder {
//供外界访问的静态常量:
private static final Singleton INSTANCE = new Singleton();
}
//私有的构造函数:
private Singleton (){}
//供外界访问获取类对象的方法:
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
自己编写:
public class SingleObject05 {
//私有的构造函数:
private SingleObject05() {
}
//将静态全局变量放到静态内部类里边:
private static class SingleObject05Inner{
private static final SingleObject05 SINGLE_OBJECT_05=new SingleObject05();
}
//供外界访问的方法:
public static SingleObject05 getInstance() {
return SingleObject05Inner.SINGLE_OBJECT_05;
}
}
·
为什么静态内部类能够实现单例模式,且保证了线程安全?
原文链接:
静态内部类为何能够实现单例?_小猪快跑22的博客-CSDN博客_为什么静态内部类可以实现单例
【JVM】为什么静态内部类实现单例模式是线程安全?_cafe-BABE的博客-CSDN博客_静态内部类为什么是线程安全的
静态内部类的优点是:
外部类加载时并不需要立即加载内部类,内部类不被加载则不会去创建实例对象,故而不占内存。
具体来说当单例类第一次被加载时,并不需要去加载静态内部类,只有当getInstance()方法第一次被调用时,才会导致虚拟机加载静态内部类。
这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例对象的实例化。
为什么可以保证线程安全?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器中的<clinit>方法。
<clinit>方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。<clinit>方法它是由类里面 所有的静态变量的赋值动作 和 静态代码块 组成的。
JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。
然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>()方法。也就是说同一个加载器下,加载一个类的时候,只会执行一次<clinit>方法。
那么回到代码中,这里的静态变量的赋值操作实际上就是一个<clinit>方法中的代码,当我们执行getInstance方法的时候,才会导致静态内部类的加载(然后才会创建实例对象),
而类加载的最后一步才会执行类的初始化,而且即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以该类始终只会有一个实例对象。
那么再增加一句,之所以这里变量定义的时候不需要volatile,因为只有一个线程会执行具体的类的初始化代码<clinit>,也就是即使有指令重排序,因为根本没有第二个线程给你去影响,所以无所谓。
·
详情参见:
静态内部类的单例模式如何保证线程安全_Morning sunshine的博客-CSDN博客
·
5.5)枚举
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
·
六,建议
一般情况下,不建议使用第 1 种非线程安全的懒汉式单例和第 2 种线程安全的懒汉方式,建议使用第 3 种饿汉方式。
只有在明确必须要实现 lazy loading 懒加载效果时,才使用第四种双重检验锁方式第 5 种静态内部类方式。
一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。
PS:什么是 lazy loading 懒加载?
懒加载其实就是延时加载,即当对象需要用到的时候再去加载。
什么叫做需要用到的时候?比如说一个对象被创建出来就需要一笔内存开支,如果接下来就没有其他的操作,那可以认为这个对象创建得“过早”了。
好处:内存占用小。
最后
以上就是整齐小蝴蝶为你收集整理的单例模式_学习的全部内容,希望文章能够帮你解决单例模式_学习所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复