饿汉式:
class Singleton1{
//定义一个本类对象并实例化
private static Singleton1 s = new Singleton1();
//构造方法私有化
private Singleton1(){}
public static Singleton1 getInstance(){
return s;
}
public void print(){
System.out.println("饿汉式-单例设计模式");
}
}
懒汉式:
class Singleton2{
//定义一个本类对象并实例化
private static Singleton2 s = null;
//构造方法私有化
private Singleton2(){}
public static Singleton2 getInstance(){
if(s==null){
s = new Singleton2();
}
return s;
}
public void print(){
System.out.println("懒汉式-单例设计模式");
}
}
调用:
public class SingletonDemo {
public static void main(String[]args){
Singleton1 s = Singleton1.getInstance();
s.print();
Singleton1 s1 = Singleton1.getInstance();
System.out.println(s==s1);// true
Singleton2 s2 = Singleton2.getInstance();
s2.print();
}
}
二者区别:
饿汉式和懒汉式区别
从名字上来说,饿汉和懒汉,
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
另外从以下两点再区分以下这两种方式:
1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,为了实现线程安全,可以使用volatile和synchronized关键字。
2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了
懒汉式解决线程安全问题 :
1.给 getInstance() 方法加锁:
public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
// 给方法加锁
private static synchronized Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
}
}
虽然解决了线程不安全的问题,但是因为简单粗暴的给getInstance() 方法加锁,导致无论什么时候访问都要排队执行,大大降低了性能。
2.使用双重校验🔒:
在第一次判断后加锁,然后再进行一次判断。这样只有第一次访问的时候才会排队执行,减小了加锁对性能的影响
public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
private static Singleton getInstance() {
// 双重校验锁
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
}
}
看似已经解决了线程不安全的问题,细看还是可能发生线程不安全的问题
让我们看看:
singleton = new Singleton();
发现这里是非原子的,执行步骤:
- 先在内存中开辟空间
- 初始化:实例变量初始化、实例代码块初始化 以及 构造函数初始化
- 将变量 singleton 指向内存空间
若发生指令重排序后的执行顺序为: 1 -> 3 -> 2 ,在多线程运行时:
线程1 singleton 已经指向了之前开辟的内存,但是因为指令重排序的原因实例没有进行初始化,线程2就获得了时间片,并且直接进行了返回
3.懒汉方式 (最终版):
在上述代码的基础上使用 volatile 修饰私有类变量
volatile关键字作用:
- 保证可见性(变量都在主存进行操作)
- 禁止指令重排序,建立内存屏障;
- 不能保证原子性。
static class Singleton {
// 1.创建一个私有的构造函数
private Singleton(){}
// 2. 创建一个私有的类变量
private static volatile Singleton singleton = null;
// 3. 提供统一的访问方法
public static Singleton getInstance() throws InterruptedException {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
// 第一次访问
singleton = new Singleton();
}
}
}
return singleton;
}
}