单例模式
懒汉式,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式,线程不安全
是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
懒汉式,线程安全
public class Singleton {
public static Singleton singleton;
private Singleton(){
}
// 主要就是加了锁机制,避免两个线程都检查到null,然后分别创建了singleton
public static synchronized Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
饿汉式,线程安全
public class Singleton {
// static 一方面保证了instance在类加载的时候就会生成
// 另一方面保证了这个instance是属于这个类的
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化
,浪费内存。
它基于 classloader 机制
避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
双检锁/双重校验锁(DCL,即 double-checked locking),线程安全,效率高
首先来看一下单线程懒汉模式的缺点
public class Singleton {
// 声明静态变量
public static Singleton singleton;
// 私有化构造方法,使得外界不能调用new
private Singleton(){
}
// 构造一个静态方法,属于这个类的静态方法
// 在这个方法中检查类的静态变量是否已经生成
// 如果没有生成,在方法中生成
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
一个情况是,如果有一个线程A检查singleton == null条件成立,此时另一个线程B也检查singleton == null条件成立。
B线程new了一个singleton,同样的A线程也new一个singleton,这样就不符合单例模式了。
另外一个情况是,如果线程A判断singleton == null条件成立,
于是开始new singleton(),但是new的过程并不是一气呵成的,
他需要三个步骤,分别是,分配空间,初始化Singleton,把内存空间地址赋给singleton,这三个步骤有可能被指令重排序,即先发生分配空间,把内存空间地址赋给singleton,然后再初始化,
如果分配空间,把内存空间地址赋给singleton,这两步完成之后,B线程进来判断singleton == null不成立,虽然对象没有完全建立起来,但是这个引用已经有值了,直接把未完全new的对象返回了,这是不对的。
首先我们来解决第一个情况,为了避免两个线程进入都进入判断,我们可以给整个函数加锁
public class Singleton {
// 声明静态变量
public static Singleton singleton;
// 私有化构造方法,使得外界不能调用new
private Singleton(){
}
// 构造一个静态方法,属于这个类的静态方法
// 在这个方法中检查类的静态变量是否已经生成
// 如果没有生成,在方法中生成
public static synchronized Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
但是实际上这是没有必要的,因为如果对整个函数加锁,在singleton已经存在之后,众多的读操作
会被阻塞在函数之外,排队等待进去函数,这是不必要的,不需要阻塞单例生成之后的读操作。
可以采用另一种写法
public class Singleton {
// 声明静态变量
public static Singleton singleton;
// 私有化构造方法,使得外界不能调用new
private Singleton(){
}
// 构造一个静态方法,属于这个类的静态方法
// 在这个方法中检查类的静态变量是否已经生成
// 如果没有生成,在方法中生成
public static Singleton getInstance(){
synchronized(Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
这样写还有一个问题,就是线程确实不需要在函数外进行等待,但是进入函数之后,线程还是在继续等待,等待判断singleton == null,
这样的写法保证了判断和产生新对象是一气呵成的,但是效率不够
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
// 第一个进来的线程会看到null,后续线程不会看到null
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这样写一方面保证了效率,因为singleton == null不成立的时候,也就是单例已经产生之后,直接可以进行读操作,不需要排队等待
另一方面也解决了重复new的问题,因为在单例未生成的时候,需要排队等待,不会产生两个线程都判断singleton == null,都产生单例的情况。
情况一到此就解决了,那么情况二呢?
这时候如果线程A正在new新对象,并且发生了执行重新排序,已经完成了分配空间,把地址赋值给引用两个操作,这时线程B走到了第一个singleton == null,发现不成立,直接取走了单例,但是这个时候的单例并不是一个合法的单例。如何解决这个问题呢?
可以使用关键字volatile
volatile有两个作用,
其一,加volatile的对象,如果想要读取这个对象,需要从主内存读取一份,保证读到的是最新的对象,如果对这个对象进行了写操作,要立刻写回主内存,保证主内存中一直都是最新的对象。
其二,禁止指令重排序,这样就保证了new对象的工程中最后才对引用singleton赋值,避免了判断singleton == null不成立,但是读取的时候读到的是一个不完全的singleton。
静态内部类,线程安全,延迟加载
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton {
private static class SingletonHelp{
private static final Singleton singleton = new Singleton();
}
private Singleton(){
}
public static Singleton getSingleton(){
return SingletonHelp.singleton;
}
}
这种方法一方面克服了传统的饿汉式外部类加载就产生单例的浪费,因为singleton类加载的时候内部类并没有被加载,所以内部类的静态变量并未生成。
另一方面,这种方法也保证了线程安全,因为单例的生成是类加载的时候一次性生成的,不存在线程并发的问题。
PS:静态内部类
静态内部类(static nested class)的类加载过程与普通类有一些相似之处,但也有其独特之处。以下是静态内部类的类加载过程的详细描述:
类加载时机
- 独立加载:静态内部类是属于外部类的一个静态成员,但它和外部类是独立的。因此,静态内部类不会在外部类加载时自动加载,只有在静态内部类本身被使用时才会加载。
- 使用场景:静态内部类会在以下情况发生时加载:
- 静态内部类的静态成员(字段、方法)被调用时。
- 静态内部类的实例被创建时。
- 静态内部类的静态初始化块被执行时。
类加载步骤
与普通类的加载过程相同,静态内部类的加载过程包括以下几个步骤:
-
加载(Loading):
- 虚拟机通过类加载器读取静态内部类的.class文件,将其字节码加载到内存中,并创建一个
Class
对象来表示该类。
- 虚拟机通过类加载器读取静态内部类的.class文件,将其字节码加载到内存中,并创建一个
-
连接(Linking):
- 验证(Verification):确保静态内部类的字节码文件格式正确,满足Java语言规范的要求。
- 准备(Preparation):为静态内部类的静态变量分配内存,并将其初始化为默认值。
- 解析(Resolution):将静态内部类的符号引用转换为直接引用。
-
初始化(Initialization):
- 执行静态内部类的静态初始化块(如果有)和静态变量的初始化赋值语句。
特点和注意事项
- 独立性:静态内部类与外部类是相对独立的。虽然静态内部类可以访问外部类的静态成员,但它们的类加载过程是独立的。
- 访问权限:静态内部类可以访问外部类的所有静态成员,包括私有静态成员。
- 外部类的引用:静态内部类不持有对外部类实例的引用,因为它不依赖于外部类的实例。这与非静态内部类(成员内部类)不同,后者需要持有外部类的实例引用。
代码示例
以下是一个包含静态内部类的简单Java代码示例:
public class OuterClass {
private static String outerStaticField = "Outer Static Field";
static class StaticNestedClass {
static {
System.out.println("Static Nested Class Initialized");
}
void display() {
System.out.println("Accessing: " + outerStaticField);
}
}
public static void main(String[] args) {
System.out.println("Main Method Start");
StaticNestedClass nestedObject = new StaticNestedClass();
nestedObject.display();
}
}
说明
- 在上述代码中,静态内部类
StaticNestedClass
在其静态成员被访问或者实例被创建时才会被加载和初始化。 StaticNestedClass
的静态初始化块会在第一次使用时执行,打印出"Static Nested Class Initialized"。- 外部类的静态字段
outerStaticField
可以被静态内部类直接访问。
总结来说,静态内部类的类加载过程与普通类的加载过程类似,只有在静态内部类的成员被访问或实例被创建时,静态内部类才会被加载和初始化。
枚举,最优解
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
SINGLETON;
public void show() {
System.out.println("枚举类实现单例");
}
}
PS:枚举类
在Java中,枚举(enum)是一种特殊的类,用于定义一组固定的常量。枚举类不仅仅是一个简单的常量集合,它还可以包含字段、方法和构造函数。以下是关于Java枚举类的详细说明和使用示例:
基本定义
枚举类使用enum
关键字定义。每个枚举常量都是该枚举类的一个实例。
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
在这个例子中,Day
枚举包含了星期天到星期六的七个常量。
使用枚举常量
你可以直接使用枚举常量,并在代码中进行比较和操作。
public class EnumTest {
public static void main(String[] args) {
Day today = Day.WEDNESDAY;
switch (today) {
case MONDAY:
System.out.println("Today is Monday");
break;
case TUESDAY:
System.out.println("Today is Tuesday");
break;
case WEDNESDAY:
System.out.println("Today is Wednesday");
break;
// 其他情况...
default:
System.out.println("Another day");
}
}
}
枚举方法
每个枚举类都继承自java.lang.Enum
,因此它有一些内置方法,如values()
和valueOf()
。
values()
方法返回包含所有枚举常量的数组。valueOf(String name)
方法返回具有指定名称的枚举常量。
public class EnumMethods {
public static void main(String[] args) {
// 使用 values() 方法
for (Day day : Day.values()) {
System.out.println(day);
}
// 使用 valueOf() 方法
Day day = Day.valueOf("MONDAY");
System.out.println("Day is: " + day);
}
}
枚举可以包含字段、方法和构造函数
枚举类不仅可以包含常量,还可以定义自己的字段、方法和构造函数。
public enum Day {
SUNDAY("Weekend"),
MONDAY("Weekday"),
TUESDAY("Weekday"),
WEDNESDAY("Weekday"),
THURSDAY("Weekday"),
FRIDAY("Weekday"),
SATURDAY("Weekend");
private String type;
// 构造函数
Day(String type) {
this.type = type;
}
// 获取 type 字段的方法
public String getType() {
return type;
}
}
在这个例子中,Day
枚举包含了一个type
字段,用于表示每一天是工作日还是周末。
带有抽象方法的枚举
枚举类还可以包含抽象方法,每个枚举常量都必须实现这个方法。
public enum Operation {
PLUS {
double apply(double x, double y) { return x + y; }
},
MINUS {
double apply(double x, double y) { return x - y; }
},
TIMES {
double apply(double x, double y) { return x * y; }
},
DIVIDE {
double apply(double x, double y) { return x / y; }
};
abstract double apply(double x, double y);
}
在这个例子中,每个枚举常量都实现了apply
方法,用于执行相应的数学运算。
枚举实现接口
枚举类可以实现接口,使得枚举实例可以被当作接口的实现来使用。
public enum Direction implements Moveable {
NORTH, SOUTH, EAST, WEST;
@Override
public void move() {
System.out.println("Moving " + this);
}
}
interface Moveable {
void move();
}
在这个例子中,Direction
枚举实现了Moveable
接口,并且每个枚举常量都可以调用move
方法。
结论
Java中的枚举类是一种功能强大的工具,不仅可以定义常量,还可以包含方法、字段、构造函数,甚至实现接口。它提供了类型安全的常量集合,并且可以进行更复杂的操作和扩展。通过枚举类,可以使代码更加清晰和易于维护。
标签:Singleton,静态,singleton,模式,枚举,static,单例,设计模式,public From: https://www.cnblogs.com/DCFV/p/18351091