震惊!!!原来这就是单例模式!!!
一:什么是设计模式
单例模式是一种典型的设计模式.
设计模式就是为解决编程中的一些典型问题,提供的一些解决方案.
遇到这个情景,遇到这个问题,代码应该怎么写,效率更高,能够更好的解决这个问题.
二:单例模式
2.1:什么是单例
在进程中的某个类,有且只有一个对象(不能new出来对个对象),这样的对象,就称为"单例".
但如何保证一个类只有一个实例???
此时就需要通过一些编程上的技巧,使编译器能够自动发现代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错.
从代码上保证单例,这种代码就称为单例模式
2.2:饿汉模式:
class Singleton{
/**
* static 修饰成员变量instance,说明这个成员变量是静态的,是属于类对象的,**只有一份**
* **在类加载的时候就被初始化了,这个创建过程就是线程安全的**
*
*/
private static Singleton instance=new Singleton();
/**
* 通过getInstance(),获取到类对象并返回
* 后续需要使用这个类的实例,就可以通过getInstance()来获取已经new 好的这个,而不是重新new
* @return
*/
public static Singleton getInstance(){
return instance;
}
/**
* private说明构造方法是私有的,在类外不能创建类对象,从而保证了类对象的唯一
* 类之外的代码,尝试new 的时候,就会调用构造方法,由于构造方法是私有的,无法调用,就会编译出错
*/
private Singleton(){
}
}
public class Demo2 {
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
/**
* 由于类对象是唯一的,所以这里输出true
*/
System.out.println(s1==s2);
}
}
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton (){
}
}
public class Demo1 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2=new Singleton();
}
}
2.3:懒汉模式:
懒汉模式:不是程序启动的时候创建实例,而是在第一次使用的时候才去创建(如果不使用,就不用创建,就会把创建实例的代价节省下来了).
class SingletonLazy{
public static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo4 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}
如果代码中存在多个单例类,使用饿汉模式,就会使这些实例都在程序启动的时候扎堆创建,就可能把程序启动的时间拖慢.
如果是懒汉模式,啥时候首次调用,调用时机是分散的.
2.4:单例模式应用场景
代码中的有些对象,本身就不应该有多个实例的,从业务的角度应该就是单个实例.
比如:服务器要从硬盘上加载100G的数据到内存中,肯定要写一个类,封装上述加载操作,并且写一些获取/处理数据的业务逻辑.
这样的类就应该 是单例的,一个实例,就管理100G的内存数据,创建多个实例:就是N*100G的内存数据,机器吃不消,而且也没必要.
再比如:服务器很可能涉及到一些"配置项",代码中也需要有专门的类,管理配置,需要加载配置数据到内存中的供其他代码使用.
这样的类也应该是单例的,如果是多个实例,就存储了多份数据,如果一样就罢了,如果不一样,以哪个为准呢?
三:线程安全吗???
在这里只考虑多个线程同时调用getInstance()方法是否会产生线程安全问题.
3.1:饿汉模式??
饿汉模式是在Java进程启动的时候创建实例,而后面在线程中调用getInstance(),实例已经创建好了,不会进行创建,也就意味着饿汉模式的getInstance(),只做了一件事,那就是读取静态变量,而读取操作又是线程安全的.
3.2:懒汉模式??
懒汉模式不仅有读操作,还有修改操作,并且这两个操作还不是原子的,因此就会造成线程安全问题.
比如:
上图情况就会造成创建了两个实例,违背了 单例模式,就是线程不安全的.
3.2.1 加锁
通过加锁的方式,来保证懒汉模式下,getInstance()是线程安全的.
class SingletonLazy{
private static SingletonLazy instance = null;
Object locker =new Object();
public SingletonLazy getInstance() {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
private SingletonLazy(){
}
}
public class Demo2 {
public static void main(String[] args) {
}
}
t1执行加锁之后,t2就会阻塞,直到t1释放锁(new 完了)t2才能拿到锁,才能进行条件判定,t2的条件就会认为instance非null,就不会创建实例了.
3.2.2 双重if判断
上述代码认为,只要调用getInstance()就需要先加锁,但懒汉模式下,只有第一次调用getInstance()的时候,才会涉及到线程安全问题,一旦把线程创建好了,后续再调用,就只是读取操作了,线程就是安全的了,就没必要加锁了.
所以我们就需要判断是否要加锁,第一次创建对象要加锁,然后都是读取操作了,就没必要加锁了,
第一次创建对象是什么时候??
答案是显然的,当instance为null的时候,当调用getInstance()就是要创建对象,此时要加锁.
/**
*
* 懒汉模式
*/
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
Object locker = new Object();
if (instance == null) {//判断是否要加锁
synchronized (locker) {
//synchronized 会阻塞在线程,阻塞过程中,其他线程就可能修改instance的值
if (instance == null) { //判断是否要创建对象,
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo2 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}
3.2.3:内存可见性问题
t1线程修改了instance的引用,t2线程可能读取不到(概率比较小),加上volatile关键字就可以解决.
3.3.4:指令重排序问题
我们写的代码,最终编译成了机器能够识别的二进制指令,正常来说,CPU,应该是按照顺序,一条一条往下执行的,但编译器比较智能,会根据情况,调整执行的顺序,调整顺序的主要目的就是为了提高效率(前提保证逻辑是等价的).
举一个生活中的例子:
家人让我去超市买菜:西红柿,鸡蛋,黄瓜,茄子.
但我们可能不按这个顺序买,会根据实际情况(摊位的顺序)调整:先买黄瓜,西红柿,茄子,鸡蛋,
把执行顺序调整之后,最终的效果没变,但效率提高了不少.
指令重排序的前提,一定是重排序之后,逻辑和之前等价.
单线程下,编译器进行指令重排序的操作,一般是没有问题的,编译器可以准确的识别出哪些操作可以重排序,而不会影响到逻辑
但在多线程下,编译器的判定就不会那么准确了,就可能会出现重排序之后,逻辑发生改变了,进而引起bug.
instance = new SingletonLazy();
/**
* 这一行代码,可以分为三个(指令)步骤:
* 1:申请内存空间
* 2:调用构造方法(对内存空间进行初始化)
* 3:把此时内存空间的地址,赋值给instance引用
*/
在指令重排序优化策略下,上述的执行顺序可能是 1 3 2 也可能是 1 2 3 (1一定是先执行的)
而如果是 1 3 2 在多线程下,可能会引起bug.
要解决上述问题,就需要引入volatile,volatile不仅仅能够解决内存可见性问题,也能禁止编译器针对这个变量读写操作的指令重排序问题.
加上volatile 之后,此时, t2 线程读到的数据,一定是t1已经构造完毕的完整对象了(一定是 1 2 3 都执行完毕的对象了).
/**
*
* 懒汉模式
*/
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
Object locker = new Object();
if (instance == null) {//判断是否要加锁
synchronized (locker) {
//synchronized 会阻塞在线程,阻塞过程中,其他线程就可能修改instance的值
if (instance == null) { //判断是否要创建对象,
instance = new SingletonLazy();
/**
* 这一行代码,可以分为三个步骤:
* 1:申请内存空间
* 2:调用构造方法
* 3:把此时内存空间的地址,赋值给instance引用
*/
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo2 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}
正确的写法:
1:先写最初的版本(不考虑线程安全的问题)
2:加上锁
3:加上双重if
4:最后加上volatile