单例模式
什么是单例模式
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点,节省了系统资源,对于一些需要频繁创建销毁的对现象,使用单例模式可以提高系统性能。
注意点:
实例化一个单例类时,需要使用相应的获取对象的方法,而不是使用new
适用场景:
- 需要频繁创建和销毁的对象。
- 需要创建的对象是一个重量级(耗时或消耗资源过多)但又会经常用到的对象。
- 工具类对象。
- 频繁访问数据库或文件的对象。
原理图
也算不上什么原理图,只是对于需要单例模式的类,需要满足这种情况:
- 构造器私有化
- 类内部创建对象
- 向外暴露一个静态的公共方法
为什么要使用单例模式
试想这样一种情况:(这个举例不代表真正应用单例模式的情景,只作为例子出现)
当今华为受到美国打压,芯片只能设计,却无法量产,现在设计出了一款麒麟9999芯片,为了纪念这款芯片,需要制造一个实体。
由于无法量产,与台积电达成协议,只制造一枚芯片以作纪念。
那么对于这款麒麟9999芯片,全世界独一份。
对于下面这种情况,显然是不合理的:
import java.util.ArrayList;
import java.util.List;
public class Huawei {
public static void main(String[] args){
List<KirinChip> chips = new ArrayList<>();
for (int i = 0; i < 100; i++){
chips.add(new KirinChip("Kirin 9999"));
}
}
}
class KirinChip {
String chipModel;
KirinChip(String chipModel){
this.chipModel = chipModel;
}
}
这代码里华为自己制造了100枚麒麟9999芯片,这是不可能的。
既然麒麟9999全世界独一份,那么它就应有且只有一个实例,即,单例。
下面这种实现是符合逻辑的:
import java.util.ArrayList;
import java.util.List;
public class Huawei {
public static void main(String[] args){
List<KirinChip> chips = new ArrayList<>();
for (int i = 0; i < 100; i++){
// chips.add(new KirinChip("Kirin 9999")); //这样会报错,因为构造器私有了
chips.add(KirinChip.getInstance()); // 这样进行add操作,其实add的都是同一份Kirin9999
}
for (KirinChip K : chips){
System.out.println(K); //结果均为:com.designpattern.singleton.KirinChip@48140564
}
}
}
class KirinChip {//这里的单例模式实现方式使用了静态常量的饿汉式实现方式,其他实现方式详见下文
private final static KirinChip KIRIN9999 = new KirinChip("Kirin 9999");
String chipModel;
private KirinChip(String chipModel){
this.chipModel = chipModel;
}
public static KirinChip getInstance() {
return KIRIN9999;
}
}
还有不同方式实现麒麟9999的独一份吗?
一、饿汉式
JDK中的Runtime就是一个经典的饿汉式单例。
优点:
- 实现简单。
- 基于classloader机制避免了线程同步问题。
缺点:
- 在类加载时完成实例化,没有达成lazy loading(延迟加载)的效果。
- 导致类加载的原因可能有多种,如果从始至终没有用到这个实例,就会造成内存浪费。
两种实现方式:
1. 静态常量
代码:
class KirinChip {
private final static KirinChip KIRIN9999 = new KirinChip("Kirin 9999"); //类内部创建对象,这种方法需要final
String chipModel;
private KirinChip(String chipModel){ //构造器私有化
this.chipModel = chipModel;
}
public static KirinChip getInstance() { //向外提供一个对象访问接口
return KIRIN9999;
}
}
2 .静态代码块
代码:
class KirinChip {
private static KirinChip kirin9999;
String chipModel;
private KirinChip(String chipModel){
this.chipModel = chipModel;
}
static {
kirin9999 = new KirinChip("Kirin 9999");
}
public static KirinChip getInstance() {
return kirin9999;
}
}
二、懒汉式
三种实现方式:
1. 线程不安全的方式
代码:
class KirinChip {
private static KirinChip kirin9999;
String chipModel;
private KirinChip(String chipModel){ //构造器私有化
this.chipModel = chipModel;
}
public static KirinChip getInstance() { //向外暴露一个静态的公共方法,当该方法被调用时才创建一个instance
if (kirin9999 == null){
kirin9999 = new KirinChip("Kirin 9999");
}
return kirin9999;
}
}
优点:
- 起到了lazy loading的效果,但只能在单线程下使用。
缺点:
- 线程不安全,当一个线程进入if语句代码块,但未来得及往下运行,另一个线程也通过了if的判断,会产生多个实例。
2. 线程安全、同步方法
在第一种懒汉式实现方法中,线程不安全,可以通过添加synchronized关键字使其线程安全。
代码:
class KirinChip {
private static KirinChip kirin9999;
String chipModel;
private KirinChip(String chipModel){ //构造器私有化
this.chipModel = chipModel;
}
public static synchronized KirinChip getInstance() { //向外暴露一个静态的公共方法,当该方法被调用时才创建一个instance,同时使用synchronized关键字同步该方法,使得该方法被调用时进行线程同步
if (kirin9999 == null){
kirin9999 = new KirinChip("Kirin 9999");
}
return kirin9999;
}
// 这两种写法都是线程安全的,下面的方法同步了代码块,但是对于线程来说,都是会在调用该方法时进行同步,时间花销上没有太大区别
// public static KirinChip getInstance() {
// synchronized (KirinChip.class){
// if (kirin9999 == null) {
// kirin9999 = new KirinChip("Kirin 9999");
// }
// }
// return kirin9999;
// }
}
优点:
- 线程安全。
缺点:
- 效率低,每个线程在获取实例时,执行getInstance()方法都需要进行同步,但该方法只执行一次实例化代码,后续获取只需要return即可,有过多时间消耗在了同步上。
3. 同步代码块(并不能保证线程安全)
class KirinChip {
private static KirinChip kirin9999;
String chipModel;
private KirinChip(String chipModel){ //构造器私有化
this.chipModel = chipModel;
}
public static synchronized KirinChip getInstance() { //向外暴露一个静态的公共方法,当该方法被调用时才创建一个instance
if (kirin9999 == null){
synchronized(KirinChip.class){ //即使使用了synchronized关键字进行同步,也无法避免线程不安全现象,因为线程安全问题产生的原因在if判断时,而非if语句块执行时。
kirin9999 = new KirinChip("Kirin 9999");
}
}
return kirin9999;
}
}
三、双重检查(推荐使用)
若想同时保证不是每一次调用getInstance()方法都进行同步和线程安全,可以再加入一个if判断语句进行判断,第二个if用于保证如果同时有多个线程进入第一个if代码块内部的情况下仅实例化出一个实例。
代码:
class KirinChip {
private static volatile KirinChip kirin9999;//需要volatile关键字防止指令重排,使得修改值立即更新到主存(保证可见性但不保证原子性)
String chipModel;
private KirinChip(String chipModel){ //构造器私有化
this.chipModel = chipModel;
}
public static KirinChip getInstance() { //向外暴露一个静态的公共方法,当该方法被调用时才创建一个instance,并加入双重检查代码
if (kirin9999 == null){
synchronized(KirinChip.class){
if (kirin9999 = null){
kirin9999 = new KirinChip("Kirin 9999");//这里会被编译成三条指令:1.分配对象的内存空间;2.初始化对象;3.设置kirin9999的指向内存空间;
}
}
}
return kirin9999;
}
}
优点:
- 线程安全
- 效率较高
- 实现了lazy loading
四、静态内部类(推荐使用)
代码:
class KirinChip {
String chipModel;
private KirinChip(String chipModel){ //构造器私有化
this.chipModel = chipModel;
}
private static class Kirin9999 { //写一个静态内部类,该类含有一个静态属性
private final static KirinChip KIRIN9999 = new KirinChip("Kirin 9999");
}
public static Singleton getInstance() { //提供一个静态的公有方法
return Kirin9999.KIRIN9999;
}
}
优点:
- 基于classloader机制避免了线程同步问题。
- 在调用getInstance()时才装在内部类,实现了lazy loading。
五、枚举(推荐使用)
上述的四种实现方式中,当遇见反射的情况时,可能会重新创建出新的对象,会破坏单例模式。
由于枚举是不可被反射的,使用枚举的方式不但可以实现lazy loading、线程安全,该方法还可以防止反射时重新创建新的对象。
该方法也是《Effective Java》的作者Josh Bloch所提倡的实现单例的方式。
代码:
public class Huawei {
public static void main(String[] args) {
KirinChip chip1 = KirinChip.KIRIN9999;
KirinChip chip2 = KirinChip.KIRIN9999;
System.out.println(chip1.hashCode()); // 1209271652
System.out.println(chip2.hashCode()); // 1209271652
System.out.println(chip1 == chip2); // true
chip1.function();
}
}
//枚举
enum KirinChip {
KIRIN9999("Kirin 9999");
String chipModel;
private KirinChip(String chipModel){
this.chipModel = chipModel;
}
public void function() {
System.out.println("这是一个方法");
}
}