首页 > 代码库 > 单例模式

单例模式

单例模式:确保一个类中只有一个实例,并提供一个全局访问点。

1. 常见的单例模式使用场景:

1.1 Servlet

  一个WEB容器可以存在多个Servlet,但是别忘记了,在WEB容器中,每个Servlet都仅仅只有一个唯一实例。在Servlet的生命周期中,当WEB容器启动的时候会根据web.xml中对Servlet的配置决定是否初始化这个Servlet单一实例,如果配置的<load-on-startup>属性不是负数,则会在启动的时候就调用Servlet的构造函数同时执行init()生命周期函数。

技术分享

1.2 Spring

  Spring中的bean配置有一个scope属性,在没有明确指定的情况下,scope是singleton,即表示当前bean在Spring容器中仅仅会创建一个唯一的实例。

技术分享

2. 如何创建一个单例类

2.1 一个单例类需要具备的3个基本条件:①一个私有的自身类成员变量;②一个私有的构造函数;③一个提供外部访问实例创建函数,由于单例模式的场景在创建的时候不存在当前类对象,所以这个方法应该是static方法。

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

2.2 单例模式在多线程场景下的多种实现

2.2.1 上述Singleton类在实际项目的开发中,由于存在多线程并发的现实条件,并不能保证Singleton在整个项目中仅存在唯一实例,而是每个线程一个唯一实例,因此必须考虑如果确保多线程下,所有的线程使用的都是同一个实例。

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

  上面的Singleton2类对生产实例的getInstance()函数做了同步的处理,但是再同步的情况下,性能会比非同步的情况下低个百倍甚至千倍,因此考虑是否能不采取同步的方式确保单例类能够仅产生一个唯一实例。

2.2.2 当类加载的时候,会将类内部的static成员先执行,因此可以将上述代码作如下处理:

public class Singleton3 {
    
    private static Singleton3 singleton = new Singleton3();
    
    private Singleton3(){}
    
    public static Singleton3 getInstance(){
        return singleton;
    }
}

  当Singleton3类加载到项目中,就会自动创建一个Singleton3实例,以后访问getInstance()方法只要放回当前实例就好了。

2.2.3 上面的方式在类加载的时刻就创建了单一实例,有时候并不需要在类加载的时候就创建这个单一实例,但是又需要回避多线程环境下可能出现的多个实例的问题,最后还是不可避免使用同步的方式,那么该怎么才将同步造成的效率影响减少到最低呢,参考以下类:

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

  Singleton4这个类还是使用了同步处理了多线程环境下可能产生多实例的问题,不同于Singleton2的情况是,它先判断了当前实例是否存在,如果不存在,先加锁,确保同一时刻只有一个线程进入同步块,然后再次判断是否有singleton实例存在(之所以在同步块里面还要在判断一次,是因为如果Singleton在同一个时刻有两个线程都通过了第一个singleton==null的判断,第一个线程先获取了锁,创建了singleton实例,之后另一个线程仍然会进入同步块,这个时候就需要在进行一次singleton==null的判断,确保第二个线程不会再次创建一个singleton实例)。

 3. 单利模式的线程安全问题

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadSafeServlet extends HttpServlet {

    public static String name = "Hello";   //静态变量,可能发生线程安全问题
    int i;  //实例变量,可能发生线程安全问题
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    @Override
    public void init() throws ServletException {
        super.init();
        System.out.println("Servlet初始化");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.printf("%s:%s[%s]\n", Thread.currentThread().getName(), i, format.format(new Date()));
        i++;
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s:%s[%s]\n", Thread.currentThread().getName(), i, format.format(new Date()));
        resp.getWriter().println("<html><body><h1>" + i + "</h1></body></html>");
    }
}

  当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求对应的Servlet对象。如果该Servlet还未被请求过,那么将进行Servlet初始化并调用Servlet并调用service()方法。否则,直接调用service()方法。要注意的是每一个Servlet对象再Tomcat容器中只有一个实例对象,即是单例模式。如果多个HTTP请求请求的是同一个Servlet,那么着两个HTTP请求对应的线程将并发调用Servlet的service()方法。这时候,如果在Servlet中定义了实例变量或静态变量,那么可能会发生线程安全问题(因为所有的线程都可能使用这些变量)。

单例模式