设计模式系列之单例模式(五种写法)
前言
设计模式是我们程序员应该要掌握的,可能没有用过,但是至少听过。毕竟没吃过猪肉,哪还能没见过猪跑,那么什么是设计模式呢?
设计模式 是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
反正很重要就对了,而且在找工作的时候也是经常问到的,通常会有面试官让你手写个设计模式,最常见的就是单例模式了,接下来我们就来看一下单例模式到底是什么?
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
饿汉式
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全。
* 缺点:不管用到与否,类加载的时候就完成实例化
*/
public class singleton1 {
private static final singleton1 INSTANCE=new singleton1();//定义一个静态的实例INSTANCE为singleton1
private singleton1(){};
public static singleton1 getInstance(){
return INSTANCE;
}
}
饿汉式可以与接下来的懒汉式相对比,在这个类加载的时候就完成了实例化,通过将构造方法私有化,也就是别人无法直接将这个对象给new出来,毕竟private只属于这个类,如果想使用这个对象,那么就可以通过singleton1 s=singleton1.getInstance()来实现。这一种是线程安全的。
懒汉式
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 懒汉式:lazy loading
* 用的时候才进行初始化,但是存在线程不安全的问题
*/
public class singleton2 {
private static singleton2 INSTANCE;
private singleton2(){};
public static singleton2 getInstance(){
if (INSTANCE==null){
//线程一进行,判断了为空,那么就要去实例化,而第二个线程进行的时候
//也是判断为空,那么也会去实例化,这样就生成了不止一个对象
INSTANCE=new singleton2();
}
return INSTANCE;
}
}
懒汉式可以与饿汉式进行对比理解,饿汉式是在加载的时候就完成了实例化,而懒汉式则是类加载完之后并不实例化,当需要的时候,再去进行实例化。
但是这种写法存在线程不安全的问题。如代码中的位置,当线程一进入,判断为空,那么就要去实例化,而此时第二个线程进入了,也是判断为空,那么也会去实例化,如果有多个线程,那么生成的就不止一个对象了。为了验证我们的猜想,我们写一个例子来测试一下:
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 懒汉式:lazy loading
* 用的时候才进行初始化,但是存在线程不安全的问题
*/
public class singleton2 {
private static singleton2 INSTANCE;
private singleton2(){};
public static singleton2 getInstance(){
if (INSTANCE==null){//在多线程情况下,如果第一个线程判断INSTANCE为空,那么
//就会去执行INSTANCE=new singleton2(),但是如果在实例化的过程中,另一个
//线程也进来了,判断出INSTANCE为空,那么他也会去实例化singleton2,这样
//的话,会实例化两个singleton2,生成两个singleton2,
try {
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE=new singleton2();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i=0;i<20;i++){
new Thread(()->{
System.out.println(singleton2.getInstance().hashCode());
}).start();
}
}
}
运行结果:
124791833
636619253
1175053369
805244208
2109215169
1834323973
1582315003
1781696970
805244208
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
我们知道,在一个方法生成的对象如果hashCode不相同,那么就说明并不是一个对象,因此,这种方法是存在线程安全问题滴。
加锁的懒汉式
在并发编程中,有个关键字叫做synchronized,那么我们能否通过对懒汉式加锁来实现线程安全呢?答案是可以的:
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 加synchronized锁实现线程安全,但是加锁会降低性能
*/
public class singleton3 {
private static singleton3 INSTANCE;
private singleton3(){};
public static synchronized singleton3 getInstance(){
if (INSTANCE==null){
INSTANCE=new singleton3();
}
return INSTANCE;
}
}
通过在方法上加锁,保证在运行方法的时候,不会被其它线程使用。当然了,使用synchronized锁也就会导致性能会有所下降。
双检锁
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 双检锁
*/
public class singleton5 {
private static volatile singleton5 INSTANCE;
private singleton5(){};
public static singleton5 getInstance(){
if (INSTANCE==null){//如果不判断的话,也可以,只不过每次都要进行加锁,消耗性能
synchronized (singleton5.class){
if (INSTANCE==null){
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE=new singleton5();
}
}
}
return INSTANCE;
}
双检锁这种方式通过两次判断是否为空来创建对象以及synchronized锁来保证线程安全。那么第一个未加锁的判断是否可以省去呢?
这个方法的含义就是说:如果INSTANCE为空,那么加上锁,再去判断是否为空,如果成立,才去实例化对象。第一个判断从线程安全的角度来看,是可以省略去的,但是会造成每次判断都要去获取到synchronized锁,因此会稍微降低下性能。
看到这里,你是否会有个疑惑:为什么要判断两次?还要加锁,直接判断为空就创建对象呗,还整那么麻烦干嘛?OK,我们来看下下面的例子:
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
*/
public class singleton4 {
private static singleton4 INSTANCE;
private singleton4(){};
public static singleton4 getInstance(){
//不是线程安全,第一个线程进来判断为null,还没有拿到锁的时候,又进来
//其它线程拿到了锁,实例化对象之后释放了锁,而之前的线程因为判断过为Null了,
//所以会拿到所继续实例化对象,造成线程不安全
if (INSTANCE==null){
synchronized (singleton4.class){
INSTANCE=new singleton4();
}
}
return INSTANCE;
}
}
我们假设有多个线程同时进来,线程一进来的时候,一看还没有实例化,正要执行实例化的时候,线程二、三、四…都进来了,正好也发现没有实例化,都去实例化了,当线程一将对象实例化之后,虽然已经有了对象,但是对于线程二、三、四来说,他们是经过了判断,认为是没有的,那么必然要去实例化,所以就会生成多个对象,为了验证我们的猜想,写个例子跑一下:
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
*/
public class singleton4 {
private static singleton4 INSTANCE;
private singleton4(){};
public static singleton4 getInstance(){
//不是线程安全,第一个线程进来判断为null,还没有拿到锁的时候,又进来
//其它线程拿到了锁,实例化对象之后释放了锁,而之前的线程因为判断过为Null了,
//所以会拿到所继续实例化对象,造成线程不安全
if (INSTANCE==null){
synchronized (singleton4.class){
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE=new singleton4();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i=0;i<20;i++){
new Thread(()->{
System.out.println(singleton4.getInstance().hashCode());
}).start();
}
}
}
执行结果:
1781696970
1998854885
1756277772
749748823
1274452530
1119442503
1627956625
1117110646
1343599390
1926683415
122651855
757961580
588558273
805244208
1175053369
636619253
2109215169
1834323973
1582315003
124791833
emmm,这个结果就离谱,这么多不一样的,这也证明了这种方式是不安全的。
静态内部类
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 静态内部类
*/
public class singleton6 {
private singleton6(){};
private static class singleton6Holder{
private final static singleton6 INSTANCE=new singleton6();
}
public static singleton6 getInstance(){
return singleton6Holder.INSTANCE;
}
}
在前面的写法中,懒汉式需要考虑线程安全,饿汉式利用类加载的特性帮助我们省去了对线程安全的考虑,那么,将这两者结合就是这中静态内部类的方式。既能保证线程安全,又能延迟加载。静态内部类的特性是使用的时候才去加载,而加载的时候又是线程安全的。
枚举
package com.dong.patten.singleton;
/**
* @author 雪浪风尘
* @Remember Keep thinking
* 不仅可以解决线程安全问题,还可以防止序列化
* 不会被反序列化的原因:枚举类没有构造方法,因此即使拿到这个对象的class文件,也无法构造它的对象,它返回的只是那么INSTANCE,
*/
public enum singleton7 {
INSTANCE;
public void n(){
//业务代码
}
}
这种据说是最完美的写法,不仅简洁,还能够防止序列化,因为枚举类没有构造方法,因此即使拿到这个对象的class文件,也无法构造它的对象,它返回的只是那么INSTANCE,
git地址
git地址:
https://github.com/PonnyDong/pattern
后续在github上会持续更新设计模式的相关代码。
上一篇: PHP form表单提交问题
下一篇: phpMyAdmin配置