概述
Singleton
:在 Java
中指单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
例如:代表 JVM
运行环境的 Runtime
类:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
常见形式
特点:
- 构造器私有化
- 由类的一个静态变量来保存唯一的实例
- 使用
public
对外暴露或者用静态变量的get
方法获取
饿汉式
:直接创建对象
,不存在线程安全问题
1、直接实例化饿汉式(简洁)
public class Single1 {
public static final Single1 INSTANCE = new Single1();
private Single1() {
}
}
在 Java - static 关键字 这篇文章中我们知道,static
关键字修饰的变量会在类加载的时候就创建。这种方式创建的单例模式有一个问题,就是说比如我们在这个单例类内部还有一个静态方法:
public class Single1 {
public static final Single1 INSTANCE = new Single1();
private Single1() {
}
public static String getSource() {
return "";
}
}
当我们调用 getSource()
方法的时候,也会创建一个 INSTANCE
单例对象,虽然我们并不需要这个单例对象。
2、枚举类(最简洁)
枚举类型:表示该类型的对象是有限的几个,我们可以限定为 1
个,这样就成了一个单例类。直接通过 类名 + INSTANCE
获取
public enum Single2 {
INSTANCE
}
3、静态代码块饿汉式(适合复杂实例化)
public class Single31 {
public static final Single31 INSTANCE;
static {
INSTANCE = new Single31();
}
private Single31() {
}
}
这种方式其实跟第一种方式 直接实例化
是一样的,我们来比较一下两种方式的字节码:
- 直接实例化单例模式的字节码:
静态代码块单例模式的字节码:
可以看出,两种方式的字节码是一样的。不过,静态代码块的使用场景要复杂一点,当初始化涉及到一些其他操作的时候,就需要用这种方式,比如:
public class Single3 {
public static final Single3 INSTANCE;
private String info;
static {
try {
Properties properties = new Properties();
properties.load(Single3.class.getClassLoader().getResourceAsStream("single3.properties"));
INSTANCE = new Single3(properties.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException();
}
}
private Single3(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "Single3{" +
"info='" + info + '\'' +
'}';
}
}
懒汉式
:延迟创建对象
1、线程不安全(适用于单线程)
public class Single41 {
private static Single41 INSTANCE;
private Single41() {
}
public static Single41 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Single41();
}
return INSTANCE;
}
}
- 单线程环境下
void test() {
Single41 instance1 = Single41.getInstance();
Single41 instance2 = Single41.getInstance();
System.out.println(instance1 == instance2); // true
}
- 多线程环境下
void test() throws ExecutionException, InterruptedException {
Callable<Single41> callable = Single41::getInstance;
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Single41> f1 = es.submit(callable);
Future<Single41> f2 = es.submit(callable);
System.out.println(f1.get() == f2.get()); // 有时为 true,有时为 false
es.shutdown();
}
2、线程安全(适用于多线程)
在上面代码的基础上,使用 synchronized
关键字,加锁:
public class Single42 {
private static Single42 INSTANCE;
private Single42() {
}
public static Single42 getInstance() {
// 当有多个线程进来后,会在这里排队来请求获取锁
synchronized (Single42.class) {
if (INSTANCE == null) {
// 假设这里比较耗时
INSTANCE = new Single42();
}
}
return INSTANCE;
}
}
上面这种方式会有性能问题,当大量线程进来,会都堵在获取锁的地方,所以我们可以修改为:
public class Single42 {
private static Single42 INSTANCE;
private Single42() {
}
public static Single42 getInstance() {
if (INSTANCE == null) {
synchronized (Single42.class) {
if (INSTANCE == null) {
INSTANCE = new Single42();
}
}
}
return INSTANCE;
}
}
3、静态内部类形式(适用于多线程)
上面这种方式,还是比较复杂的,我们可以使用 静态内部类
的方式,来简化代码:
public class Single6 {
private Single6() {
}
private static class Inner {
private static final Single6 INSTANCE = new Single6();
}
public static Single6 getInstance() {
return Inner.INSTANCE;
}
}
静态内部类
不会自动随着外部类的加载和初始化而加载和初始化,它是要单独去加载和初始化的,所以是懒加载,而又因为是在内部类加载的时候创建和初始化的,所以又是线程安全的。