前言
终于到周末了,又玩起了最爱的lol,最近新版本出了一个特别的天赋--偷钱(具体名字想不起来了),配上ez简直是吊炸天,我玩的单排,仅用了不到三十分钟就杀的对面出不了家,正当我看着伤害板沾沾自喜,对面ad说不就是选了个版本最强ad,有什么了不起的...本帅这种具有理想抱负的屌丝当然不会和这群傻叉多说什么,关闭了游戏,就来写这篇文章,构思的过程中想到,lol单排不能选重复英雄的机制不就是今天要讲的单例模式吗!
什么是单例模式
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的优缺点
优点:
1. 既然是单例模式,就说明内存中只存在一个实例,当一个对象需要频繁创建销毁时,而创建销毁对象是虚拟机控制的,我们无法直接优化其性能,单例模式的优势的就会体现的很明显。
缺点:
1.单例模式一般是没有接口的,因为它被默认要求自行实例化。
2.单例模式对测试也是不利的,在并行开发环境下,假如我们的单例代码还没有开发好,没有接口也不能通过mock的方式虚拟一个对象。
3.单例模式和上节讲的单一原则有点冲突,因为单例设计模式把必须单例当作一种业务和其他业务逻辑融入到一个类中。
使用场景
- 对象是无状态的,也就是说一个对象和多个对象办的事都是一样的,这样的话,我们就有必要减少内存使用和gc的消耗。
- 当创建一个对象消耗的资源过于昂贵,比如通过io流读写文件、连接数据库等资源。
代码分析
public class EZ {
private static EZ ez = null;
//构造函数私有化,防止外部直接调用构造函数
private EZ() {
}
//通过改静态方法获取单例对象
public static EZ getSingletonEz() {
if (ez == null) {
return new EZ();
}
return ez;
}
}
这是典型的懒汉式单例方法,低并发的情况下不会出现问题,若系统压力增大,并发量增加将有非常大的可能创建多个实例。我们可以自己创建线程池,自己伪造一个并发环境,代码如下:
public class SingletonTest {
private static ExecutorService executorService = Executors.newFixedThreadPool(1000);
public static void main(String[] args) throws InterruptedException {
//给系统足够的时候启动1000个线程
Thread.sleep(5000);
final Set<String> ezSet = new HashSet<>();
executorService.submit(new Runnable() {
@Override
public void run() {
ezSet.add(EZ.getSingletonEz().toString());
}
});
Thread.sleep(5000);
//若创建多个对象,size>1
System.out.println(ezSet.size());
}
}
这个例子在公司的时候有测试过,大概运行七八次就可以重现一次创建多个对象的现象。无奈现在家中,电脑配置过高,试了20次,竟一次都没有重现。哎~。~,配置高有时也挺不方便的,你们电脑不好的可以试下¬_¬;既然存在并发情况创建多个对象问题,那我们就有必要改善下:
public class BetterEz {
private static BetterEz ez = null;
//构造函数私有化,防止外部直接调用构造函数
private BetterEz() {
}
//通过改静态方法获取单例对象
public static BetterEz getSingletonEz() {
if (ez == null) {
synchronized (BetterEz.class) {
if (ez == null)
return new BetterEz();
}
}
return ez;
}
}
我们通过synchronized控制同一时间只有一个请求可以访问同步代码块,但为什么又要在内部加一层判断呢?假如线程A、B同时请求,A获得锁执行,B等待。当A执行完,B获得锁再去执行,若没有这个判断岂不又重新创建了新对象。但这种典型的double-check的懒汉单例存在了一个可能发生的问题, jvm接收到new指令时,简单分为3步(实际更多,可参考深入理解虚拟机),1分配内存2实例化对象3将内存地址指向引用。java的内存模型并不限制指令的重排序,也就说当执行步骤从1-》2-》3变成1-》3-》2。当线程a访问走到第2步,未完成实例化对象前,线程b访问此对象的返回一个引用,但若是进行其他操作,因为对象并没有实例化,会造成this逃逸的问题。jdk1.5之后对violate进行了重新解释,使用violate会强制保证线程可见性,增加内存屏障,禁止指令优化,从而避免这个问题。或者采用饿汉式创建对象。
上面说的都是懒汉式单例,还有一种是饿汉式单例。懒汉式的优点在于当一次食用才会new对象,饿汉式的优点在于在第一次获取该对象的效率更高。下面我们看一下饿汉式单例:
public class EZ {
private static final EZ ez = new EZ();
private EZ() {
}
public static EZ getSingletonEz() {
return ez;
}
}
当类第一次初始化的时候,就会对静态域赋值。而jvm对于类的初始化进行了严格规定,有且只有5种情况必须对类进行初始化,关于类加载的问题不属于这篇博客的范畴,就不多说了我们只需理解为这是静态变量只会初始化一次,并且这是虚拟机保证的即可。
以上就是我对单例模式的理解,感谢各位的收看
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。