单例模式这个题目可以关系到很多知识点。比如线程安全、类加载机制、synchronized的原理、volatile的原理、指令重排与内存屏障、枚举的实现、反射与单例模式、序列化如何破坏单例、CAS、CAS的ABA问题、Threadlocal等。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全版本的懒汉模式:
public class LazySingleton
{
private static volatile LazySingleton instance=null; //保证 instance 在所有线程中同步
private LazySingleton(){} //private 避免类在外部被实例化
public static synchronized LazySingleton getInstance()
{
//getInstance 方法前加同步
if(instance==null)
{
instance=new LazySingleton();
}
return instance;
}
}
注意:如果编写的是多线程程序,则不能删除代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
饿汉模式单例的变种:
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
饿汉式的创建方式在一些场景中将无法使用:比如 Singleton 实例的创建是依赖参数或者配置文件的,在getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
public class DoubleCheckSingleton{
private static volatile DoubleCheckSingleton instance;//静止指令重排,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作
private DoubleCheckSingleton(){}
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
这样写有好处: 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回, 如果没有获取锁,再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。 除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题,解决了上述的懒汉单例的缺点。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
-
遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
-
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
-
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
-
当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以天然是线程安全的。
枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。可直接以 SingleTon.INSTANCE的方式调用。
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();//大量的对象被创建,很容易造成OOM
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
ThreadLocal的理解:ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制(synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。同步机制仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
public class Singleton {
private static final ThreadLocal<Singleton> singleton =
new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
public static Singleton getInstance() {
return singleton.get();
}
private Singleton() {}
}
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private static SharedObjectStorage storage = FileSharedObjectStorage();
private static DistributedLock lock = new DistributedLock();
private IdGenerator() {}
public synchronized static IdGenerator getInstance()
if (instance == null) {
lock.lock();
instance = storage.load(IdGenerator.class);
}
return instance;
}
public synchroinzed void freeInstance() {
storage.save(this, IdGeneator.class);
instance = null; //释放对象
lock.unlock();
}
public long getId() {
return id.incrementAndGet();
}
}
// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
IdGenerator.freeInstance();
上面是一段伪代码,我们把单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。多个不同进程之间的访问通过分布式锁来控制。
/**
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
try {
Class<Singleton> singleClass = (Class<Singleton>)Class.forName("com.dev.interview.Singleton");
Constructor<Singleton> constructor = singleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
Singleton singletonByReflect = constructor.newInstance();
System.out.println("singleton : " + singleton);
System.out.println("singletonByReflect : " + singletonByReflect);
System.out.println("singleton == singletonByReflect : " + (singleton == singletonByReflect));
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出为:
singleton : com.dev.interview.Singleton@55d56113
singletonByReflect : com.dev.interview.Singleton@148080bb
singleton == singletonByReflect : false
如上,通过发射的方式即可获取到一个新的单例对象,这就破坏了单例。
private Singleton() {
if (singleton != null) {
throw new RuntimeException("Singleton constructor is called... ");
}
}
这样,在通过反射调用构造方法的时候,就会抛出异常:
Caused by: java.lang.RuntimeException: Singleton constructor is called...
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
//Write Obj to file
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(singleton);
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton singletonBySerialize = (Singleton)ois.readObject();
//判断是否是同一个对象
System.out.println("singleton : " + singleton);
System.out.println("singletonBySerialize : " + singletonBySerialize);
System.out.println("singleton == singletonBySerialize : " + (singleton == singletonBySerialize));
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果如下:
singleton : com.dev.interview.Singleton@617faa95
singletonBySerialize : com.dev.interview.Singleton@5d76b067
singleton == singletonBySerialize : false
如上,通过先序列化再反序列化的方式,可获取到一个新的单例对象,这就破坏了单例。
private Object readResolve() {
return getSingleton();
}
为什么增加readResolve就可以解决序列化破坏单例的问题了呢?