1. 单例模式常见问题
为什么要有单例模式
单例模式是一种设计模式,它限度了实例化一个对象的行为,始终至少只有一个实例。当只须要一个对象来协调整个零碎的操作时,这种模式就十分有用. 它形容了如何解决反复呈现的设计问题,
比方咱们我的项目中的配置工具类, 日志工具类等等。
如何设计单例模式 ?
1. 单例类如何管制其实例化
2. 如何确保只有一个实例
通过一下措施解决这些问题:
private 构造函数, 类的实例话不对外开放, 由 java 培训本人外部来实现这个操作, 确保永远不会从类内部实例化类, 防止内部随便 new 进去新的实例。
该实例通常存储为公有动态变量,提供一个静态方法,返回对实例的援用。如果是在多线程环境下则用锁或者外部类来解决线程安全性问题。
2. 单例类有哪些特点 ?
公有构造函数它将阻止从类内部实例化新对象
它应该只有一个实例这是通过在类中提供实例来办法实现的, 阻止外部类或子类来创立实例。这是通过在 java 中使构造函数公有来实现的,这样任何类都不能拜访构造函数,因而无奈实例化它。
单实例应该是全局可拜访的单例类的实例应该是全局可拜访的,以便每个类都能够应用它。在 Java 中,它是通过使实例的拜访说明符为 public 来实现的。
节俭内存, 缩小 GC
因为是全局至少只有一个实例,防止了到处 new 对象, 造成节约内存, 以及 GC,有了单例模式能够防止这些问题。
3. 单例模式 8 种写法
上面由我给大家介绍 8 种单例模式的写法, 各有千秋, 存在即正当,通过本人的应用场景选一款应用即可。咱们抉择单例模式时的筛选规范或者说评估一种单例模式写法的优劣时通常会依据一下两种因素来掂量:
1. 在多线程环境下行为是否线程平安
2. 饿汉以及懒汉
3. 编码是否优雅(了解起来是否比拟直观)
1. 饿汉式线程平安的
public class SingleTon{
private static final SingleTon INSTANCE = new SingleTon();
private SingleTon(){}
public static SingleTon getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
}
}
这种写法是非常简单实用的,值得举荐,惟一毛病就是懒汉式的,也就是说不论是否须要用到这个办法,当类加载的时候都会生成一个对象。
除此之外,这种写法是线程平安的。类加载到内存后,就实例化一个单例,JVM 保障线程平安。
2. 饿汉式线程平安(变种写法)。
public class SingleTon{
private static final SingleTon INSTANCE ;
static {
INSTANCE = new SingleTon();
}
private SingleTon(){}
public static SingleTon getInstance(){
return INSTANCE;
}
public static void main(String[] args) {SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
}
}
3. 懒汉式线程不平安。
public class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
public static SingleTon getInstance(){
if(instance == null){instance = new SingleTon();
}
return instance;
}
public static void main(String[] args) {
SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();}
}
}
这种写法尽管达到了按需初始化的目标,但却带来线程不平安的问题,至于为什么在并发状况下上述的例子是不平安的呢 ?
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){
new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();
}
为了使成果更直观一点咱们对 getInstance 办法稍做批改, 每个线程进入之后休眠一毫秒,这样做的目标是为了每个线程都尽可能取得 cpu 工夫片去执行。代码如下
public static SingleTon getInstance(){
if(instance == null){
try {Thread.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
instance = new SingleTon();
}
return instance;
}
执行后果如下
上述的单例写法,咱们是能够发明出多个实例的,至于为什么在这里要略微解释一下, 这里波及了同步问题
造成线程不平安的起因:
当并发拜访的时候,第一个调用 getInstance 办法的线程 t1,在判断完 singleton 是 null 的时候,线程 A 就进入了 if 块筹备发明实例,然而同时另外一个线程 B 在线程 A 还未发明出实例之前,就又进行了 singleton 是否为 null 的判断,这时 singleton 仍然为 null,所以线程 B 也会进入 if 块去发明实例,这时问题就进去了,有两个线程都进入了 if 块去发明实例,后果就造成单例模式并非单例。
注: 这里通过休眠一毫秒来模仿线程挂起, 为初始化完 instance
为了解决这个问题, 咱们能够采取加锁措施, 所以有了上面这种写法
4. 懒汉式线程平安(粗粒度 Synchronized)。
public class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
public static SingleTon synchronized getInstance(){
if(instance == null){instance = new SingleTon();
}
return instance;
}
public static void main(String[] args) {
SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();}
}
}
因为第三种形式呈现了线程不平安的问题, 所以对 getInstance 办法加了 synchronized 来保障多线程环境下的线程安全性问题,这种做法虽解决了多线程问题然而效率比拟低。
因为锁住了整个办法,其余进入的现成都只能阻塞期待了,这样会造成很多无谓的期待。
于是可能有人会想到可不可以让锁的粒度更细一点, 只锁住相干代码块可否?所以有了第五种写法。
5. 懒汉式线程不平安(synchronized 代码块)
public class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
public static SingleTon getInstance(){
if(insatnce == null){synchronied(SingleTon.class){instance = new SingleTon();
}
}
return instance;
}
public static void main(String[] args) {
SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();}
}
}
当并发拜访的时候,第一个调用 getInstance 办法的线程 t1,在判断完 instance 是 null 的时候,线程 A 就进入了 if 块并且持有了 synchronized 锁,然而同时另外一个线程 t2 在线程 t1 还未发明出实例之前,就又进行了 instance 是否为 null 的判断,这时 instance 仍然为 null,所以线程 t2 也会进入 if 块去发明实例,他会在 synchronized 代码里面阻塞期待,直到 t1 开释锁,这时问题就进去了,有两个线程都实例化了新的对象。
造成这个问题的起因就是线程进入了 if 块并且在期待 synchronized 锁的过程中有可能上一个线程曾经创立了实例, 所以进入 synchronized 代码块之后还须要在判断一次, 于是有了上面这种双重测验锁的写法。
6. 懒汉式线程平安(双重测验加锁)
public class SingleTon{
private static volatile SingleTon instance ;
private SingleTon(){}
public static SingleTon getInstance(){
if(instance == null){synchronied(SingleTon.class){if(instance == null){instance = new SingleTon();
}
}
}
return instance;
}
public static void main(String[] args) {
SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();}
}
}
这种写法根本趋于完满了,然而可能须要对一下几点须要进行解释:
• 第一个判空 (外层) 的作用?
• 第二个判空 (内层) 的作用?
• 为什么变量润饰为 volatile?
第一个判空 (外层) 的作用
首先,思考一下可不可以去掉最外层的判断?答案是:能够
其实仔细观察之后会发现最外层的判断跟是否线程平安正确生成单例无关!!!
它的作用是防止每次进来都要加锁或者期待锁,有了同步代码块之外的判断之后省了很多事,当咱们的单例类实例化一个单例之后其余后续的所有申请都没必要在进入同步代码块持续往下执行了,间接返回咱们曾生成的实例即可,也就是实例还未创立时才进行同步,否则就间接返回,这样就节俭了很多无谓的线程等待时间,所以最外的判断能够认为是对晋升性能有帮忙。
第二个判空 (内层) 的作用
假如咱们去掉同步块中的是否为 null 的判断,有这样一种状况,A 线程和 B 线程都在同步块里面判断了 instance 为 null,后果 t1 线程首先取得了线程锁,进入了同步块,而后 t1 线程会发明一个实例,此时 instance 曾经被赋予了实例,t1 线程退出同步块,间接返回了第一个发明的实例,此时 t2 线程取得线程锁,也进入同步块,此时 t1 线程其实曾经发明好了实例,t2 线程失常状况应该间接返回的,然而因为同步块里没有判断是否为 null,间接就是一条创立实例的语句,所以 t2 线程也会发明一个实例返回,此时就造成发明了多个实例的状况。
为什么变量润饰为 volatile
因为虚拟机在执行创立实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创立一个新的对象并非是原子性操作。在有些 JVM 中上述做法是没有问题的,然而有些状况下是会造成莫名的谬误。
首先要明确在 JVM 创立新的对象时,次要要通过三步。
1. 分配内存
2. 初始化结构器
3. 将对象指向调配的内存的地址
因为仅仅一个 new 新实例的操作就波及三个子操作, 所以生成对象的操作不是原子操作。
而理论状况是,JVM 会对以上三个指令进行调优,其中有一项就是调整指令的执行程序 (该操作由 JIT 编译器来实现)。
所以,在指令被排序的状况下可能会呈现问题,如果 2 和 3 的步骤是相同的,先将调配好的内存地址指给 instance,而后再进行初始化结构器,这时候前面的线程去申请 getInstance 办法时,会认为 instance 对象曾经实例化了,间接返回一个援用。
如果这时还没进行结构器初始化并且这个线程应用了 instance 的话,则会呈现线程会指向一个未初始化结构器的对象景象,从而产生谬误。
7. 动态外部类的形式(根本完满了)
public class SingleTon{
public static SingleTon getInstance(){
return StaticSingleTon.instance;
}
private static class StaticSingleTon{
private static final SingleTon instance = new SingleTon();
}
public static void main(String[] args) {
SingleTon instance1 = SingleTon.getInstance();
SingleTon instance2 = SingleTon.getInstance();
System.out.println(instance1 == instance2);
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();}
}
}
• 因为一个类的动态属性只会在第一次加载类时初始化,这是 JVM 帮咱们保障的,所以咱们无需放心并发拜访的问题。所以在初始化进行一半的时候,别的线程是无奈应用的,因为 JVM 会帮咱们强行同步这个过程。
• 另外因为动态变量只初始化一次,所以 singleton 依然是单例的。
8. 枚举类型的单例模式(太完满以至于。。。)
public Enum SingleTon{
INSTANCE;
public static void main(String[] args) {
// 通过开启 100 个线程 比拟是否是雷同对象
for(int i=0;i<100;i++){new Thread(()->
System.out.println(SingleTon.getInstance().hashCode())
).start();}
}
}
这种写法从语法上看来是完满的, 他解决了下面 7 种写法都有的问题, 就是咱们能够通过反射能够生成新的实例。然而枚举的这种写法是无奈通过反射来生成新的实例, 因为枚举没有 public 构造方法。