饿汉式
为何叫作饿汉式,意思是很饥饿,那么就会一开始就准备好,以防之后吃不饱,名字由此而来。代码如下
class HUNGRYMAN{
//这里实例化方法要设置成私有的,以防外部直接new对象,破坏单例
private HUNGRYMAN(){
}
//这里即为一开始就创建好对象,需要调用的时候,直接返回,不需要新创建
private static HUNGRYMAN INSTANCE = new HUNGRYMAN();
//调用方法返回实例对象
public static HUNGRYMAN getInstance(){
return INSTANCE;
}
}
class Main{
public static void main(String[] args) {
//我们在这里采用哈希值的方式来判断是否相等
HUNGRYMAN instance1 = HUNGRYMAN.getInstance();
HUNGRYMAN instance2 = HUNGRYMAN.getInstance();
System.out.println("instance1的哈希值为" + instance1.hashCode() + " " + "instance2的哈希值为" + instance2.hashCode());
System.out.println("两者是否相等:" + (instance1 == instance2));
}
}
//输出:instance1的哈希值为366712642 instance2的哈希值为366712642
//两者是否相等:true
- 好处:显而易见,我们在一开始就创建好了对象,就不会涉及到线程安全的问题(线程安全是在并发创建对象的时候才会发生,但是这里我们在类生成的时候就创建好了对象,所以不会发生线程安全的问题)。
- 坏处:坏处也是显而易见,因为我们在类初始化的时候就创建好了类对象,所以很可能会出现这种情况:我们需要使用单例的时间其实不长,但是这个类存活的时间很长,假如这个时候类里面有一些别的对象,比如很长的字节数组,那么就会一直占用资源。
由于上述的情况,诞生了懒汉式:
懒汉式
与饿汉式相对,懒汉式只有在需要使用的时候才会创建,也就是说可以实现“延时创建”,代码如下:
class LAZYMAN{
//同样,这里的实例方法也需要设置成私有的,防止其他类破坏单例
private LAZYMAN(){
}
//这里定义一个LAZYMAN类型的变量instance,但是并没有创建对象
private static LAZYMAN INSTANCE;
//在执行getInstance方法的时候才会创建对象
public static LAZYMAN getInstance(){
//判空时初始化,否则直接返回对象
if(INSTANCE == null){
INSTANCE = new LAZYMAN();
}
return INSTANCE;
}
}
class Main{
public static void main(String[] args) {
LAZYMAN instance1 = LAZYMAN.getInstance();
LAZYMAN instance2 = LAZYMAN.getInstance();
System.out.println("instance1的哈希值为" + instance1.hashCode() + " " + "instance2的哈希值为" + instance2.hashCode());
System.out.println("两者是否相等:" + (instance1 == instance2));
}
}
//输出:instance1的哈希值为366712642 instance2的哈希值为366712642
//输出:两者是否相等:true
- 好处:实现了延迟加载,这里不需要考虑资源消耗的问题,也就是说,只有当执行 getInstance() 方法的时候才会创建对象,避免了资源的消耗。
- 坏处:线程不安全,因为我们在并发的执行 getInstance() 的时候并没有考虑线程线程安全的问题,这势必会导致出现问题。
由此产生了双重校验锁(DCL懒汉式)的单例模式实现方式
DCL懒汉式
线程安全的懒汉模式,直接来看代码:
class LAZYMAN{
private LAZYMAN(){
}
//这里加入volatile,是为了防止实例对象创建的时候出现指令重排
/*
对象创建过程(非原子性):1.分配内存空间 2.执行构造方法,初始化对象
3.将对象指向这个内存空间。这里如果没有使用volatile修饰的话,会导致
执行顺序不是123,导致对象创建还没有完全创建完毕,但是外部
执行到if(instance == null)这里的时候,已经不为空了,会直接返回,导致
返回版初始化状态的实例,发生错误。
*/
private static volatile LAZYMAN INSTANCE;
//在执行getInstance方法的时候才会创建对象
public static LAZYMAN getInstance(){
//如果对象没有创建过,先把整个class锁住,等到第一个拿到锁的线程释放之后其他的线程在继续执行
if(INSTANCE == null){
synchronized(LAZYMAN.class){
if(INSTANCE == null){
INSTANCE = new LAZYMAN();
}
}
}
return INSTANCE;
}
}
class Main{
public static void main(String[] args) {
LAZYMAN instance1 = LAZYMAN.getInstance();
LAZYMAN instance2 = LAZYMAN.getInstance();
System.out.println("instance1的哈希值为" + instance1.hashCode() + " " + "instance2的哈希值为" + instance2.hashCode());
System.out.println("两者是否相等:" + (instance1 == instance2));
}
}
- 好处:线程安全,看起来是十分完美的方法,又有延迟加载的功能,又线程安全
- 坏处:我们知道,有个东西叫做反射,可以直接获取到类对象,可以操作私有变量的值,可以直接在外部创建实例对象,所以说会破坏单例模式,也就是说,这种方法也不是绝对安全的。
关于 volatile 和 synchronized ,在另外的文章中会探讨。
这里说一下如何通过反射来破坏:
class Main{
public static void main(String[] args) throws Exception {
LAZYMAN instance1 = LAZYMAN.getInstance();
Constructor<LAZYMAN> constructor = LAZYMAN.class.getDeclaredConstructor();
//无视私有构造器
constructor.setAccessible(true);
//直接创建
LAZYMAN instance2 = constructor.newInstance();
System.out.println("instance1的哈希值为" + instance1.hashCode() + " " + "instance2的哈希值为" + instance2.hashCode());
System.out.println("两者是否相等:" + (instance1 == instance2));
}
}
//输出:instance1的哈希值为366712642 instance2的哈希值为1829164700
//两者是否相等:false
借助内部类
class INNERSINGLE{
private INNERSINGLE(){
}
//内部类,调用的时候才进行加载
private static class SINGLEINNNER{
private static INNERSINGLE instance = new INNERSINGLE();
}
//调用方法:getInstance()
public static INNERSINGLE getInstance(){
return SINGLEINNNER.instance;
}
}
class Main{
public static void main(String[] args) throws Exception {
INNERSINGLE instance1 = INNERSINGLE.getInstance();
INNERSINGLE instance2 = INNERSINGLE.getInstance();
System.out.println("instance1的哈希值为" + instance1.hashCode() + " " + "instance2的哈希值为" + instance2.hashCode());
System.out.println("两者是否相等:" + (instance1 == instance2));
}
}
//输出:instance1的哈希值为366712642 instance2的哈希值为366712642
//两者是否相等:true
- 优点:内部类与外部类不在同一时间加载,只在调用 getInstance() 方法的时候才会去初始化INSTANCE,故而节省了内存空间。可以认为是饿汉式的改进版。所以会保证线程安全,同时也做到了延迟加载。
枚举
枚举类默认是单例模式的
public enum ENUMSINGLE{
INSTANCE;
public ENUMSINGLE getInstance(){
return INSTANCE;
}
}
class Main{
public static void main(String[] args) throws Exception {
ENUMSINGLE instance1 = ENUMSINGLE.INSTANCE;
ENUMSINGLE instance2 = ENUMSINGLE.INSTANCE;
System.out.println("instance1的哈希值为" + instance1.hashCode() + " " + "instance2的哈希值为" + instance2.hashCode());
System.out.println("两者是否相等:" + (instance1 == instance2));
}
//输出:instance1的哈希值为366712642 instance2的哈希值为366712642
//两者是否相等:true