顾名思义,单例模式的意思就是只有1个实例的对象。就像天天上的太阳和月亮只有一个一样。
单例模式有很多种写法,在这里我们一一对比:
//懒汉模式
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {};
public static Singleton1 getInstance()
{
if(instance == null)
{
instance = new Singleton1();
}
return instance;
}
}
懒汉模式是指在需要的时候再去创建对象。我们可以看到它的成员属性中有一个静态的实例对象,然后对外隐藏了自己的构造方法,是的我们只能通过静态方法getInstance()来获取它的对象。而在getInstance()中,判断了实例对象是否已经被初始化过。
但这个解法的缺点就是只能在单线程模式下工作,在多线程模式下会失效。
我们测试一下,测试代码如下:
public static void main(String[] args) {
//线程1尝试获取一个实例
new Thread(new Runnable() {
@Override
public void run() {
Singleton1 s1 = Singleton1.getInstance();
System.out.println(s1.hashCode());
}
}).start();
//线程2尝试获取一个实例
new Thread(new Runnable() {
@Override
public void run() {
Singleton1 s2 = Singleton1.getInstance();
System.out.println(s2.hashCode());
}
}).start();
}
而结果是不稳定的:
会出现这样两种情况,所以在多线程情况下不推荐使用。
public class Singleton2
{
private static Singleton2 instance = new Singleton2();
private Singleton2() {};
public static Singleton2 getInstance()
{
return instance;
}
}
饿汉模式解决了线程安全的问题,但是我们可以看到,因为instance对象是静态的,在类初始化之后,这个对象就已经存在了。可能我们并不需要它,但它仍然会占用内存。
public class Singleton3 {
private Singleton3() {};
static Lock lock = new ReentrantLock();
private static Singleton3 instance = null;
private static Object objSingleton3 = new Object();
public static Singleton3 getInstance()
{
synchronized (objSingleton3) {
if(instance == null)
{
instance = new Singleton3();
}
}
return instance;
}
}
这样相比懒汉模式解决了线程安全问题,也不会在不使用的情况下生成对象,但加锁的过程也是较为耗时的。
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {};
synchronized public static Singleton4 getInstance()
{
if(instance == null)
{
instance = new Singleton4();
}
return instance;
}
}
这种方式和上面的是一样的,就是写法不一样,都是效率比较低。
然后还有一种比较类似的写法,但却是线程不安全的,
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {};
public static Singleton4 getInstance()
{
if(instance == null)//-------①
{
synchronized (Singleton4.class) {
instance = new Singleton4();
}
}
return instance;
}
}
这里我们只锁了代码块,但在多线程情况下,线程A可能在①处让出cpu,然后线程B仍然可以获得锁,然后创建对象。当线程A重新拿到CPU的时候,还会继续创建新的对象,这样就不是线程安全的 了。
public class Singleton4 {
private static volatile Singleton4 instance;-------③
public volatile static int a=0;
private Singleton4() {};
public static Singleton4 getInstance()
{
if(instance == null)------①
{
synchronized (Singleton4.class) {
if(instance == null)-------②
{
instance = new Singleton4();
}
}
}
return instance;
}
}
在我们进行初始化的时候,检查了两次instance对象是否为空。代码①处的检查是因为要判断instance是否已经存在,避免不必要的上锁。代码②处的检查是因为为了解决并发情况下是否在加锁后再次确认。这种方式相较饿汉式提高了效率,在用到的时候进行初始化,节省了空间,相对于懒汉式能够保证线程安全。而代码③处的volatile关键字保证了instance对象的可见性。避免因为jvm底层命令执行顺序的原因导致出现意料之外的错误。
public class Singleton5 {
private Singleton5() {};
public static Singleton5 getInstance()
{
return Innerclass.instance;
}
private static class Innerclass{
private static final Singleton5 instance = new Singleton5();
}
}
这个模式结合了饿汉式和懒汉式的优点。它只会在我们第一次使用这个Innerclass的时候,会调用它的构造函数去创建这个属性,如果我们不使用Innerclass,就不会去初始化instance 。
6.枚举法
public enum Singleton6 {
Singleton6;
}
使用的时候直接Singleton6.Singleton6就可以拿到对象。而且枚举类有且只有私有构造器。
而且对序列化也有很好的支持。
总结一下:
单例模式三大要素:
1.私有构造函数:确保不会被外部类访问
2.自己持有,或者通过内部类持有自己类型的实例对象
3.对外暴露一个获取对象的静态方法
对比一下几种方式:
综上,在一般开发的时候会比较常用4/5,但也不是唯一的,在不同的场景中可以使用不同的方法才使最好的。