龙空技术网

一文彻底搞明白单例模式

灵犀架构课堂 63

前言:

现时朋友们对“java中的单例模式的使用场景有哪些”可能比较注重,姐妹们都需要学习一些“java中的单例模式的使用场景有哪些”的相关文章。那么小编同时在网络上网罗了一些关于“java中的单例模式的使用场景有哪些””的相关内容,希望姐妹们能喜欢,朋友们快快来学习一下吧!

本篇讲解Java设计模式中的单例模式,分为定义、模式应用前案例、结构、模式应用后案例、适用场景、模式可能存在的困惑和本质探讨7个部分。

定义

单例模式用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在新的分类方式中,单例模式被划分至类的复用类别中,其复用的是全局唯一的对象。

模式应用前案例

在单例模式中,我们列举一个数据库连接使用的案例,因为在资源使用时经常会用到该模式。以下代码是未应用单例模式的案例。

public class DatabaseConnection {//数据库连接类    private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";    private static final String USERNAME = "username";    private static final String PASSWORD = "password";    public Connection getConnection() {        Connection connection = null;        try {            connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);        } catch (SQLException e) {            e.printStackTrace();        }        return connection;    }    public void closeConnection(Connection conn) throws SQLException{        if(conn != null){            conn.close();        }    }}public class Client {//调用方代码    public static void main(String[] args) throws SQLException {        // 创建DatabaseConnetion实例        DatabaseConnection dbConn = new DatabaseConnection();        // 获取数据库连接对象        Connection conn1 = dbConn.getConnection();        // 使用conn1执行SQL操作        System.out.println("使用conn1执行SQL操作!");        // 关闭conn1连接        dbConn.closeConnection(conn1);        // 再次获取新的数据库连接对象        Connection conn2= dbConn.getConnection();        // 使用conn2执行SQL操作        System.out.println("使用conn2执行SQL操作!");        // 关闭conn2连接        dbConn.closeConnection(conn2);    }}

从上述代码可以看到,每次对数据库进行操作,都需要获取一个新的连接,其主要问题是每次创建连接需要的耗时较长。此外,很多情况下,数据库服务端对连接数会有一个上限限制,如果达到上限,获取连接也将会失败。

结构

Singleton类中有两个主要关注的地方。一是私有的构造函数,目的是仅有类自身能够创建自己的实例;二是对外部暴露的GetInstance方法,这是一个静态方法,用于创建该类的唯一实例。

Singleton的示例代码如下。

public class Singleton {    // 定义一个私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载    private static Singleton instance = null;    // 私有构造方法,防止被实例化    private Singleton() {}    // 单例方法,创建实例对象    public static Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance;    }}
模式应用后案例

上面数据库连接的案例。对于这种情景来说,数据库连接是一个应当可以供所有查询共用的连接。如果使用单例模式,其具体实现又可以包括多种方式,下面逐一介绍。

1.饿汉模式

饿汉模式的特点在于唯一的静态实例instance在类使用之前的初始化阶段就已经准备好,不支持延迟加载。

public class DatabaseConnectionPool {//饿汉模式    private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";    private static final String USERNAME = "username";    private static final String PASSWORD = "password";    private static final DatabaseConnectionPool instance = new DatabaseConnectionPool();    private Connection connection;    // 私有构造函数,防止外部实例化    private DatabaseConnectionPool() {        try {            this.connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);        } catch (SQLException e) {            e.printStackTrace();        }    }    // 获取唯一可用的对象实例    public synchronized static DatabaseConnectionPool getInstance() {        return instance;    }    public Connection getConnection() {        return this.connection;    }    public void closeConnection() throws SQLException{        if(this.connection != null){            this.connection.close();        }    }}public class Client {//调用方代码    public static void main(String[] args) throws SQLException {        // 获取DatabaseConnetionPool类的唯一实例        DatabaseConnectionPool connectionPool = DatabaseConnectionPool.getInstance();        // 获取数据库连接对象        Connection conn = connectionPool.getConnection();        // 使用conn执行SQL操作        conn.close();    }}

2.懒汉模式

懒汉模式的特点是相对于饿汉模式来讲的,它能够支持延迟加载。

但是相对饿汉模式的模式不足在于,在getInstance方法上的锁会在高并发场景中存在性能问题。

public class DatabaseConnectionPool {//懒汉模式    private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";    private static final String USERNAME = "username";    private static final String PASSWORD = "password";    private static DatabaseConnectionPool instance;    private Connection connection;    // 私有构造函数,防止外部实例化    private DatabaseConnectionPool() {        try {            this.connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);        } catch (SQLException e) {            e.printStackTrace();        }    }    // 获取唯一可用的对象实例    public synchronized static DatabaseConnectionPool getInstance() {        if (instance == null) {            instance = new DatabaseConnectionPool();        }        return instance;    }    public Connection getConnection() {        return this.connection;    }    public void closeConnection() throws SQLException{        if(this.connection != null){            this.connection.close();        }    }}public class Client {//调用方代码    public static void main(String[] args) throws SQLException {        // 获取DatabaseConnetionPool类的唯一实例        DatabaseConnectionPool connectionPool = DatabaseConnectionPool.getInstance();        // 获取数据库连接对象        Connection conn = connectionPool.getConnection();        // 使用conn执行SQL操作        conn.close();    }}

3.双重锁定模式

双重锁定模式的主要特点在于一定程度解决了懒汉模式同步性能瓶颈的问题,代码如下所示。

可以发现,双重锁定的同步锁放在第一个instance==null判断逻辑上。如果instance实例不为空,就不需要进入加锁逻辑中,因此一定程序解决了高并发的性能瓶颈。

public class DatabaseConnectionPool {//双重锁定模式    private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";    private static final String USERNAME = "username";    private static final String PASSWORD = "password";    private volatile static DatabaseConnectionPool instance;    private Connection connection;    // 私有构造函数,防止外部实例化    private DatabaseConnectionPool() {        try {            this.connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);        } catch (SQLException e) {            e.printStackTrace();        }    }    // 获取唯一可用的对象实例    public static DatabaseConnectionPool getInstance() {        if(instance == null){            synchronized(DatabaseConnectionPool.class){                if(instance == null){                    // 创建实例前后两次判断是否为null,以确保只有一个实例被创建                    instance = new DatabaseConnectionPool();                }            }        }        return instance;    }    public Connection getConnection() {        return this.connection;    }    public void closeConnection() throws SQLException{        if(this.connection != null){            this.connection.close();        }    }}public class Client {//调用方代码    public static void main(String[] args) throws SQLException {        // 获取DatabaseConnetionPool类的唯一实例        P01_Sigleton.stage2.DatabaseConnectionPool connectionPool = DatabaseConnectionPool.getInstance();        // 获取数据库连接对象        Connection conn = connectionPool.getConnection();        // 使用conn执行SQL操作        conn.close();    }}

4.静态内部类模式

双重检测的代码不太容易理解。相比较来言,Java的静态内部类是一种更简约的实现方式,代码如下所示。

它的最大特点也是实现了延迟加载和创建的线程安全问题,只不过依赖的是JVM的内部机制。

其中,SingletonHelper是一个静态内部类,当外部DatabaseConnectionPool初始化的时候,SingletonHelper对象并不会被创建,只有当getInstance方法调用的时候才会被创建。

public class DatabaseConnectionPool {//静态内部类模式    private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";    private static final String USERNAME = "username";    private static final String PASSWORD = "password";    // 静态内部类持有外部类的唯一实例    private static class SingletonHelper {        private static final DatabaseConnectionPool INSTANCE = new DatabaseConnectionPool();    }    // 获取唯一可用的对象实例    public synchronized static DatabaseConnectionPool getInstance() {        return SingletonHelper.INSTANCE;    }    public Connection getConnection() throws SQLException{        return DriverManager.getConnection(URL, USERNAME, PASSWORD);    }}public class Client {//调用方代码    public static void main(String[] args) throws SQLException {        // 获取DatabaseConnetionPool类的唯一实例        DatabaseConnectionPool connectionPool = DatabaseConnectionPool.getInstance();        // 获取数据库连接对象        Connection conn = connectionPool.getConnection();        // 使用conn执行SQL操作        conn.close();    }}

5.枚举模式

最后,介绍一种Java枚举支持的模式,这种方式也是利用JVM的特性,实现起来更加优雅,代码如下所示。

public enum DatabaseConnectionPool {//枚举模式    INSTANCE;  // 唯一实例    private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";    private static final String USERNAME = "username";    private static final String PASSWORD = "password";    private Connection connection;    // 在枚举类中可以定义构造函数来初始化连接池    DatabaseConnectionPool() {        try {            this.connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);        } catch (SQLException e) {            e.printStackTrace();        }    }    public Connection getConnection() {        return this.connection;    }    public void closeConnection() throws SQLException{        if(this.connection != null){            this.connection.close();        }    }}public class Client {//调用方代码    public static void main(String[] args) throws SQLException {        // 获取DatabaseConnetionPool类的唯一实例        DatabaseConnectionPool connectionPool = DatabaseConnectionPool.INSTANCE;        // 获取数据库连接对象        Connection conn = connectionPool.getConnection();        // 使用conn执行SQL操作        conn.close();    }}
适用场景

单例模式主要适用于以下两类场景:

一是当类有多个实例,并且多个实例之间在高并发场景下可能会产生一些潜在的冲突,非单个实例不可,例如文件写入这种场景,如果处理不好并发,就容易出现写冲突。

二是类可以有个实例,但是会造成资源的浪费,因为一个实例就完全足以支撑调用方使用,例如数据库连接在简单场景下即如此。

模式可能存在的困惑

困惑1:懒汉模式和饿汉模式的优劣问题?

两者没有绝对的优劣之分,需要视具体的使用场景而定。

本质

单例模式的本质是对象资源的复用,不论对象是被迫唯一还是能够唯一,实际效果都是多个调用方复用同一对象。

并且,这种复用与时间先后无关,即同一时间点多个调用方也可以同时使用。

标签: #java中的单例模式的使用场景有哪些