1.何为单列模式
Spring等企业框架大量使用了单列模式,单列类可以保证其类型只会生成一个实例。只拥有一个实例在很多时候是很有用的,比如说全局访问以及缓存代价高昂的资源。
在JavaEE中提供了一种内建机制,开发者可以通过为类添加注解来方便的创建单列。
1.1单列模式的运用场景
1.跨越整个应用程序域来访问共享数据,比如配置数据
2.只加载并缓存代价高昂的资源一次,这样可以做到全局共享访问并且改进性能。
3.创建应用日志实例,因为通常情况下只需要一个即可。
4.管理实现工厂模式的类中的对象。
5,创建门面对象,因为通常情况下只需要一个即可。
6.延时创建静态类,单列可以做到延迟实例化
1.2单列模式的弊病
1.过度使用单列模式意味着不必要的资源缓存,无法让垃圾收集器回收对象并释放宝贵的内存资源
2.无法利用对象创建和继承的好处,且对单元测试不太友好。
1.3用普通代码实现单列模式
单列模式基于一个单列类,它持有一个指向自身唯一实例的引用,同时通过唯一的getter方法控制其创建与访问。
//单列模式的实现
public class MySingleton1 {
private static MySingleton1 instance;
private MySingleton1() {
}
public static MySingleton1 getInstance() {
if (instance == null) { // 1
instance = new MySingleton1();
}
return instance;
}
}
上面的代码看起来是可以运行的,其实有bug。由于对象创建方法部署原子的,因此在j竞态条件下是很容易出错的。在多线程的环境下,这会导致创建多个单列实例的后果。
解决这一个问题,可以通过synchronized关键字实现锁机制。
public class MySingleton2 {
private static MySingleton2 instance;
private MySingleton2() {
}
public static synchronized MySingleton2 getInstance() {
if (instance == null) {
instance = new MySingleton2();
}
return instance;
}
}
另外一种手段是在加载类的同时创建单列实例,这样就不必同步单列实例的创建,并在JVM加载完所有类时就创建好单列对象(因此,这是在类调用getInstance方法之前发生的)。这所以可以这样,是因为静态成员与静态块是类加载时执行的。
public class MySingleton3 {
private final static MySingleton3 instance = new MySingleton3();
private MySingleton3() {
}
public static MySingleton3 getInstance() {
return instance;
}
}
public class MySingleton4 {
private static MySingleton4 instance = null;
static {
instance = new MySingleton4();
}
private MySingleton4() {
}
public static MySingleton4 getInstance() {
return instance;
}
}
双重检测锁,它比其它方法更加安全,因为它会在锁定单列类之前检查一次单列的创建,在对象创建前再检查一次检查。
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;
}
}
但其实上面的方法也都不是绝对安全的,在Java中,最佳创建单列的方式是使用枚举类。
枚举类型本质上就是单列的,因此JVM会处理创建单列所需的大部分工作。这样无需再处理同步对象创建和提供工作了,还能避免初始化相关的问题。
public enum MySingletonEnum {
INSTANCE;
public void doSomethingInteresting() {
}
}
2.使用JAVA EE实现单列模式
2.1单列Bean
1.只需注解@Singleton添加到类上就可以将其为单列Bean
2.其将类标记为一个单列EJB,容器会处理该单列的创建和使用
3.如果再服务器上执行该EJB,那么你是不会看到来自单列的日志输出的,这是因为带有注解@PostConstruct的方法并没有被调用。
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import java.util.logging.Logger;
@Singleton
public class CacheSingletonBean8 {
private Map<Integer, String> myCache;
@PostConstruct
public void start() {
Logger.getLogger("MyGlobalLogger").info("Started!");
myCache = new HashMap<Integer, String>();
}
public void addUser(Integer id, String name) {
myCache.put(id, name);
}
public String getName(Integer id) {
return myCache.get(id);
}
}
@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:
Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
从Java EE5规范开始,Servlet中增加了两个影响Servlet生命周期的注解,@PostConstruct和@PreDestroy,这两个注解被用来修饰一个非静态的void()方法。
在默认情况下,javaEE中的单列是延迟初始化的,只在需要实例并且是首次访问时才创建它,要想确保启动时就创建实例,不需要任何延迟即可访问到单列,可在类上加@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
@Singleton
public class CacheSingletonBean9 {
private Map<Integer, String> myCache;
@PostConstruct
public void start() {
Logger.getLogger("MyGlobalLogger").info("Started!");
myCache = new HashMap<Integer, String>();
}
public void addUser(Integer id, String name) {
myCache.put(id, name);
}
public String getName(Integer id) {
return myCache.get(id);
}
}
2.2 确定启动顺序
如果上例创建的单列依赖别的资源该怎么办,该如何等待其它资源就绪呢?
可以使用@DependsOn()注解解决这个问题
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)
@Singleton
public class CacheSingletonBean12 {
private Map<Integer, String> myCache;
@EJB
MyLoggingBean loggingBean;
@PostConstruct
public void start() {
loggingBean.logInfo("Started!");
myCache = new HashMap<Integer, String>();
}
@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);
}
}
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import java.util.logging.Logger;
@Startup
@Singleton
public 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);
}
}
一个单列Bean可能会依赖于其他一系列Bean的初始化,这种情况下,可在@DependOn注解中,指定多个Bean
@DependOn({"Bean1","Bean2"})
3.管理并发
Java EE 提供了两种并发管理:容器管理并发与Bean管理并发
在容器并发中,容器负责处理读写访问相关的一切事宜,而bean管理并发则需要开发者使用同步等传统的java方法来处理并发。可以通过ConcurrencyManagementType.BEAN注解进行显式声明
默认情况下,java EE使用的是容器管理并发,不过可以ConcurrencyManagementType.CONTAINER注解进行显式声明。
3.2 使用@locktype注解管理并发
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)
@Singleton
public class CacheSingletonBean12 {
private Map<Integer, String> myCache;
@EJB
MyLoggingBean loggingBean;
@PostConstruct
public void start() {
loggingBean.logInfo("Started!");
myCache = new HashMap<Integer, String>();
}
@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的业务方法的访问:分别是LockType.WRITE和LockType.READ
对于前者来说,当方法被调用的时候,其它客户端是无法访问bean的;对于后者来说,它允许对方法的并发访问,并且不会对其它客户端锁定bean
还可以在类级别使用LockType,这样会应用到所有没有显示定义的方法上去为Write。
3.3 并发超时访问
mport javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.DependsOn;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.AccessTimeout;
import java.util.Map;
import javax.ejb.EJB;
import java.util.HashMap;
import javax.ejb.Lock;
import javax.ejb.LockType;
import java.util.concurrent.TimeUnit;
@Startup
@DependsOn("MyLoggingBean")
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
@Singleton
@AccessTimeout(value = 120000)
// default in milliseconds
public class CacheSingletonBean13 {
private Map<Integer, String> myCache;
@EJB
MyLoggingBean loggingBean;
@PostConstruct
public void start() {
loggingBean.logInfo("Started!");
myCache = new HashMap<Integer, String>();
}
@AccessTimeout(value = 30, unit = TimeUnit.SECONDS)
@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);
}
}
@AccessTimeout 默认是毫秒单位
还可以在整个类上使用该注解,将其应用到所有没有显示定义的方法上去。