理解单例模式
首先我们来对单例模式的概念了解一下。
单例模式:对象单例设计,就是设计类时保证类的实例在内存中只有一份。
实现方式有:
1)内部设计实现(对类自身进行设计)
2)外部设计实现(对类的对象提供一种池)
那么我们 要思考的是:如何保证类的设计在内存中只有一份类的实例?
下面我们介绍几种模式:
一:懒汉模式
懒汉模式-1:
缺陷:线程不安全的单例设计(适用单线程)
``java`
class Singleton01{
//构造方法私有化,不允许外界直接构建对象
private Singleton01() {
System.out.println("Singleton01()");
}
private static Singleton01 instance;
public static Singleton01 getInstance() {
if(instance == null) {
instance = new Singleton01();
}
return instance;
}
}
此类的设计存在线程不安全?
思考:导致线程不安全的原因:
1):多个线程并发执行
2):多个线程有共享数据集
3):多个线程在共享数据集上的操作是非原子(不可再分对象)操作。
原子操作:必须是一个线程执行完了一个操作,其它线程才能执行操作
懒汉模式-2:
优点:线程安全
缺陷:低效率 (synchronized:要让多个线程在这个代码块上顺序执行)
class Singleton02{
//构造方法私有化,不允许外界直接构建对象
private Singleton02() {
System.out.println("Singleton02()");
}
private static Singleton02 instance;
//保证线程安全可以加锁synchronized,但是加锁后会按顺序执行,效率降低
//synchronized:保证代码的原子性(不能同时又多个线程对这个代码块进行访问)
//synchronized:要让多个线程在这个代码块上顺序执行
//此设计虽然保证了安全 ,但是性能会被降低
public synchronized static Singleton02 getInstance() {
if(instance == null) {
instance = new Singleton02();
}
return instance;
}
}
二:饿汉模式
优点:线程安全且高效无阻塞的单例设计
适用:适用小对象,频繁使用
此单例的缺陷:隐式加载都会执行类的初始化,会先初始化类变量(1次)后续多线程访问的时候已经有值了。
可能会对资源占用较多,尤其是对大对象(长时间不用的对象),造成内存占用浪费。
class Singleton05{
//大对象(长时间不用)
//int[] array= new int[2048];
//构造方法私有化,不允许外界直接构建对象
private Singleton05() {
System.out.println("Singleton05()");
}
//类加载的时候构建类的实例,类变量初始化一次
private static Singleton05 instance = new Singleton05();
public static Singleton05 getInstance() {
return instance;
}
public static void show() {
}
}
三:静态内部类
优点:线程安全且高效无阻塞的单例设计
适用:适用,大对象,频繁用(多线程高并发,大量访问 )
class Singleton06{
int[] array= new int[2048];
//构造方法私有化,不允许外界直接构建对象
private Singleton06() {
System.out.println("Singleton06()");
}
//通过内部类实现属性的延迟初始化(懒加载/延迟加载)
static class inner{
private static Singleton06 instance = new Singleton06();
}
public static Singleton06 getInstance() {
return inner.instance;
}
//public static void show() {} 访问show方法是内部类不会被加载
}
四:双重校验锁
优点:线程安全且高效有阻塞或少阻塞的单例设计
适用:大对象,稀少用(并发小,并发访问少 )
class Singleton04{
//构造方法私有化,不允许外界直接构建对象
private Singleton04() {
System.out.println("Singleton04()");
}
//记住:当多个线程对一个共享变量进行操作时,使用volatile关键字
private static volatile Singleton04 instance;
public static Singleton04 getInstance() {
//静态方法的锁,默认是类名.class.字节码对象
//实例方法的锁,默认是this(关键字)
if(instance==null) {
synchronized(Singleton04.class) {
if(instance == null) {
instance = new Singleton04();
}
}
}
return instance;
}
}
volatile关键字(修饰类中属性)的作用:
1)保证线程的可见性
分析:操作系统给JVM开辟内存空间,运行程序可能是多个CPU,CPU执行程序要把代码读到CPU
CPU中有cache(高速缓冲存储器),CPU执行计算会做以下内容:
①会把主内存(操作系统给JVM开辟内存空间)的数据读到CPU内部的cache中
②操作系统和硬件进行调度的时候,需要 总线 来传递数据到CPU
CPU要读取内存空间的数据,是通过一个线程thread来读的 总线(bus)
总线(bus):计算机各种功能部件之间传递信息的公共通信干线。
保证线程的可见性:
一旦有一个线程修改了变量的取值,还会把这个值写回到主内存,一旦写回到主内存,如果这个
变量使用volatile关键字修饰,会立刻向总线发一个消息,这个消息就是通知其它线程我这个
变量的值已经改变了,其它线程不要修改值往主内存写了。然后这个CPU会立刻设置为这个线程
中的cache中的数据失效,它再想修改就要从主内存中重新去读取。
操作系统立刻通知总线(bus)其它线程这个变量值已经发生改变,你给原先CPU的值已经失效了。(底层做的事)
2)禁止指令重排序:
如instance = new Singleton04();会有如下过程
内存分配空间–>属性初始化–>调用构造方法–>为instance 赋值
可能存在内存分配空间后,有内存地址,直接为instance 赋值内存地址
这就没有按照顺序执行,加volatile关键字禁止指令重排序,按顺序执行
3)但不保证其原子性