单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。它分为懒汉式和饿汉式。
注意:
- 构造器必须私有化。
- 对外必须获得一个公有的访问方式来获得实例。
1. 饿汉式:
顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。
public class Teacher {
private static Teacher t=new Teacher();
private Teacher() {
}
public static Teacher Get() {
return t;
}
}
优点:写法简单,在类加载的时候就完成实例化,避免线程同步。
缺点:没有达到懒加载的效果,若果自始至终都没有用过这个对象,就会造成内存浪费。
2 懒汉式:
当程序第一次访问单例模式实例时才进行创建(延迟加载)。
2.1 第一种实现方式(不推荐)
public class Singleton1 {
private static Singleton1 singleton;
private Singleton1() {
}
public static Singleton1 getInstance() {
if(singleton==null)
{
singleton=new Singleton1();
}
return singleton;
}
}
缺点:只能在单线程下使用,多线程下会产生多个对象。
2.2 第二种实现方式(不推荐)
public class Singleton2 {
private static Singleton2 singleton;
private Singleton2() {
}
public static synchronized Singleton2 getInstance() {
if(singleton==null) {
singleton=new Singleton2();
}
return singleton;
}
}
缺点:加锁进行同步,虽然可以保证单例,但效率太低,浪费大量时间。
2.3 第三种实现方式(推荐)
著名的双重检查机制
public class Singleton3 {
private volatile static Singleton3 singleton;
//private static Singleton singleton;
private Singleton3() {
}
public static Singleton3 getInstance() {
if(singleton==null) {
synchronized (Singleton3.class) {
if(singleton==null) {
singleton=new Singleton3();
}
}
}
return singleton;
}
}
优点:保证单例的同时,也提高了效率。
注意:singleton前面要加volatile关键字来保证程序运行的有序性,否则多线程访问下可能会出现对象未初始化错误!
说明:在内存中,创建一个变量需要三步:1.申请一块内存 ;2.调用构造方法初始化 ;3 分配一个指针指向这块内存
在编译原理中,有一个重要的内容叫做编译器优化,即在不改变原来语义的情况下,调整语句的执行顺序,来让程序运行的更快
因此存在这样一种情况,有两个线程A、B同时访问getInstance方法
- A线程判断对象为空,没来得及进行第二次判断,(时间片用完了,B线程进入)
- B线程判断对象为空,执行创建变量的3步,先申请一块内存,后分配一个指针指向这块内存,但还没有进行初始化(时间片用完了,A线程进入)
- A线程接着执行,发现此时singleton已经不为空了,所以直接返回,但此时返回的singleton对象虽然B线程已经new了,但还没有初始化这个实例并没有构造完成,此时如果A线程使用这个实例,程序就会出现对象未初始化错误了。
2.4 第四种实现方式(推荐)
public class Singleton4 {
private Singleton4() {
}
private static class SingletonHolder
{
private static Singleton4 singleton=new Singleton4();
}
public static Singleton4 getInstance() {
return SingletonHolder.singleton;
}
}
这里采用了静态内部类实例singleton对象,静态内部类相当于一个静态属性,只有在第一次加载类时才会初始化,在类初始化时,别的线程是无法进入的,因此保证了线程安全。