模式是脱离语言而存在的,设计模式中的单例模式在并发中非常重要,大家不要沉迷于语言和架构,需要从设计角度去思考问题。 技术是最容易被替代的,只有形成了自己的方法论和产品思维才能走得更远。 ------ 写在开篇前
单例模式怎么产生的呢?
多线程操作对象是要操作不同对象还是操作同一个对象呢?
如果要操作同一个对象的话,需要保证对象的唯一性。
单例模式要解决的问题就是:实例化过程中只实例化一次。
大致的方法就是: 1)提供一个实例化的过程,也就是new方法; 2)提供返回实例对象的方法 getInstance方法。
分类有很多种,都是不断演化而来的,我将常用的给大家列一下,以后搬砖的时候如果能帮到大家就值得了。并且我们从线程安全、性能、懒加载 三方面来分析每个分类。
加载的时候就产生实例对象,形象的成为饿汉模式,很饥饿马上就要产生出来对象。
代码示例:
public class HungerySingleton {
//加载的时候就产生的实例对象
private static HungerySingleton instance=new HungerySingleton();
private HungerySingleton(){
}
//返回实例对象
public static HungerySingleton getInstance(){
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HungerySingleton.getInstance());
}).start();
}
}
}
线程安全性: 在加载的时候被实例化,所以只实例化一次,线程安全的。
性能: 性能比较好,主要影响的也就是内存的影响,长久不用占着内存。
懒加载: 没有延迟加载,如果成员变量较多的时候,长时间不使用会一直在内存中存在,该释放却释放不了会导致内存溢出。
对饿汉模式尽行了优化,采用了延迟加载,使用的时候才加载进内存。
代码示例:
public class HoonSingleton {
private static HoonSingleton instance=null;
private HoonSingleton(){
}
public static HoonSingleton getInstance(){
if(null==instance)
instance=new HoonSingleton();
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HoonSingleton.getInstance());
}).start();
}
}
}
线程安全性: 不安全,不能保证示例对象的唯一性,如图所示,当2个线程都判断为null来实例化的时候就会产生问题。下图展示了为啥线程是不安全的。
懒加载:是
性能: 较好
Double-Check-Locking 的实现就是这样的,实例代码:
public class DCL {
private static DCL instance=null;
private DCL(){
}
public static DCL getInstance(){
if(null==instance)
synchronized (DCL.class){
if(null==instance)
instance=new DCL();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(DCL.getInstance());
}).start();
}
}
}
线程安全性: 线程安全
性能:比较好
懒加载:是
但是这个模式下有个问题。
比如:
public class DCL {
private static DCL instance=null;
private DCL(){
//调用数据库连接,socket连接,实例化对象。这里面的顺序就不保证了,
//有可能指令重排,将instance重排到最前面了。
conn;
socket;
instance = new DCL();
//可能指令重排成:
// instance = new DCL();
// conn;
// socket;
}
}
就是如果是建立socket或者数据库的连接的实例化对象,容易因为指令重排引起空指针异常。如下图,当第一个线程实例化了以后并没有真正连理网络或者数据库连接的时候,第二个线程来了,就会直接调用连接,因为指令重排了,instance已经不为null了,但是conn和socket还没有建立好,所以会导致第二个出现空指针异常。这个是因为指令重排导致的,所以大家心里也有概念就行了。也不一定能遇到,但是遇到了至少排查起来心里有谱。
可以利用volatile来 限制指令重排。比如用这个限制,instance之前的语句不会重排到它后面去。
private volatile static DCL instance=null;
声明类的时候,成员变量中不声明实例变量,而放到内部静态类中。利用静态内部类来保证同步,跟加锁效果一样。在初始化HolerDemo的时候并不会建立实例对象,静态内部类,只有在被调用的时候才会加载,把实例化放在静态内部类中,就可以在使用的时候实例化,实现了懒加载。
该模式结合了饿汉模式和懒汉模式,懒加载还不用加锁。目前来说使用最广泛的一种方式了。
public class HolderDemo {
private HolderDemo(){
}
//静态内部类只有在调用的时候才会加载
private static class Holder{
private static HolderDemo instance=new HolderDemo();
}
//懒加载
public static HolderDemo getInstance(){
return Holder.instance;
}
}
线程安全性:线程安全,因为静态类实例化的时候只能实例化一次
懒加载:是
性能:好,因为没有加锁,不会串行化,性能更好。
下面的方式更加聪明,大家不一定见到过。
这个是在Effective Java一书中作者大力推荐的一种方法。如下实例,
枚举里面的常量,在加载的时候只实例化一次。这样就实现了只加载一次,但是没有懒加载,我们将holder模式的思想引入进来。
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
枚举+Holder 实现懒加载
这个大家看着是不是有点懵逼,需要结合上面的枚举和holder在来理解。多看几遍自己在运行下。
因为枚举类型里面的常量是可以直接调用枚举里面的方法和变量的。INSTANCE就是EnumHolder类型。在调用的时候才实例化。大家好好理解下。
public class EnumSingletonDemo {
private EnumSingletonDemo(){
}
//内部类 枚举
//懒加载,在调用的时候才会加载。
private enum EnumHolder{
//类型就是EnumHoler类型
INSTANCE;
private EnumSingletonDemo instance=null;
EnumHolder(){
instance=new EnumSingletonDemo();
}
}
//懒加载
public static EnumSingletonDemo getInstance(){
return EnumHolder.INSTANCE.instance;
}
}
最后2种 模式的使用时最广泛的,大家一定要理解了,这样写的代码才能牛逼。
我曾经在一个创业公司里面实习见过一个哥们写的代码,非常NB。可惜跟他共事了几个月就离开了。 他的代码就是这种风格的,非常优雅。还是那句话,架构这些东西年年出新的。很容易被取代。设计思想和基础只是才是立身之本。