对某些类来说,其对象在内存中只需要一个,或者必须保证其被访问到的对象始终是同一个,拥有固定的属性和方法,用来完成某些固定而统一的操作。而在开发这些类的时候,如果不加以设计,则仍然可能被使用者多次创建产生多余的或不统一的对象,无意义地在内存中产生多个副本或产生多个不一致的对象。因此我们需要对类进行设计,实现单例模式,即保证某类只有单个实例的模式,实现单例模式有以下几种方式。
一、饿汉式
为什么叫饿汉式?饿汉式从字面理解就是一有食物就迫不及待地吃东西的状态。实际上,饿汉式的单例模式实现便是在类的加载中便马上莫不急待地对本类对象进行创建并作为该类的私有静态属性,由于是静态的,故一个类有且仅有这么一个对象属性。在饿汉式的定义中,最重要的还是屏蔽外部对构造方法的调用,即将构造函数设置成类私有的,这样一来,该构造方法(new object() )就无法被外部类调用了,也就无法在外部通过构造方法来创建对象,只能通过类内部定义的public方法获取唯一的私有静态对象。由于jvm对每个类只加载一次,而饿汉式的static对象是在类加载的过程中就已经创建,故是线程安全的。
public class Hungry {
private Hungry(){}//重写构造方法将其私有化屏蔽外部调用
private static Hungry hungry=new Hungry();//调用私有构造方法,创建私有静态对象
public static Hungry getHungry() {//定义静态类方法用以返回唯一的私有静态对象
return hungry;
}
}
public class HungryTest {
public static void main(String args[]){
// Hungry hungry= new Hungry();//错误写法,将出现下图中的错误提示
Hungry hungry1= Hungry.getHungry();
Hungry hungry2= Hungry.getHungry();
System.out.println(hungry1==hungry2);//打印结果为true
}
}
也有一种写法,称为“使用静态代码块实现单例模式”,我认为没有必要单独出来,实际上和饿汉式没有什么区别,静态代码块也是在类加载的时候执行一次。只是一句话分成两部分来写罢了。如下所示。
public class Hungry {
private Hungry(){}//重写构造方法将其私有化屏蔽外部调用
private static Hungry hungry=null;//创建私有静态对象的空引用
static{//静态代码块,也在类加载时执行
hungry=new Hungry();
}
public static Hungry getHungry() {//定义静态类方法用以返回唯一的私有静态对象
return hungry;
}
}
二、懒汉式
为什么叫懒汉式?这个命名和饿汉式是相对应的,饿汉式在类加载的时候便会迫不及待地创建对象,因为他很饿。而懒汉则不会这么急迫,和饿汉式相同的是仍然会私有化构造函数屏蔽外部调用,而且也有一个私有静态的对象引用,但是在类加载的时候这个引用是空的,构造函数不会立刻被调用,很懒,但是这也解决了饿汉式可能存在的对象没被使用而浪费内存的问题。等到获取对象的方法被调用时才会尝试创建该对象,若该引用为空则调用私有构造函数创建对象并返回,若该引用已不为空则直接将引用指向的对象返回。因为懒汉式并未在类加载时就创建对象,故可能存在线程不安全的问题,如线程1执行到“ if(lazy==null) ”但未执行到“ lazy=new Lazy() ”语句的时候,线程2也访问了getLazy()方法并且执行“ if(lazy==null) ”,此时因为线程1未完成对象创建,因此线程2的if语句也判断为true并继续执行if语句底下的方法块,两个线程各自创建了一个对象,从而线程1和线程2访问到的将不是同一个对象。
public class Lazy {
private Lazy(){}//
private static Lazy lazy;
public static Lazy getLazy(){
if(lazy==null){
lazy=new Lazy();
}
return lazy;
}
}
懒汉式(1):针对线程安全问题的改进是给getLazy加锁,保证该方法不会被不同线程同时使用即可。但是这样一来,即便唯一的lazy对象已经创建好了,每次获取该对象都还是需要进行互斥锁的相关操作,这带来了效率的降低。
public class Lazy {
private Lazy(){}//
private static Lazy lazy;
public static synchronized Lazy getLazy(){//加锁
if(lazy==null){
lazy=new Lazy();
}
return lazy;
}
}
懒汉式(2):针对懒汉式(1)的效率问题,后来提出了双重检查单例写法。这种写法,在lazy实例已经创建了的情况下,会直接返回lazy而不会进行互斥锁的相关操作。我们可以想象,在程序运行之初,可能有多个线程(假设两个线程1和线程2)同时通过了第一个if检测,但是线程1率先拿到锁并创建lazy对象,而线程2则被阻塞,当线程1创建完对象释放锁后,线程2会在第二个if检测时发现线程1已经创建了对象,所以不会创建新的lazy对象覆盖线程1已经创建好的对象。这么做既避免了程序运行之初可能出现的创建多个对象的问题,又避免了后续每次获取对象都进行互斥锁的相关操作。这种写法被视为比较完美的写法,既实现了单例,也不浪费内存,同时也无线程安全问题,但是比较复杂,所以一般来说推荐饿汉式写法。
public class Lazy {
private Lazy(){}//
private static Lazy lazy;
public static Lazy getLazy(){//加锁
if(lazy==null){//多个线程在对象未创建时访问到该方法并通过了这个if检测
synchronized(Lazy.class){//保证始终只有一个线程会执行下列操作
if(lazy==null){ //若不检测,则线程1的锁释放后线程2也还是会创建新对象
lazy=new Lazy();//虽然同时进入该方法并都通过第一个if,最终只会有一个对象被创建
}
}
}
return lazy;//若访问该方法时对象已经创建直接返回
}
}
三、静态内部类实现
针对饿汉式带来的内存浪费的问题,又有人提出了静态内部类实现单例模式的方法。我们知道,内部类可以调用定义内部类的那个类的私有方法。而在加载一个类的时候,并不会同时加载其内部类,只有在使用到该内部类时才会被加载,由于静态内部类也只会被加载一次,故不管有多少线程同时执行getInstance方法,我们也无需上锁。因为jvm会保证该内部类只被加载一次,也即是利用jvm保证类加载线程安全的特性,从而保证了内部类加载时对象创建的线程安全。这也是一个比较完美的写法。
public class InnerCla {
private InnerCla(){}//屏蔽外部调用构造函数
private static class InnerClaHolder{//该内部类不和InnerCla类同时加载
private static InnerCla innerCla =new InnerCla();//创建InnerCla对象作为InnerClaHolder内部类的私有静态属性
}
public static InnerCla getInnerCla(){
return InnerClaHolder.innerCla;//当此处尝试获取内部类InnerClaHolder的私有静态属性时,该内部类才被加载
}
}
四、枚举实现
上述三种实现单例的方法,总体思想都是屏蔽内部构造方法,然后定义一个类的对象作为类或内部类的私有静态属性,也就是说,类的对象唯一地存在于类或内部类的属性之中(因为该对象是静态的,可以说就是类的属性而不是对象的属性所以不会出现对象中包含对象的情况)。我们知道,枚举类型也可以定义属性和方法,枚举类型中的枚举项就是枚举类的对象且是常量,枚举类型默认的私有构造方法会被自动调用以加载枚举项。也就是说,枚举类型天然地就是一种类中包含对象的结构,并且自动帮我们完成对象的创建。不过遗憾的是,其本质也只是一种饿汉式的实现,枚举类在加载的时候就会调用构造函数创建枚举项对象,只不过在写法上要简单得多。
public enum myenum{
INSTANCE;//实际上等于INSTANCE(),会调用无参构造方法
//可以重写构造方法,但必须是private
//下面可添加类属性和方法
}
public static void main(String[] args){
System.out .println(myenum.INSTANCE.hashCode());
}