单例模式
目录:
一、何为单例 二、使用Java EE实现单例模式 三、使用场景一、何为单例
确保一个类只有一个实例,并且提供了实例的一个全局访问点
1.1 单例模式类图 1.2 单例模式实现 (1)简单实现public class MySingleton1 { private static MySingleton1 instance; private MySingleton1() { } public static MySingleton1 getInstance() { if (instance == null) { // 1 instance = new MySingleton1(); } return instance; }}
(2)线程安全的单例模式
要解决竞态条件问题,你就需要获得一把锁,并且在实例返回后才释放。public class MySingleton2 { private static MySingleton2 instance; private MySingleton2() { } public static synchronized MySingleton2 getInstance() { if (instance == null) { instance = new MySingleton2(); } return instance; }}
(3)类加载时创建单例对象
这样不必同步单例实例的创建,并在JVM加载完所有类时就创建好单例对象public class MySingleton3 { private final static MySingleton3 instance = new MySingleton3(); private MySingleton3() { } public static MySingleton3 getInstance() { return instance; }}
(4)静态块中的单例
这会导致延迟初始化,因为静态块是在构造方法调用前执行的。public class MySingleton4 { private static MySingleton4 instance = null; static { instance = new MySingleton4(); } private MySingleton4() { } public static MySingleton4 getInstance() { return instance; }}
(5)双重检测锁
双重检测锁比其他方法更加安全,因为它会在锁定单例类之前检查一次单例的创建,在对象创造前再一次检查public class MySingleton6 { private volatile MySingleton6 instance; private MySingleton6() { } public MySingleton6 getInstance() { if (instance == null) { // 1 synchronized (MySingleton6.class) { if (instance == null) { // 2 instance = new MySingleton6(); } } } return instance; }}
(6)枚举类型的单例模式
上面的方法都不是绝对安全的,如果开发者讲Java Reflection API的访问修饰符改为public,就可以创建单例了。Java中创建单例最佳方式是枚举类型。 枚举类型本质上就是单例的,因此JVM会处理创建单例所需的大部分工作。这样,通过使用枚举类型,就无需再处理同步对象创建与提供等工作了,还能避免与初始化相关的问题。public enum MySingletonEnum { INSTANCE; public void doSomethingInteresting() { }}
在该示例中,对单例对象示例的引用是通过以下方式获得的:
MySingletonEnum mse=MySingletonEnum.INSTANCE; 一旦拥有了单例的引用,你就可以向下面这样调用它的任何方法了: mse.doSomeThingInteresting();二、使用Java EE实现单例模式
Java EE中可以使用上面的方法,但是还有一种更加优雅且易于使用的方式:单例Bean
1.单例Bean 只需将注解@Singleton添加到类上就可以将其转换为单例Beanimport java.util.HashMap;import java.util.Map;import javax.annotation.PostConstruct;import javax.ejb.Singleton;import java.util.logging.Logger;@Singletonpublic class CacheSingletonBean8 { private MapmyCache; @PostConstruct public void start() { Logger.getLogger("MyGlobalLogger").info("Started!"); myCache = new HashMap (); } public void addUser(Integer id, String name) { myCache.put(id, name); } public String getName(Integer id) { return myCache.get(id); }}
通过注解的简单使用,Java EE不必配置XML文件。项目中有一个beans.xml文件,不过大多数时候其内容都是空的。你只是在启动上下文与依赖注入(CDI)容器时才需要使用它。@Singleton注解将类标记为一个单例EJB,容器会处理该单例实例的创建与使用。
2.在启动时使用单例 默认情况下,Java EE的单例是延迟初始化的,只在需要实例时并且是首次访问时才创建它。不过,你可能希望在启动时就创建实例,不需要任何延迟即可访问到单例,特别是创建实例的代价很大或是在容器启动时就需要Bean。要确保创建时就启动,可在类上使用@Startup注解。import java.util.HashMap;import java.util.Map;import javax.annotation.PostConstruct;import javax.ejb.Singleton;import javax.ejb.Startup;import java.util.logging.Logger;@Startup@Singletonpublic class CacheSingletonBean9 { private MapmyCache; @PostConstruct public void start() { Logger.getLogger("MyGlobalLogger").info("Started!"); myCache = new HashMap (); } public void addUser(Integer id, String name) { myCache.put(id, name); } public String getName(Integer id) { return myCache.get(id); }}
3.确定启动顺序
但是,如果单例依赖于其他资源怎么办?比如:如果连接池是由另一个单例创建的会怎么样,或者日志依赖于另一个单例呢?Java EE提供了一个简单的注解来解决这个问题。使用@DependsOn注解,并将该类所以来的Bean的名字传递给它。import java.util.HashMap;import java.util.Map;import javax.annotation.PostConstruct;import javax.ejb.ConcurrencyManagement;import javax.ejb.ConcurrencyManagementType;import javax.ejb.DependsOn;import javax.ejb.EJB;import javax.ejb.Lock;import javax.ejb.LockType;import javax.ejb.Singleton;import javax.ejb.Startup;@Startup@DependsOn("MyLoggingBean") //加上此注解@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)@Singletonpublic class CacheSingletonBean12 { private MapmyCache; @EJB MyLoggingBean loggingBean; @PostConstruct public void start() { loggingBean.logInfo("Started!"); myCache = new HashMap (); } @Lock(LockType.WRITE) public void addUser(Integer id, String name) { myCache.put(id, name); } @Lock(LockType.READ) public String getName(Integer id) { return myCache.get(id); }}
接下来再创建一个单例Bean,作为上一个Bean所用的Bean
import javax.annotation.PostConstruct;import javax.ejb.Singleton;import javax.ejb.Startup;import java.util.logging.Logger;@Startup@Singletonpublic class MyLoggingBean { private Logger logger; @PostConstruct public void start() { logger = Logger.getLogger("MyGlobalLogger"); logger.info("Well, I started first!!!"); } public void logInfo(String msg) { logger.info(msg); }}
4.管理并发
Java Ee提供了两种并发管理:容器管理并发与Bean管理并发。在容器管理并发中,容器负责处理读写访问相关的一切事宜,而Bean管理并发则需要开发者使用同步等传统的Java方法来处理并发。 可以通过ConcurrencyManagementType.BEAN注解管理并发。 默认情况下,Java EE使用的事容器管理并发,不过可以通过ConcurrentManagementType.CONTAINER注解进行显示声明。@Startup@DependsOn("MyLoggingBean")@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)@Singleton@AccessTimeout(value = 120000)// default in millisecondspublic class CacheSingletonBean13 {
回到之前的示例,使用@Lock注解来控制访问
import java.util.HashMap;import java.util.Map;import javax.annotation.PostConstruct;import javax.ejb.ConcurrencyManagement;import javax.ejb.ConcurrencyManagementType;import javax.ejb.DependsOn;import javax.ejb.EJB;import javax.ejb.Lock;import javax.ejb.LockType;import javax.ejb.Singleton;import javax.ejb.Startup;@Startup@DependsOn("MyLoggingBean")@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)@Singletonpublic class CacheSingletonBean12 { private MapmyCache; @EJB MyLoggingBean loggingBean; @PostConstruct public void start() { loggingBean.logInfo("Started!"); myCache = new HashMap (); } @Lock(LockType.WRITE) public void addUser(Integer id, String name) { myCache.put(id, name); } @Lock(LockType.READ) public String getName(Integer id) { return myCache.get(id); }}
三、单例模式的使用场景
一般来说,大量使用单例可能是一个滥用的信号,你应该在合理情况下使用单例:
- 跨越整个应用程序域来访问共享数据,比如配置数据
- 只加载并缓存代价高傲的资源一次,这样可以做到全局共享访问并改进性能
- 创建应用日志实例,因为通常情况下只需要一个即可
- 管理实现了工厂模式的类中的对象
- 创建门面对象,因为通常情况下只需要一个即可
- 延迟创建静态类,单例可以做到延迟实例化
对于重要的缓存解决方案来说,请考虑使用框架,比如:Ehcache、Java Caching System