單例模式是一種常見的設計模式;Java Singleton 模式就為我們提供了這樣實現的可能。使用Singleton的好處還在於可以節省內存,因為它限制了實例的個數,
有利於Java垃圾回收(garbage collection)。
單例模式也是一種比較常見的設計模式,它到底能帶給我們什么好處呢?其實無非是三個方面的作用:
1、控制資源的使用,通過線程同步來控制資源的並發訪問;
2、控制實例產生的數量,達到節約資源的目的。
3、作為通信媒介使用,也就是數據共享,它可以在不建立直接關聯的條件下,讓多個不相關的兩個線程或者進程之間實現通信。
單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三種。
單例模式有一下特點:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
1. [代碼]懶漢式單例
//單例模式-懶漢式單例 public class LazySingleton { //私有靜態對象,加載時候不做初始化 private static LazySingleton m_intance=null; // 私有構造方法,避免外部創建實例 private LazySingleton(){} /** * 靜態工廠方法,返回此類的唯一實例. * 當發現實例沒有初始化的時候,才初始化. * @return LazySingleton */ synchronized public static LazySingleton getInstance(){ if(m_intance==null){ m_intance=new LazySingleton(); } return m_intance; } }
2. [代碼]餓漢式單例
在類加載的時候不創建單例實例。只有在第一次請求實例的時候的時候創建,並且只在第一次創建后,以后不再創建該類的實例。
/* * 單例模式-餓漢式單例 */ public class EagerSingleton { /* * 私有的(private)唯一(static final)實例成員,在類加載的時候就創建好了單例對象 */ private static final EagerSingleton m_instance = new EagerSingleton(); /** * 私有構造方法,避免外部創建實例 */ private EagerSingleton() { } /** * 靜態工廠方法,返回此類的唯一實例. * @return EagerSingleton */ public static EagerSingleton getInstance() { return m_instance; } }
3. [代碼]登記式單例
這個單例實際上維護的是一組單例類的實例,將這些實例存放在一個Map(登記薄)中,對於已經登記過的實例,則從工廠直接返回,對於沒有登記的,則先登記,而后返回。
/** * 單例模式- 登記式單例 */ public class RegSingleton { /** * 登記薄,用來存放所有登記的實例 */ private static Map m_registry = new HashMap(); //在類加載的時候添加一個實例到登記薄 static { RegSingleton x = new RegSingleton(); m_registry.put(x.getClass().getName(), x); } /** * 受保護的默認構造方法 */ protected RegSingleton() {} /** * 靜態工廠方法,返回指定登記對象的唯一實例; * 對於已登記的直接取出返回,對於還未登記的,先登記,然后取出返回 * @param name * @return RegSingleton */ public static RegSingleton getInstance(String name) { if (name == null) { name = "RegSingleton"; } if (m_registry.get(name) == null) { try { m_registry.put(name, (RegSingleton) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return m_registry.get(name); } /** * 一個示意性的商業方法 * @return String */ public String about() { return "Hello,I am RegSingleton!"; } }
四、單例模式的一個應用
該應用是配置文件管理類。為了本例能正確運行,我在C盤下先建立了一個xxxx.properties文件,內容如下:
user=root
password=leizhimin
這個配置文件管理類的代碼如下:
/** * 單例模式應用-單例類應用-配置文件管理 */ public class ConfigManager { /** * 屬性文件全名 */ private static final String PFILE = "C:/xxx.properties"; /** * 對應於屬性文件的文件對象變量 */ private File m_file = null; /** * 屬性文件的最后修改日期 */ private long m_lastModifiedTime = 0; /** * 屬性文件所對應的屬性對象變量 */ private Properties m_props = null; /** * 本類可能存在的唯一的一個實例 */ private static ConfigManager m_instance = new ConfigManager(); /** * 私有構造子,用以保證外界無法直接實例化 */ private ConfigManager() { m_file = new File(PFILE); m_lastModifiedTime = m_file.lastModified(); if (m_lastModifiedTime == 0) { System.err.println(PFILE + " file does not exist!"); } m_props = new Properties(); try { m_props.load(new FileInputStream(PFILE)); } catch (IOException e) { e.printStackTrace(); } } /** * 靜態工廠方法 * * @return ConfigManager */ synchronized public static ConfigManager getInstance() { return m_instance; } /** * 獲取屬性配置項的值 * @param name * @param defaultVal * @return Object */ public final Object getConfigItem(String name, Object defaultVal) { long newTime = m_file.lastModified(); if (newTime == 0) { //屬性文件不存在 if (m_lastModifiedTime == 0) { System.err.println(PFILE + " file does not exist!"); } else { System.err.println(PFILE + " file was deleted!"); } return defaultVal; } else if (newTime > m_lastModifiedTime) { m_props.clear(); try { m_props.load(new FileInputStream(PFILE)); } catch (IOException e) { e.printStackTrace(); } } m_lastModifiedTime = newTime; Object val = m_props.getProperty(name); if (val == null) { return defaultVal; } else { return val; } } }
測試配置文件類:
/** * 配置文件管理類測試 */ public class Test_ConfigManager { public static void main(String[] args) { ConfigManager cfgm = ConfigManager.getInstance(); Object val1 = cfgm.getConfigItem("sdf", "leizhimin"); Object val2 = cfgm.getConfigItem("user", "leizhimin"); System.out.println(val1.toString()); System.out.println(val2.toString()); } }
運行結果:
leizhimin
root
Process finished with exit code 0
五、一個JDBC數據庫工具類的單例實現
/** * 單例模式在JDBC編程中的應用,用於設計數據庫工具類 */ public class DBUtil { //單一實例 private static final DBUtil _instance = new DBUtil(); //數據源的JNDI private static final String datasource = "java:comp/env/jdbc/zvfims"; /** * 私有構造方法,防止外部實例化 */ private DBUtil() { } /** * 數據庫工具類實例工廠 * @return DBUtil */ public DBUtil getInstance() { return _instance; } /** * 業務方法:用於獲取數據庫連接 * @return Connection */ public Connection makeConnection() { Connection conn = null; try { Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup(datasource); conn = ds.getConnection(); } catch (NamingException e) { System.out.println("獲取數據源異常,請AppServer的JNDI數據源配置!"); e.printStackTrace(); } catch (SQLException e) { System.err.println("獲取數據庫連接發生異常!"); e.printStackTrace(); } return conn; } }
通過這個單例類和開放的業務方法,可以為整個系統應用提供數據庫連接。
單例模式的實現方式:懶漢單例類和餓漢單例類
單例模式的實現有多種方法,常見的就有懶漢式單例類和餓漢式單例類。我們前面介紹的實現方法就屬於懶漢式單例類。
懶漢式單例類
對於懶漢模式,我們可以這樣理解:該單例類非常懶,只有在自身需要的時候才會行動,從來不知道及早做好准備。它在需要對象的時候,才判斷是否已有對象,如果沒有就立即創建一個對象,然后返回,如果已有對象就不再創建,立即返回。
懶漢模式只在外部對象第一次請求實例的時候才去創建。
餓漢式單例
對於餓漢模式,我們可以這樣理解:該單例類非常餓,迫切需要吃東西,所以它在類加載的時候就立即創建對象。
我們對比一下懶漢模式和餓漢模式的優缺點:
懶漢模式,它的特點是運行時獲得對象的速度比較慢,但加載類的時候比較快。它在整個應用的生命周期只有一部分時間在占用資源。
餓漢模式,它的特點是加載類的時候比較慢,但運行時獲得對象的速度比較快。它從加載到應用結束會一直占用資源。
這 兩種模式對於初始化較快,占用資源少的輕量級對象來說,沒有多大的性能差異,選擇懶漢式還是餓漢式都沒有問題。但是對於初始化慢,占用資源多的重量級對象 來說,就會有比較明顯的差別了。所以,對重量級對象應用餓漢模式,類加載時速度慢,但運行時速度快;懶漢模式則與之相反,類加載時速度快,但運行時第一次 獲得對象的速度慢。
從用戶體驗的角度來說,我們應該首選餓漢模式。我們願意等待某個程序花較長的時間初始化,卻不喜歡在程序運行時等待太久,給人一種反應遲鈍的感覺,所以對於有重量級對象參與的單例模式, 我們推薦使用餓漢模式 。
而對於初始化較快的輕量級對象來說,選用哪種方法都可以。如果一個應用中使用了大量單例模式,我們就應該權衡兩種方法了。輕量級對象的單例采用懶漢模式,減 輕加載時的負擔,縮短加載時間,提高加載效率;同時由於是輕量級對象,把這些對象的創建放在使用時進行,實際就是把創建單例對象所消耗的時間分攤到整個應 用中去了,對於整個應用的運行效率沒有太大影響。
什么情況下使用單例模式
單例模式也是一種比較常見的設計模式,它到底能帶給我們什么好處呢?其實無非是三個方面的作用:
第一、控制資源的使用,通過線程同步來控制資源的並發訪問;
第二、控制實例產生的數量,達到節約資源的目的。
第三、作為通信媒介使用,也就是數據共享,它可以在不建立直接關聯的條件下,讓多個不相關的兩個線程或者進程之間實現通信。
比如,數據庫連接池的設計一般采用單例模式,數據庫連接是一種數據庫資源。軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損 耗,這種效率上的損耗還是非常昂貴的。當然,使用數據庫連接池還有很多其它的好處,可以屏蔽不同數據數據庫之間的差異,實現系統對數據庫的低度耦合,也可 以被多個系統同時使用,具有高可復用性,還能方便對數據庫連接的管理等等。數據庫連接池屬於重量級資源,一個應用中只需要保留一份即可,既節省了資源又方 便管理。所以數據庫連接池采用單例模式進行設計會是一個非常好的選擇。
在我們日常使用的在Windows中也有不少單例模式設計的組件,象 常用的文件管理器。由於Windows操作系統是一個典型的多進程多線程系統,那么在創建或者刪除某個文件的時候,就不可避免地出現多個進程或線程同時操 作一個文件的現象。采用單例模式設計的文件管理器就可以完美的解決這個問題,所有的文件操作都必須通過唯一的實例進行,這樣就不會產生混亂的現象。
再比如,每台計算機可以有若干個打印機,如果每一個進程或者線程都獨立地使用打印機資源的話,那么我們打印出來的結果就有可能既包含這個打印任務的一部分, 又包含另外一個打印任務的一部分。所以,大多數的操作系統最終為打印任務設計了一個單例模式的假脫機服務Printer Spooler,所有的打印任務都需要通過假脫機服務進行。
實際上,配置信息類、管理類、控制類、門面類、代理類通常被設計為單例類。像Java的Struts、Spring框架,.Net的Spring.Net框架,以及Php的Zend框架都大量使用了單例模式。
使用單例模式最核心的一點是體現了面向對象封裝特性中的“單一職責”和“對象自治”原則。
很多時候我們要保證類的實例只有一個。我們可能在自己的代碼中判斷需要的類的實例有無,無就new一個。這樣看似不錯。問題是,你用到這個類的地方有n個,這樣你就需要n個判斷。為什么不把這個職責交給類本身呢?然后讓類提供一個接口訪問。
private static Student student = null; //不建立對象 Student getInstance(){ if(student == null) { //先判斷是否為空 student = new Student(); //懶漢式做法 } return student; }
private static Student student = new Student(); //建立對象 Student getInstance(){ return student; //直接返回單例對象 }
單例模式----數據庫連接池管理類的應用
package com.cvicse.util; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import java.util.Properties; import java.util.StringTokenizer; import java.util.Vector; /** * 數據庫連接池管理類 * @功能 :管理類DBConnectionManager支持對一個或多個由屬性文件定義的數據庫連接池的訪問. * 客戶程序可以調用getInstance()方法訪問本類的唯一實例 */ public class DBConnectionManager { private static DBConnectionManager instance; // 唯一實例 private static int clients; // 連接的客戶端 private Vector drivers = new Vector();// 驅動集合 private Hashtable pools = new Hashtable();// 連接池 private Properties dbProps;// 屬性文件 private PrintWriter log; // 日志變量 /** * 單例模式建構私有函數以防止其它對象創建本類實例 */ private DBConnectionManager() { this.init(); } /** * 采用單例模式,返回唯一實例.如果是第一次調用此方法,則創建實例 * @return DBConnectionManager 唯一實例 */ public static synchronized DBConnectionManager getInstance() { if (instance == null) { instance = new DBConnectionManager(); } clients++; return instance; } /** * 獲得一個可用的(空閑的)連接.如果沒有可用連接,且已有連接數小於最大連接數 限制,則創建並返回新連接 * @param name 在屬性文件中定義的連接池名字 * @return Connection 可用連接或null */ public Connection getConnection(String name) { DBConnectionPool dbPool = (DBConnectionPool) pools.get(name); if (dbPool != null) { return dbPool.getConnection(); } return null; } /** * 獲得一個可用連接.若沒有可用連接,且已有連接數小於最大連接數限制, 則創建並返回新連接. 否則,在指定的時間內等待其它線程釋放連接. * @param name * 連接池名字 * @param time * 以毫秒計的等待時間 * @return Connection 可用連接或null */ public Connection getConnection(String name, long time) { DBConnectionPool dbPool = (DBConnectionPool) pools.get(name); if (dbPool != null) { return dbPool.getConnection(time); } return null; } /** * 將連接對象返回給由名字指定的連接池 * @param name * 在屬性文件中定義的連接池名字 * @param con * 連接對象 */ public void freeConnection(String name, Connection con) { DBConnectionPool dbPool = (DBConnectionPool) pools.get(name); if (dbPool != null) { dbPool.freeConnection(con); } } /** * 關閉所有連接,撤銷驅動程序的注冊 */ public synchronized void release() { // 等待直到最后一個客戶程序調用 if (--clients != 0) { return; } Enumeration allPools = pools.elements(); while (allPools.hasMoreElements()) { DBConnectionPool pool = (DBConnectionPool) allPools.nextElement(); pool.release(); } Enumeration allDrivers = drivers.elements(); while (allDrivers.hasMoreElements()) { Driver driver = (Driver) allDrivers.nextElement(); try { DriverManager.deregisterDriver(driver); log("撤銷JDBC驅動程序 " + driver.getClass().getName() + "的注冊"); } catch (SQLException e) { log(e, "無法撤銷下列JDBC驅動程序的注冊: " + driver.getClass().getName()); } } } /** * 讀取屬性完成初始化 */ private void init() { // 文件流輸入方式 InputStream fileinputstream = null; try { fileinputstream = new FileInputStream("./src/db.properties"); } catch (FileNotFoundException e) { e.printStackTrace(); } try { dbProps = new Properties(); dbProps.load(fileinputstream); } catch (Exception e) { e.printStackTrace(); System.err.println("不能讀取屬性文件. " + "請確保db.properties在CLASSPATH指定的路徑中"); return; } String logFile = dbProps.getProperty("logfile", "DBConnectionManager.log"); try { log = new PrintWriter(new FileWriter(logFile, true), true); } catch (IOException e) { System.err.println("無法打開日志文件: " + logFile); log = new PrintWriter(System.err); } // 加載驅動 loadDrivers(dbProps); // 創建連接池 createPools(dbProps); } /** * 裝載和注冊所有JDBC驅動程序 * * @param props * 屬性 */ private void loadDrivers(Properties props) { String driverClasses = props.getProperty("drivers"); StringTokenizer st = new StringTokenizer(driverClasses); while (st.hasMoreElements()) { String driverClassName = st.nextToken().trim(); try { Driver driver = (Driver) Class.forName(driverClassName) .newInstance(); DriverManager.registerDriver(driver); drivers.addElement(driver); log("成功注冊JDBC驅動程序" + driverClassName); } catch (Exception e) { log("無法注冊JDBC驅動程序: " + driverClassName + ", 錯誤: " + e); } } } /** * 根據指定屬性創建連接池實例. * * @param props * 連接池屬性 */ private void createPools(Properties props) { Enumeration propNames = props.propertyNames(); while (propNames.hasMoreElements()) { String name = (String) propNames.nextElement(); if (name.endsWith(".url")) { String poolName = name.substring(0, name.lastIndexOf(".")); System.out.println(" poolName ||" + poolName + "|"); String url = props.getProperty(poolName + ".url"); if (url == null) { log("沒有為連接池" + poolName + "指定URL"); continue; } String user = props.getProperty(poolName + ".user"); String password = props.getProperty(poolName + ".password"); String maxconn = props.getProperty(poolName + ".maxconn", "0"); int max; try { max = Integer.valueOf(maxconn).intValue(); } catch (NumberFormatException e) { log("錯誤的最大連接數限制: " + maxconn + " .連接池: " + poolName); max = 0; } DBConnectionPool pool = new DBConnectionPool(poolName, url, user, password, max); pools.put(poolName, pool); log("成功創建連接池" + poolName); } } } /** * 將文本信息寫入日志文件 */ private void log(String msg) { log.println(new Date() + ": " + msg); } /** * 將文本信息與異常寫入日志文件 */ private void log(Throwable e, String msg) { log.println(new Date() + ": " + msg); e.printStackTrace(log); } /*************************************************************************** ************************數據庫連接池內部類************************************ **************************************************************************/ /** * * @功能:數據庫連接池內類 此內部類定義了一個連接池.它能夠根據要求創建新連接,直到預定的最大連接數為止. * 在返回連接給客戶程序之前,它能夠驗證連接的有效性 */ class DBConnectionPool { private String poolName; // 連接池名字 private String dbConnUrl; // 數據庫的JDBC URL private String dbUserName; // 數據庫賬號或null private String dbPassWord; // 數據庫賬號密碼或null private int maxConn; // 此連接池允許建立的最大連接數 private int checkedOut; // 當前連接數 private Vector<Connection> freeConnections; // 保存所有可用連接 /** * 創建新的連接池構造函數 * * @param poolName * 連接池名字 * @param dbConnUrl * 數據庫的JDBC URL * @param dbUserName * 數據庫帳號或 null * @param dbPassWord * 密碼或 null * @param maxConn * 此連接池允許建立的最大連接數 */ public DBConnectionPool(String poolName, String dbConnUrl, String dbUserName, String dbPassWord, int maxConn) { this.poolName = poolName; this.dbConnUrl = dbConnUrl; this.dbUserName = dbUserName; this.dbPassWord = dbPassWord; this.maxConn = maxConn; this.freeConnections = new Vector<Connection>(); } /** * 從連接池獲得一個可用連接.如果沒有空閑的連接且當前連接數小於最大連接 數限制,則創建新連接. * 如原來登記為可用的連接不再有效,則從向量刪除之,然后遞歸調用自己以嘗試新的可用連接. */ public synchronized Connection getConnection() { Connection conn = null;// 定義連接標量 if (freeConnections != null && freeConnections.size() > 0) { // 獲取向量中第一個可用連接 conn = (Connection) freeConnections.firstElement(); freeConnections.removeElementAt(0); try { if (conn.isClosed()) { log("從連接池" + poolName + "刪除一個無效連接"); // 遞歸調用自己,嘗試再次獲取可用連接 conn = getConnection(); } } catch (SQLException e) { log("從連接池" + poolName + "刪除一個無效連接"); // 遞歸調用自己,嘗試再次獲取可用連接 conn = getConnection(); } } else if (maxConn == 0 || checkedOut < maxConn) { conn = newConnection(); } if (conn != null) { checkedOut++; } return conn; } /** * 從連接池獲取可用連接.可以指定客戶程序能夠等待的最長時間 參見前一個getConnection()方法. * * @param timeout * 以毫秒計的等待時間限制 */ public synchronized Connection getConnection(long timeout) { long startTime = System.currentTimeMillis(); Connection conn = null;// 定義連接標量 while ((conn = getConnection()) == null) { try { wait(timeout); } catch (InterruptedException e) { e.printStackTrace(); } if ((System.currentTimeMillis() - startTime) >= timeout) { // wait()返回的原因是超時 return null; } } return conn; } /** * 創建新的連接 * * @return 返回數據庫連接 */ private Connection newConnection() { Connection conn = null;// 定義連接標量 try { if (dbUserName == null) { conn = DriverManager.getConnection(dbConnUrl); } else { conn = DriverManager.getConnection(dbConnUrl, dbUserName, dbPassWord); } log("連接池" + poolName + "創建一個新的連接"); } catch (SQLException e) { log(e, "無法創建下列URL的連接: " + dbConnUrl); return null; } return conn; } /** * 將不再使用的連接返回給連接池 * * @param con * 客戶程序釋放的連接 */ public synchronized void freeConnection(Connection conn) { // 將指定連接加入到向量末尾 freeConnections.addElement(conn); checkedOut--; notifyAll(); // 刪除等待隊列中的所有線程 } /** * 關閉所有連接 */ public synchronized void release() { Enumeration<Connection> allConnections = freeConnections.elements(); while (allConnections.hasMoreElements()) { Connection con = (Connection) allConnections.nextElement(); try { con.close(); log("關閉連接池" + poolName + "中的一個連接"); } catch (SQLException e) { log(e, "無法關閉連接池" + poolName + "中的連接"); } } freeConnections.removeAllElements(); } } } 測試類 /* * * InforGuard Copyright 2008 CVICSE, Co.ltd . * All rights reserved. * Package: com.cvicse.util * FileName: Test.java */ package com.cvicse.util; import java.sql.Connection; public class Test { /** * @param args */ public static void main(String[] args) { // 文件流輸入方式 DBConnectionManager connectionManager = DBConnectionManager .getInstance(); try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } Connection conn1 = connectionManager.getConnection("MySQL"); Connection conn2 = connectionManager.getConnection("mysql"); Connection conn3 = connectionManager.getConnection("mysql"); Connection conn4 = connectionManager.getConnection("mysql"); Connection conn5 = connectionManager.getConnection("mysql"); System.out.println(" conn1 == " + conn1); System.out.println(" conn2 == " + conn2); System.out.println(" conn3 == " + conn3); System.out.println(" conn4 == " + conn4); System.out.println(" conn5 == " + conn5); connectionManager.freeConnection("mysql", conn1); connectionManager.freeConnection("mysql", conn2); connectionManager.freeConnection("mysql", conn3); connectionManager.freeConnection("mysql", conn4); connectionManager.freeConnection("mysql", conn5); try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } Connection conn6 = connectionManager.getConnection("mysql"); Connection conn7 = connectionManager.getConnection("mysql"); Connection conn8 = connectionManager.getConnection("mysql"); Connection conn9 = connectionManager.getConnection("mysql"); Connection conn10 = connectionManager.getConnection("mysql"); System.out.println(" conn6 == " + conn6); System.out.println(" conn7 == " + conn7); System.out.println(" conn8 == " + conn8); System.out.println(" conn9 == " + conn9); System.out.println(" conn10 == " + conn10); } }