单例模式常见的有懒汉式和饿汉式;这样划分是按照单例的初始化的时机划分的,在未使用单例时,就不得不初始化好,如此的迫不及待被称作饿汉式。只有在使用单例的时候,才进行初始化,如此的不上进被称作懒汉式。至于这个“汉”字,我想大多的程序员都是男孩纸吧。
懒汉式
package com.duofei;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* 懒汉式
*/
public class Singleton1 {
private static volatile Singleton1 instatnce;
private static Lock lock = new ReentrantLock();
/*
** 私有化构造函数,单例必须实现的步骤
** @author duofei
*/
private Singleton1(){}
/*
** 最基础的懒汉式,但存在线程安全问题
** @author duofei
*/
public static Singleton1 getInstance(){
if(instatnce == null){
instatnce = new Singleton1();
}
return instatnce;
}
/*
** 同步块解决并发问题,由于每次只能有一个调用者进入,效率低。
** @author duofei
*/
public static synchronized Singleton1 getInstance1(){
return getInstance();
}
public static Singleton1 getInstance2(){
// 同步需要到类上
synchronized (Singleton1.class){
return getInstance();
}
}
/*
** 使用锁解决并发问题, 因为在加锁前,会提前进行 null 判断,所以竞争下降;但仍然存在线程安全问题,
** 当两个线程都通过 null 判断时,不难想象这种状况的出现。所以有了后面的双重检测
** @author duofei
*/
public static Singleton1 getInstance3(){
if(instatnce == null){
lock.lock();
try{
instatnce = new Singleton1();
return instatnce;
}finally {
lock.unlock();
}
}
return instatnce;
}
/*
** 双重检测;在获取到锁之后,重新检测单例对象是否已经创建;难道这样就安全了吗?
* 并不是,我们还差最后一步,不安全的原因是因为通过构造函数创建对象并不是原子的,
* 也就是说它真正创建成功还是需要时间的,当然,这都是比较极端的情况下了。
* 解决这个问题只需给 instance 字段加上 volatile 修饰。
* volatile 的作用是内存可见性和防止指令重排
** @author duofei
*/
public static Singleton1 getInstance4(){
if(instatnce == null){
lock.lock();
try{
if(instatnce == null){
instatnce = new Singleton1();
}
return instatnce;
}finally {
lock.unlock();
}
}
return instatnce;
}
}
完整的阅读上述代码是不难的。每一次的优化都是为了解决上一次的缺陷,如此看来,实现一个安全的懒汉式还是很复杂的。
饿汉式
package com.duofei;
/*
* 饿汉式
*/
public class Singleton2 {
private static Singleton2 ourInstance = new Singleton2();
public static Singleton2 getInstance() {
return ourInstance;
}
private Singleton2() {
}
}
有趣的是上述单例的创建是使用 Intellij
自动生成的,看来它默认推荐的是饿汉式了。饿汉式的实现非常简单,它利用了静态修饰的成员或者方法只能执行一次的特点(虚拟机层面的保证,所以无需我们多做什么)。那么这样的实现就没有什么问题了吗?
线程安全的问题肯定没有了。那么从使用角度讲呢?如果在当前类中还存在其它静态方法,在通过当前类调用其它静态方法时,将不得不初始化当前的单例对象,这对于某些场景来说,并不合适。那么,有么有既能够避免线程的复杂性又能够控制单例对象初始化时机的方法呢?且看下面。
内部类方式
package com.duofei;
public class Singleton3 {
private Singleton3(){}
public static Singleton3 getInstance(){
return InnerClass.INSTANCE;
}
private static class InnerClass{
private static Singleton3 INSTANCE ;
static {
INSTANCE = new Singleton3();
}
}
}
这种方式就很好的避免了单例对象实例化过早的问题,同时也利用了 static
的特性。