目录
1.设计模式
2.饿汉模式
3.懒汉模式
4.线程安全与单例模式
设计模式是什么?
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案
这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的
单例模式的作用就是保证某个类在程序中只存在唯一一份实例,不会创建出多个实例(之前学过的JDBC编程,DataSource这样的类就适合单例模式)
特点:
- 1、单例类只能有一个实例
- 2、单例类必须自己创建自己的唯一实例
- 3、单例类必须给所有其他对象提供这一实例
单例模式分为"饿汉""懒汉"两种
//饿汉模式
//此处保证只能创建一个实例
class Singleton{
private static Singleton instance = new Singleton();
//想要使用时,通过Singleton.getInstance()来获取!
public static Singleton getInstance(){
return instance;
}
//构造方法私有化,类外无法通过new来调用构造器创建实例!!
private Singleton(){}
}
public class Thread {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1==singleton2);
}
}
构造器私有化之后是不能通过new来调用构造器实例化对象的
此处我们将这个实例设置成私有的,通过get方法来获取,并且将构造方法私有化,不能创建新实例,因此访问这个实例的时候,每次访问得到的是同一个引用
private static Singleton instance = new Singleton();
Singleton这个属性和实例无关,是和类相关的,java代码中的每个类在编译完成后都会得到.class文件,JVM运行时会加载这个文件读取其中的二进制指令,并在内存中构造对应的类对象(Singleton.class),这个过程就是类加载的过程
该模式是如何保证实例唯一呢
1.static修饰的实例instance,让当前实例的属性是类属性.在类加载阶段就被创建,一个类只加载一次,这个实例只创建唯一一份
2.构造方法私有化,类外无法再创建新的实例
这个单例模式的名称是"饿汉模式",这个名字的来由是与后面的"懒汉模式"相比较得出的,体现在:在类加载阶段,就直接创建出了实例,实在很靠前的阶段给人一种急迫的感觉,所以叫饿汉模式
//懒汉模式
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){};
}
public class Thread {
public static void main(String[] args) {
SingletonLazy singleton3 = SingletonLazy.getInstance();
SingletonLazy singleton4 = SingletonLazy.getInstance();
System.out.println(singleton3==singleton4);
}
}
懒汉模式的实例初始情况下是null,并非是在类加载时就创建出来了,而是第一次使用的时候才创建出来的,如果没有使用,那么就不创建了,单从效率来说是更好的选择
上述两种模式在多线程环境下调用getInstance是否是线程安全的呢?
先分析一下饿汉模式
在饿汉模式中.多线程调用只涉及到了"读" 操作,我们知道多个线程只读一个变量是安全的,那么这个饿汉模式就是安全的
再看懒汉模式
这里涉及到了"读和写"两个操作,在多线程中调用,是不安全的
上述途中两个线程调用时,由于随机调度和指令重排序的特点,如果在t1线程还没有创建出实例,t2线程就调用,那么instance还是null,继续往下执行,那么t1t2会创建出两个实例,触发多次new操作了,就不满足单例模式这个应用场景的需求了!!导致了线程不安全
如何解决这个线程安全问题呢?
刚才的安全问题根本原因是读,比较,写这三个操作不是原子的,导致了t2读到的值可能是t1没来得及写的(脏读)
加锁肯定是解决线程安全问题的普适方法
public static SingletonLazy getInstance() {
if(instance == null){
synchronized (SingletonLazy.class){
instance = new SingletonLazy();
}
}
return instance;
}
这种加锁方式是不可取的!!这里只给new操作加锁了,那么t2还是可能读到t1没来得及写的数据,所以我们要给整个操作加锁! 保证读,比较,new,写这几个操作整体是原子的,正确的加锁方法如下
public static SingletonLazy getInstance() { synchronized (SingletonLazy.class){ if (instance == null) { instance = new SingletonLazy(); } } return instance; }
到这里t2读到的就是t1更新过的数据了,是一个非空值,不会触发if条件,也就不能new新的实例了,满足了单例模式的要求
但是我们每个线程调用get时都要加锁,加锁操作也是有开销的,频繁的加锁会降低效率.我们发现一旦有一个实例后,后续调用get时,instance肯定是非空的,就直接触发return,那么就不需要锁了!
所以我们再进行一个判定,如果对象还没创建就加锁,创建过了,就不加锁!
这种方式采用双校验锁机制,安全且在多线程情况下能保持高性能
getInstance() 的性能对应用程序很关键时使用
public static SingletonLazy getInstance() {
if(instance == null){
synchronized (SingletonLazy.class){
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
1处的if语句是判定是否需要加锁!
2处的if语句是判定是否要创建实例对象!
这两个连续相同的if语句在没有加锁的情况下是没有意义的,一个两个效果相同,但是中间加了锁,就可能引起线程阻塞,等到解锁之后,第一个if和第二个if之间对于计算机来说已经沧海桑田了!程序内部的状态,变量的值都可能发生很大改变
这样减少了不必要的加锁,但是还存在内存可见性问题!
假设有很多线程都来调用get,这个时候第一次调用是读内存,后续都是读寄存器/cache,那么就会有被优化的风险!
还有指令重排序引入的线程安全问题,new操作可以拆分为三个步骤
1.申请内存空间
2.调用构造方法,初始化对象
3.把空间地址赋给instance引用
编译器可能会为了提高程序效率将指令执行顺序调整,1不会被调整.23会被调整,单线程情况写123,132没有本质区别,最后都能new出实例对象,但是多线程情况下,t1如果执行132,执行到13后就被切换到t2来执行,此时t1的2还没有执行,instance仍然是一个null,t2却认为t1已经执行完3了,那么此处的引用就是非null的了,按照代码t2会直接返回一个instance引用,可能还会尝试使用引用中的属性,但是这是一个非法的实例对象,它并没有被构造完成!
解决内存可见性,指令重排序问题需要用到关键字--volatile
所以要使用volatile修饰instance!!这样就能解决内存可见性和指令重排序
线程安全的饿汉版单例模式:
class SingletonLazy{
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null){
synchronized (SingletonLazy.class){
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){};
}