什么是单例模式
- 一个类中只有一个实例对象
单例模式的特点
- 私有构造方法,外部类无法通过new关键字来创建单例类对象
- 单例类自己创建实例对象
- 提供一个获取单例实例对象的方法,供外部类调用
单例类的五种模式
- 饿汉式
public class Singleton2 {
private static Singleton1 singleton1=new Singleton1();
private Singleton1(){}
public static Singleton1 getSingleton1(){
return singleton1;
}
public static void fun(){
System.out.println("hello world");
}
}
这种方法线程是安全的,单例对象会随便类的加载而实例化,如果我们没有用到这个对象的,它也会帮我们创建这个对象,类如上面代码,外部类只是想调用fun方法。只需要Singleton2.fun()即可,并不需要单例对象,这样容易造成资源的浪费,增大了内存的开销。
- 懒汉式
/**
* 懒汉式
*/
public class Singleton2 {
private static Singleton1 singleton1=null ;
private Singleton2(){}
public static Singleton2 getSingleton2(){
if (singleton2==null){
singleton2=new Singleton1();
}
return singleton2;
}
}
这种方法是线程不安全的,当多线程访问的时候,有可能多个线程同时到达if判断那里,此时singleton2为null,多个线程创建多个多个Singleton对象,不再是单例的了。
- 双重检查锁定
package com.linewell.designmode.singleton;
public class Singleton3 {
private static Singleton3 singleton3=null;
private Singleton3(){}
public static Singleton3 getSingleton3(){
if (singleton3==null){
synchronized (Singleton3.class){
if (singleton3==null){
singleton3 = new Singleton3();
}
}
}
return singleton3;
}
}
这种方法表面上看起来很完美,线程A和线程B同时到达第一个if语句判断的时候,假如线程A先获取到锁,进入同步代码块,实例化单例对象,然后释放锁,然后线程B获取到锁,进行二次判断单例对象是否为null,此时单例对象不为null,直接返回单例对象,保证了单例对象的唯一性。但是java的内存模型来看,这种方式是有问题的。这里面涉及到指令重排序的问题。假如此时还有一条线程C进入第一个if判断的时候,由于指令重排序的问题,单例对象还没有初始化完成,这样获取的单例对象就不完整了。下面讲解一下什么是指定重排序。
-
指令重排序
指令重排序是指计算机为了提高程序的执行性能,编译器和处理器会指令的处理过程进行重新排序。
指令重排序分为 :编译器优化重排序 指令级的并行重排序 内存系统的重排序。
int a=1;
int b=1;
int c=a+b;
正常情况下,这段代码会从上到下依次执行,但是有时候计算机会考虑到程序的执行性能,会调整代码的执行顺序,如上,有可能是先执行第二行代码,再执行第一行代码,但是最终执行效果是一样的。对于具有数据依赖的多个指令来说,jvm是禁止执行顺序的重排序,对于不具有数据依赖的指令来说,jvm是允许执行顺序的重排序的,只要保证单一线程内执行的效果保持一致就可以了。
那么上面标红部分是怎么回事呢,下面我们看下对象创建的过程是怎么样的。
1:给对象分配内存空间
2:实例化对象
3:将刚刚分配的内存空间地址指向实例化对象
从上面可以看出第二步和第三布都需要依赖第一步的结果分配内存空间,他们之间存在数据的依赖性,第二步和第三步不存在数据的依赖性,也就是说第三步有可能在第二步之前执行,先将对象指向分配的内存地址,然后再实例化单例对象,如果单例对象实例化比较久,此时singleton3不为null,此时线程C进入第一个if判断的时候,判断singleton3不等于null,直接返回了一个未初始化的单例对象。这个时候我们可以通过volatile关键字来解决这个问题。
首先我们通过一个例子来了解volatile
package com.linewell.designmode.singleton;
public class Singleton5 extends Thread{
private static boolean result=false;
@Override
public void run() {
while (!result){
System.out.println("hello world");
}
}
public static void main(String[] args) throws InterruptedException {
new Thread().start();
Singleton5.sleep(2000);
result=true;
}
}
上面代码如果是按照我们的理解,“hello world”最终会打印一次,其实并非如此,共享变量result存在主内存中,每条线程都有自己的工作内存,当需要用到共享变量的时候,首先会从主存中copy一份到工作内存,读取完后会写入到工作内存,然后同步到主内存中。上面代码中,线程A(主线程)从主内存中读取的result的值为false,线程B从主内存中读取到的result的值也为false,当主线程修改的result的时,并不能修改线程B工作内存的result的值,此时线程B工作内存中的值仍为false;要想解决这个问题,我们可以通过volitile关键字。
-
volatile 关键字的作用
1:保证了线程之前的可见性,被volatile 关键字修饰的变量,如果一个线程修改了它的的值,会马上同步到主存中,其他线程想要读取它的值只有等它写完之后才能读取(happens-before原则),事实上,基于jvm内存模型,如果一个线程修改了volatile 修饰的变量,内存模型会通知其他使用到该变量的线程将自己工作内存中的该变量置为无效,只能从主存中获取。从而保证了线程的可见性。
2:禁止指令重排序。
单例双重检查锁定最终完善如下
package com.linewell.designmode.singleton;
public class Singleton6 {
private static volatile Singleton6 singleton6=null;
private Singleton6(){}
public static Singleton6 getSingleton6(){
if (singleton6==null){
synchronized (Singleton6.class){
if (singleton6==null){
singleton6= new Singleton6();
}
}
}
return singleton6;
}
}
如果继续了解volatile关键字的含义,请参照https://www.cnblogs.com/dolphin0520/p/3920373.html
- 静态内部类
package com.linewell.designmode.singleton;
public class Singleton7 {
private Singleton7(){}
private static class Singleton {
private static final Singleton INSTANCE =new Singleton();
}
public static Singleton getSingleton(){
return Singleton.INSTANCE;
}
}
这种方法和饿汉式有点相像,都是通过类加载机制来保证初始化单例对象的时候只有一个线程,单是饿汉式没有启动Lazy-Loading的效果,内部类在Singleton7 类加载的时候并不会加载,只有当getSingleton方法的时候才会加载Singleton类,从而实例化单例对象。类的静态属性只有当的类第一次加载的时候才会实例化。
- 枚举
JDK1.5开始添加的,所以实际开发中,很少有人这么用。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}