对于系统中的某些类来说,只有一个实例很重要,例如,一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性且易于访问,我们可以通过单例模式来实现,这就是单例模式的动机所在。
单例模式
单例模式(Singleton Pattern):必须确保类只有一个实例,而且自行实例化向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:
1.确保某一个类只能有一个实例,禁止外部直接使用new来创建对象
2.必须是类自行创建这个实例
3.必须自行向整个系统提供这个实例
伪代码
//按钮类
public class Button{
//私有构造函数,设置为private禁止外部直接new创建对象
private Button(){...}
//私有静态成员变量,存储唯一实例
private static Button btn = null;
//公有静态成员方法,返回唯一实例
public static getButton(){
if(btn == null){
btn = new Button();
}
return btn;
}
}
//客户端
public class Client{
public static void main(String[] args){
Button myBtn = Button.getButton();
}
}
饿汉式、懒汉式、双重检查锁定
在多线程情况下,上述代码将不再保证只产生唯一实例,或者说是线程不安全的。例如,有两个线程A,B同时访问getButton()方法,线程A先抢到CPU时间片执行,该线程在执行到初始化对象但还未完成时发生了CPU时间片的切换。由于实例未创建完成,线程B执行时通过了判断语句并创建了实例对象,等线程B结束后,线程A拿到时间片继续执行上次执行未完成的任务,又创建了一个实例对象,导致最终创建了多个实例,违背了单例模式。
1.饿汉式单例类
class EagerSingleton{
//类加载时instance完成实例化,确保了单例
private static final EagerSingleton instance = new EagerSingleton();
//私有构造函数
private EagerSingleton(){...}
//返回实例
public static EagerSingleton getInstance(){
return instance;
}
}
instance 在类装载时就实例化,确保了只有一份。但是没有懒加载,可能会浪费内存。
final
1)如果某个成员变量用final修饰,JVM规范做出如下明确的保证:一旦该变量的引用对象对其他线程可见,则其final成员也必须已经完成初始化了。
2)final关键字禁止cpu指令集重新排序,来保证对象的安全发布,防止对象引用被其他线程在对象被完全构造完成前拿到并使用。
3)final修饰的成员变量还有另一个特性,如果是基本类型,则值不能改变。如果是引用类型,则地址不能改变。
final关键字保证了多线程下变量的数据安全性。
2.懒汉式单例类
class LazySingleton{
private static LazySingleton instance = null;
private LazySingleton(){...};
//加同步锁
public synchronized static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
懒汉式单例类在getInstance()前加了synchronized关键字
进行同步锁,以处理多线程同时访问的问题。虽然解决了问题,但是每次调用getInstance()方法时都需要进行线程锁定判断进而线程阻塞,在多线程高并发访问环境中,会导致系统性能大大下降。如何既解决了线程安全问题又可以不影响系统性能呢?
我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。
class LazySingleton{
private static LazySingleton instance = null;
private LazySingleton(){...};
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}
}
但是这样又存在线程安全问题了,需进行进一步改造。
3.双重检查锁定(Double-Checking Locking)
class LazySingleton{
private static volatile LazySingleton instance = null;
private LazySingleton(){...};
//双重检查
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
双重检查锁单例写法解决了线程安全问题和性能问题,使用双重锁定检查实现懒汉式单例,需要在静态变量前加修饰符volatile,保证变量在多线程之间的可见性以及指令的有序性。
静态内部类单例
双重检查锁单例写法虽然解决了线程安全和性能问题,但终归要使用synchoronized
,对性能还是存在一定影响。
饿汉式单例不能实现延迟加载,不管将来用不用终究要占用内存,可能会带来内存浪费问题。
静态内部类
public class InnerClassSingleton{
//私有构造方法
private InnerClassSingleton(){}
//静态内部类
private static class LazyHolder{
private static final InnerClassSingleton instance = new Singleton();
}
//如果调用getInstance方法,则不会加载静态内部类
public static InnerClassSingleton getInstance(){
return LazyHolder.instance;
}
}
利用Java语法的特点,默认不加载内部类。只有调用getInstance方法时才会加载内部类完成类的初始化,同时JVM确保了static和final修饰的变量只有一份。
枚举单例
静态内部类无法防止利用反射来重复构建对象【后续学习…】
public enum EnumSingleton{
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}