单例模式就是指一个类只能产生一个实例,所有操作都围绕这一个实例进行。
单例模式的作用是:当一个类被频繁的用到,并且使用的用途几乎都是一样的,那么就可以是用单例模式,比如说windows上的任务管理器或者回收站,用途都是一样的,而且我们绝对不可能打开两个任务管理器。还有我们用到的数据库连接池,如果是简单地把一个connection对象封存在单例对象中,这样是错误的,因此连接池里有多个链接可以用,如果使用Singleton,那在WEB访问时,就只能用一个数据库链接。但是链接池可以使用单例模式,初始化的时候创建譬如100个connection对象,然后再需要的时候提供一个,用过之后返回到pool中,我们用单例模式,是保证连接池有且只有一个。
单例模式可以避免创建多个对象,节省内存空间,加快对象访问速度。应用的场景一般是在:
(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。
单例和多例的选择:用单例,是因为没必要每个请求都新建一个对象,这样子既浪费CPU又浪费内存; 用多例,是为了防止并发问题,即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理;。用单例和多例的标准只有一个: 当对象在应用中状态会改变时,用多例,否则单例。对于struts2来说,action必须用多例,因为action本身含有请求参数的值,即可改变的状态, 而对于我们常用的service、dao还有spring框架中的bean来说,则可用单例,因为一般情况下作用是不会改变的。
单例模式的实现主要有三种:懒汉式、饿汉式、登记式。
懒汉式
懒汉式是在实例被调用的时候才创建。
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
通过私有化构造方法,将实例声明为static,就保证了只有一个实例,不过这种方式通过Java的反射机制得到类,也能够再创建实例,所以是不安全的单例模式。另外在多线程状态下也不安全,也可能出现多个实例。比如线程A和线程B,当A执行完了判断实例是否为空,还没有来得及创建实例,CPU就被线程B抢去了,由于A还没有创建实例,所以此时singleton还是空,所以B创建了实例,然后时间片又轮转给A使用,A也创建了实例,那这样就创建了两个实例,违背了单例模式原则。
通过反射得到类,生成实例:
Class<Singleton> singleton = Singleton.class;
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = constructor.newInstance();
singleton1.play();
Singleton singleton2 = constructor.newInstance();
singleton2.play();
通过反射得到实例的解决方法是在构造方法中添加实例个数控制,构造方法每调用一次,count++,当count>1则抛出异常。
public class Singleton {
private static int count = 0;
private Singleton() throws Exception {
synchronized(Singleton.class) {
if (count > 0){
throw new Exception("创建了两个实例");
}
count++;
}
}
private static Singleton singleton = null;
public static Singleton getInstance() throws Exception {
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
public void play(){
System.out.println("执行!");
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<Singleton> singleton = Singleton.class;
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = constructor.newInstance();
singleton1.play();
Singleton singleton2 = constructor.newInstance();
singleton2.play();
}
}
另一种解决懒汉式线程不安全的方法是给getInstance()方法加锁,保证每次只有一个线程访问。这样:
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
但这样也有坏处,因为每个线程都要以此等待这个锁,singleton为空只是少数情况,所以这样造成了线程多余的等待,浪费了时间。
另一种方法是在new Singleton()的时候加锁,但这样同样有上述线程A和线程B先后判断了singleton为空然后以此创建实例的问题,所以要在new的时候再判断一次,进行双重判断。
public static SingleDemo getInstance(){
if(s == null){
synchronized(SingleDemo.class){
if(s == null){
s = new SingleDemo();
}
}
}
return s;
}
保证了每次只有一个线程得到锁,并且得到后还要再进形判断,解决了A和B的问题。不过java存在啊指令重排序问题。
instance = new Singleton2()其实可以分为下面的步骤:
1.申请一块内存空间;
2.在这块空间里实例化对象;
3.instance的引用指向这块空间地址;
指令重排序存在的问题是:
对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?
加上volatile关键字,因为volatile可以禁止指令重排序。
private static volatile Singleton2 instance;
还有一种解决办法是使用静态内部类:
public class Singleton {
private Singleton(){}
public static class innerGetInstance{
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return innerGetInstance.singleton;
}
}
通过static在类加载的时候就创建实例,类似饿汉式。
饿汉式
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton1 {
private Singleton1() {}
private static final Singleton1 single = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}
实例在还没有被用到之前就已经创建完成,所以不存在创建多个实例的问题。相对于懒汉式,饿汉式会提前加载,如果加载的类在没有用到之前常驻内存比较耗费资源,或者我不想让他提前加载,那还是使用懒汉式更合适。
登记式
登记式主要解决懒汉式和饿汉式不能被继承的问题,因为这两个方式的构造方法时私有的,Java中被final修饰的类和构造方法私有的类都不允许被继承,因为子类的构造方法必须要调用父类的构造方法,而私有的方法和属性只能自己的类访问。
public class RegSingleton {
//登记簿,用来存放所有登记的实例
private static Map<String,RegSingleton> map = new HashMap<String,RegSingleton>();
//受保护的默认构造方法
protected RegSingleton(){
}
//在类加载时添加一个实例到登记簿
static {
RegSingleton regSingleton = new RegSingleton();
map.put(RegSingleton.class.getName(),regSingleton);
}
//静态工厂方法,返回指定登记对象的唯一实例
//对于已经登记的直接取出返回,对于还未登记的先登记,然后取出返回
public RegSingleton getInstance(String name) {
if (name == null){
name = RegSingleton.class.getName();
}
if (map.get(name) == null){
try {
map.put(RegSingleton.class.getName(),(RegSingleton) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
}
因为构造方法是protected的,要将这个单例类单独放到一个包里,防止其他类调用构造方法。
使用一个map来存储各个子类的实例。