要实现单例模式,最主要的就是 私有化 构造函数,然后提供一个对外调用的方法,去创建一个唯一的对象。
/**
这是一种懒汉方法创建的单例模式,是线程不安全的
**/
@NOThreadSafe //自定义的注解,标记线程不安全的类
public class NoSafeExample1 {
private static NoSafeExample1 instance = null;
//私有化构造函数
private NoSafeExample1() {
}
public static NoSafeExample1 getInstance() {
if(instance == null){
instance = new NoSafeExample1();
}
return instance;
}
}
缺点:在多线程环境下,是线程不安全的,假如有两个线程同时进行访问该类,拿到了 instance = null 的值,就会创建出不止一个对象。
@ThreadSafe
public class SafeExample1 {
private static SafeExample1 instance = null;
//私有化构造函数
private SafeExample1(){
}
//采用synchronized 来确保线程的安全
public static synchronized SafeExample1 getInstance(){
if(instance == null){
instance = new SafeExample1();
}
return instance;
}
}
缺点:采用synchronized 锁住这个方法,使线程达到安全,当并发量达到一定程度的时候,会造成性能下降。
public class NoSafeExample2 {
private static NoSafeExample2 instance = null;
//私有构造函数
private NoSafeExample2(){
}
public static NoSafeExample2 getInstance(){
//采用双重检验,若有对象,则不用加锁,提高性能
if(instance == null){
synchronized(NoSafeExample2.class){
if(instance == null){
instance = new NoSafeExample2();
}
}
}
return instance;
}
}
缺点:这种写法即使采用 synchronized 也是线程不安全的,不安全在 instance = new NaSafeExample2()这个语句
instance = new NoSafeExample2();
在程序中分三步进行执行
1. memory = allocate() 分配对象的内存空间
2. ctorInstance() 初始化对象
3. instance = memory 设置instance 指向内存的分配空间
由于cpu与jvm的优化,会将没有关联的指令进行重排
可能造成的顺序为 1,3,2
假设现在有两个线程
线程A 执行到 instance = new NoSafeExample2(); 此时的指令顺序是经过指令重排的
线程A执行了第3部,还未执行第2步
线程B执行到 instance == null,由于线程A行了第3步,所以 instance != null,直接返回
instance,但线程A还未执行第2步,所以是有问题的。
public static NoSafeExample2 getInstance(){
if(instance == null){ //B
synchronized(NoSafeExample2.class){
if(instance == null){
instance = new NoSafeExample2(); //A -> 3
}
}
}
return instance;
}
解决方案: 加 volatile 关键字 作用 防止 指令重排 与 可见行
private volatile static NoSafeExample2 instance = null; // 就是线程安全的了。
/**
饿汉式单例模式,在类加载的时候就创建对象,是线程安全的
**/
public class SafeExample2 {
//创建对象
private static volatile SafeExample2 instance = new SafeExample2();
//私有化构造函数
private SafeExample2(){
}
public static SafeExample2 getInstance(){
return instance;
}
}
缺点:由于在类加载的时候就创建对象,如果对象过大,就会导致响应时间变长。
采用枚举类进行单例的实现
简单的写法:
public enum SafeExample3 {
INSTANCE;
public SafeExample3 getInstance(){
return INSTANCE;
}
}
复杂的写法,可能不是太严谨
public class SafeExample3 {
//私有化构造函数
private SafeExample3(){
}
public static SafeExample3 getInstance(){
return Singleton.INSTANCE.getSingleton();
}
private enum Singleton{
INSTANCE;
private SafeExample3 Singleton;
Singleton(){
Singleton = new SafeExample3();
}
public SafeExample3 getSingleton() {
return Singleton;
}
}
}
如果是其他的方式实现单例模式,可能会存在 1. 反射 2.序列话问题。
这里主要参考了博客:https://www.cnblogs.com/chiclee/p/9097772.html
这边简单的说一下:
1.反射可以通过借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,并且调用,就可以创建多个对象;
标有黄色那行是 反射的源码,反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
2.序列化,readObject 会创建新的对象
详细的内容,关于枚举类的单例,参考上面给出的博客地址。