- 八种写法(两种完美无缺、工作中一般不用完美无缺的)
- 主要记住饿汉、懒汉和枚举
- 语法上最完美的是enum,实际中最完美的是饿汉式
- 后面用单例时,多数可以用spring来做单例,spring的bean工厂产生单例(自己写的越来越少)
饿汉式(推荐使用)
- 简单实用,线程安全
- 构造方法私有,通过getInstance静态方法获取实例
- 类加载到内存后,就实例化一个单例,jvm保证线程安全(jvm对每个类只load一次)
- 唯一缺点:不管使用与否,类装载时就会完成实例化(不用时没必要加载—->类装载Class.forName(“”);)
作为静态变量初始化
```java package com.mashibing.dp.singleton;
/**
- 饿汉式
- 类加载到内存后,就实例化一个单例,JVM保证线程安全
- 简单实用,推荐使用!
- 唯一缺点:不管用到与否,类装载时就完成实例化
- Class.forName(“”)
(话说你不用的,你装载它干啥) */ public class Mgr01 { private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
} }
<a name="OCHPS"></a>
### 在静态代码块中初始化
```java
package com.mashibing.dp.singleton;
/**
* 跟01是一个意思
*/
public class Mgr02 {
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
private Mgr02() {};
public static Mgr02 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}
懒汉式
- 什么时候用才给他初始化
- 第一次调用getInstance方法时给他初始化
- 弥补了饿汉式的缺点但是带来了线程不安全
- 线程不安全问题—->多线程访问时不安全问题
- 两线程在未实例化的情况下同时进入if判断,会产生两个不同的实例(二次判空)
- 在getInstance中睡一会儿,产生不同实例的现象会越来越严重(因为线程产生得比一次线程执行完毕的更快) ```java package com.mashibing.dp.singleton;
/**
- lazy loading
- 也称懒汉式
虽然达到了按需初始化的目的,但却带来线程不安全的问题 */ public class Mgr03 { private static Mgr03 INSTANCE;
private Mgr03() { }
public static Mgr03 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
// 业务代码 public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
} }
<a name="IWYV7"></a>
### 在方法上加锁synchronized
- 锁定的是类(加在静态方法上)
- 但是效率会降低(互斥-->排队)
- synchronized是一个稍重量级的锁(jdk1.x之后进行了优化?)
```java
package com.mashibing.dp.singleton;
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
synchronized代码块
- 更精确地加锁
- 但是这种方法不能保证只有一个对象
- 因为会有好几个线程同时进入if判断,虽然要被阻塞但是仍会获得创建实例的机会 ```java package com.mashibing.dp.singleton;
/**
- lazy loading
- 也称懒汉式
- 虽然达到了按需初始化的目的,但却带来线程不安全的问题
可以通过synchronized解决,但也带来效率下降 */ public class Mgr05 { private static Mgr05 INSTANCE;
private Mgr05() { }
public static Mgr05 getInstance() {
if (INSTANCE == null) {
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
} }
<a name="Q9UsT"></a>
### 双重检查单例写法(双重校验锁)
- 在上面synchronized代码块中只有一个处在代码块外面的if判断约束下,在代码块内部再加一个if判断实例是否已经被创建
- 目前为止,这种单例模式最为完美,但是用第一种写法就已经足够了(虽然存在一些小瑕疵但是不影响),追求完美可以使用这种方式
- 外面的if依然要判断,会避免那些在单例已实例化完成的情况下而只是想单纯地获取实例的线程依然会因锁而排队阻塞的情况
- 在变量上要加volatile关键字(jvm中会对代码优化进行指令重排序,JIT即时编译器尤其明显)
- JIT(编译)没有将代码优化成本地代码时,可能不加volatile也没事
- 类比受精卵的两次屏蔽作用(同学nb)
```java
package com.mashibing.dp.singleton;
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
静态内部类的方式(相对来说最完美)
- 完美写法之一,比第一种完美
- 定义一私有静态内部类xxxHolder,在静态内部类中初始化了一个外部类的实例常量(final修饰+private修饰+static修饰)
- 在外部类中定义一静态方法(public修饰)该方法返回静态内部类中外部类的实例常量
- 这种方式得到对象后会多一份字节码(同学说)
- 只加载外部类时,静态内部类是不会被加载的(不会被初始化),调getInstance时获取实例时内部类才会被加载
- 线程安全有jvm来保证,虚拟机加载一个class时只加载一次(线程安全jvm内部自己保证,不需要再加锁)
- 无法防止反序列化? ```java package com.mashibing.dp.singleton;
/**
- 静态内部类方式
- JVM保证单例
加载外部类时不会加载内部类,这样可以实现懒加载 */ public class Mgr07 {
private Mgr07() { }
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
<a name="WYiNt"></a>
## 枚举型单例(完美中的完美)
- 不仅可以解决线程同步,还可以防止序列化
- enum只有一个取值INSTANCE
- 在枚举型中可以有一些业务方法与变量(变量不确定?)
- 《Effective Java》中的推荐写法
- 反序列化不成枚举
- 反射能通过字节码文件load到内存创建对象
- 反序列化直接通过读取内存生成对象(不使用构造方法)
- 想挡住反序列化很复杂--->需要设置一些内部的变量(重写readResource方法可以防止反序列化?)
- 最完美的写法--->枚举单例
- enum只能有一个实例
- ResouceManager原本是类,定义成enum很别扭
- ~~enum的用法查一查~~
```java
package com.mashibing.dp.singleton;
/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 {
INSTANCE;
public void m() {}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
枚举单例不会被反序列化的原因
- 枚举类没有构造方法?拿到class文件也没办法构造他的对象
- 枚举反编译后是一个abstract class(java语言的规定)
- 反序列化仍然返回的是同一个值?根据这个值找到的对象和单例创建的是同一个对象
-
对枚举单例额外的想法
一个枚举不给他实例,只写方法——>最简单的单例
- 使用直接类名+.调用,也不能new出实例
- 相当于定义一个类把所有东西都静态化了
- 实际业务中这样写不一定很合适,有些东西要new出来才更合适,不是所有东西都搞成静态的最合适,这时搞单例比较合适
🤏随想
- lambda表达式是对只有一个方法的匿名接口或匿名内部类(函数式接口)的简化,省略不写也知道你重写的是这个方法(一种简写)
- 不同对象的哈希码hashcode是不同的(通过地址来算?)
- jvm对每个类只load一次
- hash码相同也有可能不是同一个对象(hash碰撞?),但是同一个类的不同对象的hash码是不同的(打印地址看是否是同一个对象更精确)
- 变量加final必须给他初始化(不在定义的时候初始化必须马上跟上static语句块给他初始化)
- while循环判断—->乐观锁?自旋锁?
- 《Effective Java》java创始人(多读书!)
- 心中有剑,手中无剑。在工作中用解决实际问题更合适的方式更好,不追求不必要的完美
- 学东西由薄到厚,再由厚到薄
- interface里面可以写静态?