单例模式虽然简单,却是面试中经常出现的一类问题。
1 单例模式
单例模式的特点:
- 一是某个类只能有一个实例
- 二是它必须自行创建这个实例
- 三是它必须自行向整个系统提供这个实例
应用情况:对于多个对象使用同一个配置信息时,就需要保证该对象的唯一性。
如何保证对象的唯一性?
- 一不允许其他程序用new创建该类对象。
- 二在该类创建一个本类实例
- 三对外提供一个方法让其他程序可以获取该对象
实现的方法:
- 一是构造函数私有化
- 二是类定义中含有一个该类的静态私有对象
- 三是该类提供了一个静态的公共的函数用于创建或获取它本身的静态私有对象
方法一 饿汉式
public class Person1 { //定义该类的静态私有对象 private static final Person1 person1 =new Person1(); //构造函数私有化 private Person1(){ }; //一个静态的公共的函数用于创建或获取它本身的静态私有对象 public static Person1 getPerson1() { return person1; } }
该方法虽然在多线程下也能正确运行但是不能实现延迟加载(什么是延迟加载?)
资源效率不高,可能getPerson1()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化
方法二 懒汉式
public class Person1 { private static Person1 person1 =null; private Person1(){ } public static Person1 getPerson1() { if(person1==null){ person1=new Person1(); } return person1; } }
该方法只能在单线程下运行,当在多线程下运行时可能会出现创建多个实例的情况。
对该方法可以进行优化
懒汉式 (优化一)
public class Person1 { private static Person1 person1 =null; private Person1(){ } public static synchronized Person1 getPerson1() { if(person1==null){ person1=new Person1(); } return person1; } }
该方法虽然能保证多线程下正常运行,但是效率很低,因为 person1=new Person1(); 这句话在整个程序运行中只执行一次,但是所有调用getPerson1的线程都要进行同步,这样会大大减慢程序的运行效率。所以虽然该优化解决了问题但是并不好。
懒汉式 (优化二)
public class Person1 { private static Person1 person1 =null; private Person1(){ } public static Person1 getPerson1() { if(person1==null){ synchronized(Person1.class){ if(person1==null) person1=new Person1(); } } return person1; } }
这个优化比较好的解决了多线程问题,而且效率也很好,同时也兼顾了lazy loading。
今天看了《大话设计模式》终于明白双重判空的的意义:
对于person1存在的情况,就直接返回。当person1为null并且同时存在两个线程调用getPerson1()方法时,它们都将通过第一重的person1==null的判断。
然后由于类锁机制,这两个线程只有一个可以获得锁并进入,另一个在外排队等候,必须要其中一个进入并出来后,另一个才能进入。
而此时如果没有了第二重的person1==null是否为null的判断,则第一个线程创建了实例,而第二个线程获得锁后还是可以继续再创建新的实例,这就没有达到单例的目的。
ps:这讲解真的是通俗易懂。看完之后,对于单例怎么写,为什么要这么设计,都有了个清晰的认识。知识在书中往往有一个比较透彻的讲解,I like it。
对于getPerson1()方法的访问控制符,之前也一直停留在知道的阶段,为什么这么用并不清楚。然后自己写了个代码,使用private修饰,发现这个方法只能在当前类中引用,在类外就无法使用了。所以必须用public,实践是学习代码最快的方式,只看不练跟没学一样!!!!
volatile关键字在多线程中防止指令重排序。
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton();
这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。
if (uniqueInstance == null) { synchronized (Singleton.class) { uniqueInstance = new Singleton(); } }
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
happens-before规则,指令重排序在多线程中的不安全性
方法三缩小同步锁的范围
我们只是需要在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做加锁操作了。
使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁”——>修改——>释放锁 的操作模式。
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁。使用格式如下;
import java.util.concurrent.locks.ReentrantLock; /** * ClassName:X <br/> * Function: ReentrantLock 语法格式 * Date: 2017年11月30日 上午11:17:45 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class X { //定义对象 private final ReentrantLock lock = new ReentrantLock(); //.. //定义需要保证线程安全的方法 public void m(){ //加锁 lock.lock(); try{ //需要保证线程安全的代码 // ...method body } //使用finally块来保证释放锁 finally{ lock.unlock(); } } }
使用ReentrantLock 对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。
这里我们使用Java中的Lock对象,并进行双重校验:
import java.util.concurrent.locks.ReentrantLock; public class Person1 { private static Person1 person1 =null; private static ReentrantLock lock = new ReentrantLock(false); // 创建可重入锁,false代表非公平锁 private Person1(){ } public static Person1 getPerson1() { if(person1==null){ lock.lock(); try{ if(person1==null) person1=new Person1(); }finally{ lock.unlock(); } } return person1; } }
该方法和懒汉式 (优化二)非常类似。因为一个类中 lock对象是唯一的,相当于一把类锁。
可行的解决办法;类装载时初始化实例
/** * ClassName:Singleton4 <br/> * Function: 类装载时初始化 * Date: 2017年11月30日 下午3:08:05 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class Singleton4 { private static Singleton4 instance = new Singleton4(); private Singleton4() { System.out.println("初始化"); } public static Singleton4 getInstance() { return instance; } public static void main(String[] args) { // 任何代码都不写,此时打印一次"初始化",不能达到延迟加载 // getInstance(); //注释打开,只打印一次"初始化" } }
这个方法是在类装载时就初始化instance,虽然避免了多线程同步问题,但是没有达到lazy loading的效果
方法四 静态内部类方法实现
/** * ClassName:Singleton5 <br/> * Function: 静态内部类来实现单例,能够达到延迟加载和多线程的目的 * Date: 2017年11月30日 下午3:02:56 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class Singleton5 { private Singleton5() { System.out.println("初始化成员"); } private static class SingletonHolder { private static final Singleton5 INSTANCE = new Singleton5(); } public static Singleton5 getInstance() { return SingletonHolder.INSTANCE; } public static void main(String[] args){ //不打印任何内容,可以达到延迟加载的目的 } }
这种方法也能保证线程安全,而且也达到了lazy loading的效果
参考:https://www.cnblogs.com/paul011/p/8574650.html
方法五 枚举
public enum Person1 { person1; String name=new String("ssss"); public String getName() { return name; } public void setName(String name) { this.name = name; } }
public class test { public static void main(String[] args) { Person1 person1 =Person1.person1; Person1 person2=Person1.person1 ; person1.setName("aaa"); person2.setName("bbb"); System.out.println(person1.getName()); System.out.println(person2.getName()); } }
输出结果都为bbb
总结:相对来说懒汉式 (优化二),缩小同步锁范围,静态内部类,枚举等方法是比较好的方法。
2 其它问题
延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。可以简单理解为,只有在使用的时候,才会发出sql语句进行查询。
懒加载---即为延迟加载,顾名思义在需要的时候才加载,这样做效率会比较低,但是占用内存低,iOS设备内存资源有限,如果程序启动使用一次性加载的方式可能会耗尽内存,这时可以使用懒加载,先判断是否有,没有再去创建
延迟加载也叫动态函数加载,它是一种模式允许开发者指定程序的什么组件不应该在程序启动的时候默认读入内存。通常情况下,系统加载程序会同时自动加载初始程序和从属组件。在迟加载中这些组件只在调用的时候才加载。当程序有许多从属组件而且并不常用的时候,迟加载可以用于提高程序的性能。
延迟加载可能出现的问题:
第一,延迟加载搞不好就容易导致N+1 select问题,性能反而不能保障 第二,延迟加载一般是在ORM中采用字节码增强方式来实现,这种方式处理后的对象往往会导致对象序列化失效,而在大型web应用中一般都会采用 独立的缓存架构,一但应用系统引入独立的缓存系统对应用数据对象进行缓存,采用延迟加载后的对象序列化将失效,导致缓存失败。 第三,ORM中的延迟加载将业务对象的加载关系搞得不清不楚,如果某天想换ORM,那么还得针对不同的ORM处理延迟加载关系,即使不还ORM后来人想理解加载关系也会很头疼。 第四,延迟加载目的是一方面是对了使应用只加载必要的数据,减少数据传输量,提高查询速度。另一方面,为了减轻数据库的进行不必要查询而进行运行增加的压力,避免一次性进行过多的查询,减少系统消耗。对于第一个问题,通过必要的缓存一般可以解决,对于这点系统消耗一般还是可以承受;对于第二个问题,通过在业务层进行单表查询配合必要的索引一般也是不存在问题的。 第五,从另外一方面考虑,ORM需要承担的仅仅是O R M,和事务、缓存等特性一样,它们应该由其他更有资格的家伙来承担,不需要搞那么负载,否则对于以后的底层扩展,那可是一个艰巨的工作。
/** * ClassName:Singleton4 <br/> * Function: 类装载时初始化 * Date: 2017年11月30日 下午3:08:05 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */public class Singleton4 {private static Singleton4 instance = new Singleton4(); private Singleton4() { System.out.println("初始化"); } public synchronized static Singleton4 getInstance() { return instance; }
public static void main(String[] args) {//任何代码都不写,此时打印一次"初始化"//getInstance();//注释打开,只打印一次"初始化"}
}