什么是单例模式:
众所周知,java中的对象是new出来的,如果没有对象,就new 出来一个,每一个new出来的对象都是这个java类的实例,如果不加以限制,每一个java类都可以new出无数个对象
这些new出来的对象并不是平白无故就产生的,在创建的过程中,需要占用内存,占用资源等等。
对于普通对象而言,多一个少一个并没有区别。但是对于一些占用有限资源的对象,如线程池,缓存等,如果new出来的相关的对象多了,很快就把资源分配完,如果再新的请求new对应对象,由于已经没有资源,会导致系统异常。
为了有效的利用有限的资源,单例模式出现了。顾名思义,在系统中,每个java类只会创建一个实例对象,实例化的过程可以放在系统启动的时候,也可以放在第一次调用的时候。当实例化完成后,无论调用多少次,返回的都是同一个对象。
单例模式的作用:
单例模式主要用到全局只需要一个实例的对象,比如项目的配置对象,如线程池,数据库连接池,缓存配置等等。
现在的spring默认使用的就是单例,每一个配置的bean在spring容器中只有一个。
单例模式的要点:
- 要点一: 构造私有化
构造私有化,可以保证实例对象无法通过new的方式创建。 - 要点二: 提供一个静态方法获取对象
构造私有化之后,无法通过new的方式创建对象,但是这样我们也没有办法获得到对象了,这时就需要提供一个 对外暴露的静态方法,获取对象只能通过调用这个静态方法。 - 要点三 :私有化静态变量用来保存实例对象
如果实现单例模式:
好的,根据上面的描述(构造私有话,提供对外暴露获取对象的方法),我们可以创建一个最简单的单例对象
饿汉式
public class SingleTestDemo{
//1. 私有化静态全局变量
private static SingleTestDemo singleTestDemo = new SingleTestDemo();
// 2. 构造私有化
private SingleTestDemo(){
}
// 3. 提供对外暴露获取实例变量的静态方法
public static SingleTestDemo getInstance(){
return singleTestDemo;
}
}
如上所示,确实我们实现了一个满足条件的单例,满足了私有化全局变量,私有化构造,提供唯一的静态方法返回实例。
但是上面的方式有个特点,单例是在项目启动的时候就创建了(饿汉式),如果这个对象很耗费资源和时间,但是创建好后又很久不使用,就造成了浪费。基于这个想法,让我们改进一下,如下:
懒汉式
public class SingleTestDemo{
// 1. 私有化静态全局变量
private static SingleTestDemo singleTestDemo = null;
// 2. 构造私有化
private SingleTestDemo(){
}
// 3. 提供对外暴露获取实例变量的静态方法
public static SingleTestDemo getInstance(){
if (null == singleTestDemo) {
singleTestDemo = new SingleTestDemo();
}
return singleTestDemo;
}
}
相比较饿汉式的方式,在项目启动的时候,不会创建对象,而是在第一次调用静态方法的时候才会创建,这样就解决了在项目启动的时候就创建对象可能造成的浪费的问题。
如果我们的项目一直是单线程的话,这样做当然没有问题,但是实际上我们项目大都是多线程的情况,这就可能出现问题,当第一个线程运行到 if (null == singleTestDemo)
这一步后,但是又没有调用 singleTestDemo = new SingleTestDemo();
,另一个线程也执行到 if (null == singleTestDemo)
这一步,会发现此时单例对象还没有创建,然后进入if块内,调用 singleTestDemo = new SingleTestDemo();
,这样就创建了两个实例对象了。
提到了多线程安全问题,我们第一个想到的方式是什么?
Synchronized
同步关键字
对,我们可以使用同步关键字保证线程的安全性。
于是,我们把Synchronized
同步关键字放到对外暴露的静态方法上,如下
同步方法版懒汉式单例模式
public class SingleTestDemo{
// 1. 私有化静态全局变量
private static SingleTestDemo singleTestDemo = null;
// 2. 构造私有化
private SingleTestDemo(){
}
// 3. 提供对外暴露获取实例变量的静态方法
synchronized public static SingleTestDemo getInstance(){
if (null == singleTestDemo) {
singleTestDemo = new SingleTestDemo();
}
return singleTestDemo;
}
}
好了,我们把Synchronized
放到方法上,解决了多线程下的线程安全问题。
但是同时我们也要意识掉一个问题,方法上使用同步关键字,相当于对整个方法加锁了,必须等上一个线程执行完方法中所有的代码后,才会允许下一个线程调用这个方法,如果上一个线程没有执行完,那么下一个线程就必须无限期的等待了。这样的话效率就变低了。
有没有办法可以解决呢 ?
还真有,现在然我们回忆一下,我们为什么使用同步关键字?
为了防止new
的时候出现多个对象,只为了防止这一行代码出错,就给整个方法添加同步关键字,有点得不偿失了,所以,可以使用 同步代码块 的方式对new
对象加锁,如下:
同步代码块版懒汉式单例模式
public class SingleTestDemo{
// 1. 私有化静态全局变量
private static SingleTestDemo singleTestDemo = null;
// 2. 构造私有化
private SingleTestDemo(){
}
// 3. 提供对外暴露获取实例变量的静态方法
public static SingleTestDemo getInstance(){
synchronized (SingleTestDemo.class){
if (null == singleTestDemo) {
singleTestDemo = new SingleTestDemo();
}
}
return singleTestDemo;
}
}
相对于使用同步方法,使用同步代码块通过对关键部位加锁而达到和同步方法相同的效果,又可以解决同步方法效率过低的问题。
事情到了这一不,好像已经解决了同步方法效率低的问题了。
然而事情并没有结束,虽然使用同步代码块比同步方法效率高点,但是相对于不使用Synchronized
,效率还是低了不少。
程序员是很懒的,总想使用更少的代码更高效的完成代码。那么有办法提高效率吗?
仔细想一下,我们为什么使用同步代码块?
上面已经说了,是为了防止多线程情况下new
出来多个对象,但是实际上单例模式中的懒汉式只有第一次调用这个方法的时候,才会执行new
对象这一步,其他的时候都是直接返回对象。
好像发现了什么?
对,上面的代码中,使用Synchronized
包裹的是整个判断加new
对象这一步,如果把判断这一步放到关键字外面怎么样? 如下:
同步代码块版懒汉式单例模式2
public class SingleTestDemo {
// 1. 私有化静态全局变量
private static SingleTestDemo singleTestDemo = null;
// 2. 构造私有化
private SingleTestDemo() {
}
// 3. 提供对外暴露获取实例变量的静态方法
public static SingleTestDemo getInstance() {
if (null == singleTestDemo) {
synchronized (SingleTestDemo.class) {
singleTestDemo = new SingleTestDemo();
}
}
return singleTestDemo;
}
}
上面的代码先判断单例对象是不是为空,如果为空,则执行同步关键字里面的代码,创建一个对象,如果不为空,直接返回单例对象。
完美,好像保证了只有第一次的调用的时候会执行同步代码块里面的new
代码!
但是别高兴的太早,别忘记了,我们是在多线程的环境下,让我们放慢线程的执行:
线程一执行到if (null == singleTestDemo)
这一步,发现实例对象是空的,然后正准备执行synchronized (SingleTestDemo.class)
这部分,这时,线程二也来了,执行到if (null == singleTestDemo)
这一块,也发现实例对象是空的,也准备执行synchronized (SingleTestDemo.class)
这一步,这时,线程一腿脚比较快,先一步获取到了锁,执行了同步代码块中的代码,线程二没有获取到锁,只能等线程一执行完,等线程一执行完,已经创建好实例对象了,然后线程二开始执行同步代码块中的代码,由于没有任何判断,线程二也创建了一个实例对象。问题就出现在这,由于同步代码块中没有判断,会出现线程安全问题,怎么解决呢? 那就在同步代码块中添加一个判断就好了,所以,一个新的单例代码如下:
双重否定模式版懒汉式单例模式
public class SingleTestDemo {
// 1. 私有化静态全局变量
private volatile static SingleTestDemo singleTestDemo = null;
// 2. 构造私有化
private SingleTestDemo() {
}
// 3. 提供对外暴露获取实例变量的静态方法
public static SingleTestDemo getInstance() {
if (null == singleTestDemo) {
synchronized (SingleTestDemo.class) {
if (null == singleTestDemo) {
singleTestDemo = new SingleTestDemo();
}
}
}
return singleTestDemo;
}
}
经过上面的场景还原,可以明白为啥使用双重否定单例模式了吧。使用双重否定单例模式,即可以保证线程的安全性,也可以实现只有第一次创建new
的时候才会执行到同步代码块中的代码,提高了效率。
注意:
关于使用Synchronized
关键字的说明:
懒汉式通过使用同步关键字可以达到线程安全问题,但是因为使用了Synchronized
,效率总是会变小的,这就引出一个问题:
什么情况下使用Synchronized
?
这就需要看我们的需求,不是所有的单例都一定要使用Synchronized
来处理的
例如 :
如果我们对性能不是要求很严,完全可以使用同步关键字来处理单例模式多线程安全问题。但是我们必须要知道,使用Synchronized
关键字可能会造成效率下降一百倍。
如果程序总是创建单例并使用单例或者,在项目在启动或者运行的时候创建单例的负担在可接受的范围,可以使用饿汉式,在项目启动的时候就创建好单例对象,这样,肯定不会出现线程安全问题。
上面的几种实现是比较正常的实现,有没有非正常的呢?
有
使用静态内置类实现单例模式(饿汉式)
public class SingleTestDemo {
// 私有静态内置类
private static class InnerSingleDemo{
private static final SingleTestDemo singleTestDemo = new SingleTestDemo();
}
// 构造私有化
private SingleTestDemo(){
}
// 唯一静态方法暴露,用于获取对象
public static SingleTestDemo getInstance(){
return InnerSingleDemo.singleTestDemo;
}
}
使用静态内置类可以实现单例模式,原理是静态内置类可以调用外部类的私有构造防范,并且在项目启动的时候实例化对象,饿汉式,内置静态类可以实现线程安全问题,当调用唯一暴露的方法的时候,单例的实例已经创建了,这样就可以避免线程安全问题了。
但是,使用上面的静态内置类方法,遇到序列化问题时,也会产生多例,解决的方式是实现序列化,如下:
序列化静态内置类版单例模式
public class SingleTestDemo implements Serializable {
private static final long serialVersionUID = 7231734676127745287L;
// 私有静态内置类
private static class InnerSingleDemo{
private static final SingleTestDemo singleTestDemo = new SingleTestDemo();
}
// 构造私有化
private SingleTestDemo(){
System.out.println("SingleTestDemo 构造");
}
// 唯一静态方法暴露,用于获取对象
public static SingleTestDemo getInstance(){
return InnerSingleDemo.singleTestDemo;
}
// 如果是使用序列化和反序列化方式,需要调用下面这个方法,如果不调用,会导致序列化和反序列化不同的对象
protected Object readResolve() {
System.out.println("调用了 readResolve 方法");
return InnerSingleDemo.singleTestDemo;
}
}
通过实现 Serializable 来达到在序列化和反序列化对象的过程中不会出现多个实例。
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
静态代码块实现单例
public class SingleTestDemo {
// 私有化变量
private static SingleTestDemo singleTestDemo = null;
// 构造私有化
private SingleTestDemo(){
}
// 静态代码块
static {
singleTestDemo = new SingleTestDemo();
}
// 提供暴露的方法
public static SingleTestDemo getInstance(){
return singleTestDemo;
}
}
原理和上面静态内置类相似,在调用getInstance
获取实例对象的时候,执行静态代码块中的代码,实例化对象。
第二次调用的时候,已经创建好实例对象了,所以保证了实例的单一
上面所有的方法都有一个特点,构造私有化,但是我们不要忘记,java中有个方式叫反射
,可以通过反射
,绕过私有化的构造,从而创建实例。
那有没有办法避免呢 ?
可以使用枚举类型,枚举可以防止反射和序列化出现多例的情况;
简化版枚举单例模式
public class SingleTestDemo {
public EnumSingleDemo{
singleDemo;
private EnumSingleDemo(){
}
private EnumSingleDemo getSingleDemo(){
return singleDemo;
}
}
public static EnumSingleDemo doSomething(){
return EnumSingleDemo.singleDemo.getSingleDemo();
}
}
枚举类实现单例模式
// 单例模式10 使用枚举的方式实现单例模式 缺点 暴漏了 枚举类
enum SingleDemo10 {
connectionFactory;
private Connection connection;
private SingleDemo10() {
try {
System.out.println("调用了SingleDemo10构造!");
String username = "root";
String password = "root";
String url = "jdbc:mysql://192.168.2.224:3306/mytest2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String driverName = "com.mysql.cj.jdbc.Driver";
Class.forName(driverName);
connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
public Connection getConnection(){
return connection;
}
}
枚举类实现单例模式2-避免暴露枚举类
// 单例模式11 避免暴漏 枚举类
class SingleDemo11{
public enum SingleDemo12 {
connectionFactory;
private Connection connection;
private SingleDemo12() {
try {
System.out.println("调用了SingleDemo10构造!");
String username = "root";
String password = "root";
String url = "jdbc:mysql://192.168.2.224:3306/mytest2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String driverName = "com.mysql.cj.jdbc.Driver";
Class.forName(driverName);
connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
public Connection getConnection(){
return connection;
}
}
public static Connection getConnection(){
return SingleDemo12.connectionFactory.connection;
}
}