1.单例模式是什么?
单例模式是设计模式中最简单的形式之一,也是一种常见的软件设计模式。这一模式的目的是使得类中的一个对象为系统中的唯一实例并且提供一个全局访问点,这里就要解决如何绕过常规的构造方法,提供一种机制来保证一个类只有一个实例?想要解决这以问题就需要从客户端对其进行实例化开始,因为客户端在调用某一个类进行实例化的过程中,它不会考虑这个类是否只能有一个实例的问题,所有在设计这个类的过程中就需要用一种只允许生成对象类的唯一实例的机制,阻止所有想要生成对象的访问。
从上面的单例模式结构图中可以看出,在Singleton类中定义了一个GetInstance()方法,它是一个静态方法,在这个方法中允许客户端访问并且返回唯一实例。
将上面的结构图转换成代码
public class Singleton
{
private static Singleton instance;
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//此方法是全局获取实例的唯一入口
public static Singleton GetInstance()
{
//对实例进行判断,若不存在则new一个新的实例(对实例化的数量进行控制,使实例化唯一)
if(instance == null)
{
instance = new Singleton();
}
return instance;
}
}
客户端中调取Singleton类
public class Main
{
public static void main(String[] args)
{
Singleton s1 = Singleton.GetInstance();
Singleton s2 = Singleton.GetInstance();
//通过比较s1和s2的引用地址是不是相同,来确定s1和s2是否为同一对象
if(s1 == s2)
{
System.out.println("两个对象是相同的实例");
}
else
{
System.out.println("两个对象不是相同的实例");
}
}
}
2.在多线程中的单例模式
如果代码所在的进程中有多个线程在同时运行,而且这些线程有可能同时运行创建新实例的代码,如果出现这种情况在调用GetInstance()方法时就会创建多个实例,也就是以上的写法在多线程的情况下是不安全的。
1)锁定方法
在方法调用中加锁,虽然线程安全了但是每次调用GetInstance()方法时都需要同步,会影响性能,毕竟绝大多数的情况下是不需要同步的。
public class Singleton
{
private static Singleton instance;
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//此方法是全局获取实例的唯一入口
public static synchronized Singleton GetInstance()
{
//对实例进行判断,若不存在则new一个新的实例(对实例化的数量进行控制,使实例化唯一)
if(instance == null)
{
if(instance == null)
{
instance = new Singleton();
}
}
return instance;
}
}
2)双重锁定
在GetInstance()方法中做了两次判空,确保只有第一次调用时才会同步,这样在保证线程安全的情况下也可以避免每次都同步造成的性能损耗。
public class Singleton
{
private static Singleton instance;
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//此方法是全局获取实例的唯一入口
public static Singleton GetInstance()
{
//对实例进行判断,若不存在则new一个新的实例(对实例化的数量进行控制,使实例化唯一)
if(instance == null)
{
synchronized (instance)
{
if(instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
3)静态初始化
利用了Java的类加载机制来保证初始化时只有一个线程,所以线程也是安全的同时也没有性能的损耗。
public class Singleton
{
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//添加静态内部类,创建实例
private static class staticSingleton
{
private static final Singleton instance = new Singleton();
}
//此方法是全局获取实例的唯一入口
public static final Singleton GetInstance()
{
return staticSingleton.instance;
}
}
3.经典的单例模式
1)懒汉式
懒汉式的特点是延迟加载,只有当调用GetInstance()方法时才会去创建实例。但是懒汉式本身是非线程安全的,可以使用双重锁定的方法保证线程的安全。
public class Singleton
{
//设立静态变量
private static Singleton instance = null;
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//此方法是全局获取实例的唯一入口
public static Singleton GetInstance()
{
//对实例进行判断,若不存在则new一个新的实例(对实例化的数量进行控制,使实例化唯一)
if(instance == null)
{
synchronized (instance)
{
if(instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
2)饿汉试
饿汉式就是类一旦加载就创建一个实例,保证在调用GetInstance()方法的时候,实例就已经存在了。饿汉式天生就是线程安全的,可以直接用于多线程中。
public class Singleton
{
//设立静态变量,直接创建实例
private static Singleton instance = new Singleton();
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//此方法是全局获取实例的唯一入口
public static Singleton GetInstance()
{
return instance;
}
}
3)登记式
登记式实际上就维护了一组单例类的实例,将这些实例存放在一个Map(登记本)中,对于已经登记过的实例直接返回,对于没有登记的实例,先登记再返回。其实登记式的内部还是使用了饿汉式的方式,在静态代码块中就创建一个实例并且登记在Map中,因此登记式也是线程安全的,可以直接用于多线程中。
import java.util.HashMap;
import java.util.Map;
public class Singleton
{
private static Map<String, Singleton> map = new HashMap<String, Singleton>();
static
{
Singleton instance = new Singleton();
map.put(instance.getClass().getName(), instance);
}
//重写构造方法,并且使用private修饰,这样在外界就不能通过常规方式创建实例
private Singleton()
{
}
//此方法是全局获取实例的唯一入口
public static Singleton GetInstance(String name)
{
if(name == null)
{
name = Singleton.class.getName();
}
if(map.get(name) == null)
{
try
{
map.put(name, (Singleton) Class.forName(name).newInstance());
}
catch(Exception e)
{
e.printStackTrace();
}
}
return map.get(name);
}
}
4.总结
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。