概述
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式的特点
在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在。
构造器必须是私有的,外部类无法通过调用构造器方法创建该实例。
没有公开的set方法,外部类无法调用set方法创建该实例。
提供一个公开的get方法获取唯一的这个实例。
单例模式的优点
1.某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
2.系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了。
3.避免了对资源的重复占用。
单例模式的缺点
1.单例模式没有抽象层,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
2.单例类的职责过重,在一定程度上违背了“单一职责原则”。
3.滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;又比如:在多个线程中操作单例类的成员时,但单例中并没有对该成员进行线程互斥处理。
单例模式的写法
1.饿汉式(两种)
package Singleton1;
/**
* @ClassName Mgr01
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:21
* @Version 1.0
*
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类加载时就完成实例化(但是你不用的话写它干嘛)
*
**/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01(){};
public void m() {
System.out.println("m");
}
public static Mgr01 getInstance()
{
return INSTANCE;
}
public static void main(String[] args) {
Mgr01.getInstance().m();
}
}
package Singleton1;
/**
* @ClassName Mgr02
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:26
* @Version 1.0
**/
public class Mgr02 {
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
public static Mgr02 getInstance()
{
return INSTANCE;
}
}
和第一种方式几乎没区别。
2.懒汉式(6种)
package Singleton1;
/**
* @ClassName Mgr03
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:27
* @Version 1.0
**/
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03(){};
public Mgr03 getInstance()
{
if(INSTANCE==null)
{
INSTANCE = new Mgr03();
}
return INSTANCE;
}
}
说明:此种方式线程不安全,当两个线程同时进入if判断INSTANCE是否为null时,因为INSTANCE还没被创建,所以会创建出多个不同的对象。
package Singleton1;
/**
* @ClassName mGR04
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:31
* @Version 1.0
**/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04(){};
public static synchronized Mgr04 getInstance()
{
if(INSTANCE==null)
{
INSTANCE = new Mgr04();
}
return INSTANCE;
}
}
说明:在此处由于我们采用static synchronized 锁住了当前Mgr04类(后面更新线程八锁问题),每个线程来都会锁住这个类,就会导致程序执行效率低下。
package Singleton1;
/**
* @ClassName Mgr05
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:33
* @Version 1.0
**/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05(){};
public Mgr05 getInstance()
{
if(INSTANCE == null)
{
synchronized (Mgr05.class)
{
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
}
说明:此处会产生懒汉式第一种写法的线程安全问题
package Singleton1;
/**
* @ClassName Mgr06
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:35
* @Version 1.0
**/
public class Mgr06 {
private volatile static Mgr06 INSTANCE;
private Mgr06(){};
public Mgr06 getInstance()
{
if(INSTANCE == null)
{
synchronized (Mgr06.class)
{
if(INSTANCE == null)
{
INSTANCE = new Mgr06();
return INSTANCE;
}
}
}
return INSTANCE;
}
}
双检锁的机制有效的解决了上面的问题,这也是以前最推荐的方式。
注意:对象前面记得用volatile关键字修饰。因为创建对象不是一个原子性操作,但是操作系统为了提高效率会进行指令重排序,会导致程序在判断时出现误判现象。加了volatile关键字后,其中一个作用就是禁止指令重排序。
概述:
A、B线程同时进入了第一个if判断
A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
由于JVM内部的优化机制,JVM先划出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
package Singleton1;
/**
* @ClassName Mgr07
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:39
* @Version 1.0
**/
public class Mgr07 {
private Mgr07(){};
private static class Holder{
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance()
{
return Holder.INSTANCE;
}
}
说明:
这个方法是一个很完美的方法,当jvm加载.class对象时,只会加载Mgr07.class,而不会加载内部类Holder。
当我们使用Holder时才会被jvm加载。这样就完美的解决了饿汉式的小毛病。做到了需要时才创建(懒加载)。
package Singleton1;
/**
* @ClassName Mgr08
* @Description
* @Author SD.LIU
* @Date 2021/7/7 22:42
* @Version 1.0
**/
public enum Mgr08 {
INSTANCE;
}
使用枚举来实现单实例控制会更加简洁,而且JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。因为枚举类型无法反序列化,在其他方式我们都可以通过.class对象进行反射(newIntance()),然后创建对象。但是枚举类型没有构造函数所以无法被创建对象。
总结
推荐采用饿汉式或者静态内部类的方式创建单例对象,如果有特殊需求可以采用双检锁的方式去创建单例对象。