Java单例模式

本文深入探讨Java单例模式的两种实现方式:懒汉式和饿汉式,分析其优缺点,尤其关注线程安全问题。通过代码示例,讲解如何在多线程环境下保证单例模式的有效性,并引入volatile关键字解决双重校验机制的缺陷。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java单例模式分为懒汉式和饿汉式两种

为什么单例?

 在内存中只有一个对象,节省内存空间,避免频繁的创建销毁对象,可以提高性能。避免对共享资源的多重占用。可以全局访问。
 确保一个类只有一个实例,自行实例化并向系统提供这个实例

单例需要注意的问题

1、线程安全问题

2、资源使用问题

1、方式1 (饿汉式)

package com;
 
public class Singleton {
 
    private static Singleton instance = new Singleton() ;
 
 
    private Singleton(){
 
    }
 
    public static Singleton getInstance() { 
        return  instance ; 
    } 
 
}

优点:在未调用getInstance() 之前,实例就已经创建了,天生线程安全
缺点:如果一直没有调用getInstance() , 但是已经创建了实例,造成了资源浪费。

2、方式1 (懒汉式)

package com;
 
public class Person {
    private static Person person ;
 
    private Person(){
         
    }
     
    public static Person get(){
        if ( person == null ) {
            person = new Person() ;
        }
        return person ;
    }
 
}

优点:get() 方法被调用的时候,才创建实例,节省资源。
缺点:线程不安全。

这种模式,可以做到单例模式,但是只是在单线程中是单例的,如果在多线程中操作,可能出现多个实例。
测试:启动20个线程,然后在线程中打印 Person 实例的内存地址

package com;
 
public class A1  {
     
    public static void main(String[] args) {
         
        for ( int i = 0 ;  i < 20 ; i ++ ) {
            new Thread( new Runnable() {
                 
                @Override
                public void run() {
                    System.out.println( Person.get().hashCode() );
                }
            }).start(); ;
             
        }
         
    }
}

结果:可以看到出现了两个实例
在这里插入图片描述
造成的原因:
线程A希望使用Person,调用get()方法。因为是第一次调用,A就发现 person 是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用 Person ,调用get()方法,同样检测到 person 是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个 Person 的对象——单例失败!
总结:
1.可以实现单线程单例 2. 多线单例无法保证
改进:1、加锁

package com;
 
public class Person {
    private static Person person ;
 
    private Person(){
 
    }
 
    public synchronized static Person get(){
        if ( person == null ) {
            person = new Person() ;
        }
        return person ;
    }
 
 
 
}

经过测试,已经可以满足多线程的安全问题了,synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次get()的调用,那性能问题就不得不考虑了!
优点:
1、满足单线程的单例
2、满足多线程的单例
缺点:
1、性能差

改进性能 双重校验

package com;
 
public class Person {
    private static Person person ;
 
    private Person(){
    }
 
    public static Person get(){
        if ( person == null ) {
            synchronized ( Person.class ){
                if (person == null) {
                    person = new Person();
                }
            }
        }
        return person ;
    }
 
}

首先判断person 是不是为null,如果为null,加锁初始化;如果不为null,直接返回 person 。整个设计,进行了双重校验。

优点:
1、满足单线程单例
2、满足多线程单例
3、性能问题得以优化

缺点:第一次加载时反应不快,由于java内存模型一些原因偶尔失败
volatile 关键字,解决双重校验带来的弊端

package com;
 
public class Person {
 
    private static volatile Person person = null ;
 
 
    private Person(){
 
    }
 
    public static Person getInstance(){
        if ( person == null ) {
            synchronized ( Person.class ){
                if ( person == null ) {
                    person = new Person() ;
                }
            }
        }
 
        return person ;
    }
 
}

假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行 person = new Person(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行 person 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 person 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整 (没有完成初始化)的 Person对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。

补充:看了图片加载框架 Glide (3.7.0版) 源码,发现glide 也是使用volatile 关键字的双重校验实现的单例,可见这种方法是值得信赖的。

总结:
1、上面的5中方法,都实现了某种程度的单例,各有利弊,根据使用的场景不同,需要满足的特性不同,选取合适的单例方法才是正道。
2、对线程要求严格,对资源要求不严格的推荐使用:1 饿汉式
3、对线程要求不严格,对资源要求严格的推荐使用:2 懒汉式
4、对线程要求稍微严格,对资源要求严格的推荐使用:4 双重加锁
5、同时对线程、资源要求非常严格的推荐使用:5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值