单例模式
本文最后更新于 2025-03-23,文章超过7天没更新,应该是已完结了~
什么是单例模式?
单例模式(Singleton Pattern)是最简单的创建型设计模式。它会确保一个类只有一个实例存在。
单例模式最重要的特点就是构造函数私有,从而避免外界直接使用构造函数直接实例化该类的对象。
单例模式在Java种通常有两种表现形式: - 饿汉式:类加载时就进行对象实例化 - 懒汉式:第一次引用类时才进行对象实例化
在一个对象需要频繁的销毁、创建,而销毁、创建性能又无法优化时,单例模式的优势尤其明显 - 在一个对象的产生需要比较多资源时,如读取配置、产生其他依赖对象时,则可以通过在启用时直接产生一个单例对象,然后用永久驻留内存的方式来解决 - 单例模式可以避免对资源的多重占用,因为只有一个实例,避免了对一个共享资源的并发操作 - 单例模式可以在系统设置全局的访问点,优化和共享资源访问
1.饿汉式
public class SingletonTest01 {
public static void main(String[] args) {
Singleton instance1=Singleton.getInstance();
Singleton instance2=Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
//饿汉式(静态变量)
class Singleton{
//构造函数定义成private,禁止外部创建Singleton实例
private Singleton(){}
//1.本类内部创建对象实例
private static final Singleton instance=new Singleton();
//2.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return instance;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题;
缺点:在类装载的时候就完成实例化,没有达到懒加载的效果,如果从始至终从未使用过这个实例,则会造成内存的浪费
2.懒汉式(线程不安全)
public class SingletonTest03 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Singleton{
private static Singleton instance = null;
private Singleton(){}
//提供一个静态的公有方法,当使用到该方法时,才会创建instance
//懒汉式写法,线程不安全
public static Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
起到了懒加载的效果,但是只能在单线程下使用
4.懒汉式(线程安全)
public class SingletonTest04 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Singleton{
private static Singleton instance;
private Singleton(){}
//懒汉式线程安全写法
public static synchronized Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
解决了线程安全问题;
效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
接下来介绍推荐使用的方法
5.双重检查
public class SingletonTest05 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
//双重检查 适合多线程+有懒加载机制
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
当还没有实例 且 多线程的情况下 都进来第一层if判断-->通过, 然后第一个抢占到锁的线程经过第二层if判断-->通过,然后创建了实例 ;第二个线程经过第二次if判断-->发现有实例--->退出,以后任何线程都直接返回第一次创建的实例
7.枚举(最推荐)
public class SingletonTest07 {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
}
}
enum Singleton{
INSTANCE;
Singleton(){
}
}
这借助了JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步问题,代码简洁,而且还能防止反序列化重新创建新的对象
补充:什么是枚举?
我们学习过单例模式,即一个类只有一个实例。而枚举其实就是多例,一个类有多个实例,但实例的个数不是无穷的,是有限个数的。我们称呼枚举类中实例为枚举项。一般一个枚举类的枚举项的个数不应该太多,如果一个枚举类有30个枚举项就太多了!
2.定义枚举类型
定义枚举类型需要使用enum关键字,例如:
public enum Direction {
FRONT, BEHIND, LEFT, RIGHT;
}
Direction d = Direction.FRONT;
注意,定义枚举类的关键字是enum,而不是Enum,所有关键字都是小写的!
其中FRONT、BEHIND、LEFT、RIGHT都是枚举项,它们都是本类的实例,本类一共就只有四个实例对象。
在定义枚举项时,多个枚举项之间使用逗号分隔,最后一个枚举项后需要给出分号。不能使用new来创建枚举类的对象,因为枚举类中的实例就是类中的枚举项,所以在类外只能使用类名.枚举项。
3.枚举与switch
枚举类型可以在switch中使用
Direction d = Direction.FRONT;
switch(d) {
case FRONT: System.out.println("前面");break;
case BEHIND:System.out.println("后面");break;
case LEFT: System.out.println("左面");break;
case RIGHT: System.out.println("右面");break;
default:System.out.println("错误的方向");
}
Direction d1 = d;
System.out.println(d1);
注意,在switch中,不能使用枚举类名称,例如:“case Direction.FRONT:”这是错误的,因为编译器会根据switch中d的类型来判定每个枚举类型,在case中必须直接给出与d相同类型的枚举选项,而不能再有类型。
4.所有枚举类都是Enum的子类
所有枚举类都默认是Enum类的子类,无需我们使用extends来继承。这说明Enum中的方法所有枚举类都拥有。
5.枚举类的构造器
枚举类也可以有构造器,构造器默认都是private修饰,而且只能是private。因为枚举类的实例不能让外界来创建!
enum Direction {
FRONT, BEHIND, LEFT, RIGHT;
Direction()//枚举类的构造器不可以添加访问修饰符,枚举类的构造器默认是private的。但你自己不能添加private来修饰构造器
{
System.out.println("hello");
}
}
其实创建枚举项就等同于调用本类的无参构造器,所以FRONT、BEHIND、LEFT、RIGHT四个枚举项等同于调用了四次无参构造器,所以你会看到四个hello输出。
6.其实枚举类和正常的类一样,可以有实例变量,实例方法,静态方法等等
为什么说枚举可以解决线程安全问题?
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后:
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
线程安全原理同<clinit>方法加载机制。
那么枚举类的防止反序列化创建实例是为什么?
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。
- 感谢你赐予我前进的力量