**
一、饿汉式(在多线程情况是线程安全的)
**
/** 饿汉式单例模式
* @author harry
* @date 2021/11/29 22:19
*/
public class Hungry {
// 无参构造器私有化
private Hungry(){
System.out.println(Thread.currentThread().getName());
}
private static final Hungry instance = new Hungry();
public static Hungry getInstance(){
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Hungry.getInstance();
}).start();
}
Hungry instance = Hungry.getInstance();
System.out.println(instance);
// main
// com.harry.demo.Hungry@246b179d
// 创建的实例都是同一份 且当前仅有一个线程进来并走了Hungry的无参构造 打印了main主线程
}
}
通过结果可以看到:我这里给它new了1000个线程去启动 但是最后执行Hungry无参构造方法输出的只有一个主线程main,说明饿汉式这种单例模式的加载方式 对于多线程它是安全的。
注意:这里为什么不是Thread-getName()
因为当前是main主线程去执行的Hungry的实例 main是静态的
private static final Hungry instance = new Hungry(); 也是静态的 这里new了Hungry的实例 所以会去执行
Hungry的无参构造 然后打印当前线程是main 后面再去执行for循环1000次 都不会执行Hungry.getInstance();因为当前在堆中已经给你分配好了一份唯一的且有地址指向的Hungry实例。
二、懒汉式(多线程下不是线程安全的)
/**
* 懒汉式单例模式
*
* @author harry
* @date 2021/11/29 22:31
*/
public class Lazy {
// 无参构造私有化
private Lazy() {
System.out.println(Thread.currentThread().getName());
}
// 此处是没有final关键字修饰的 所以每次创建一个实例就会分配一个新的给它
private static Lazy instance;
public static Lazy getInstance() {
if (instance == null) {
instance = new Lazy();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
Lazy instance = Lazy.getInstance();
System.out.println(instance);
// Thread-0
//Thread-4
//Thread-3
//Thread-2
//Thread-1
//com.harry.demo.Lazy@246b179d
// 懒汉式加载方式 针对多线程是无效的
}
}
通过结果可以看到:我这里给它new了1000个线程去启动 但是最后执行Hungry无参构造方法输出的有多个线程的存在,说明饿汉式这种单例模式的加载方式 对于多线程它是不安全的。
这里没有出现main的线程打印 是因为 在main主线程执行的时候 去执行静态private static Lazy instance;但是这里没有对Lazy进行实例化 故不会走无参构造方法去打印出main主线程。因为在第一次main加载的时候没有创建对象 分配地址 初始化值 所以 在多个线程进来的时候 都会去找instance为null的情况,故存在多个线程的打印。
解决办法:
基于instance 判断是否为空 再去实例化Lazy对象,故衍生出来了一种解决办法:双重检测锁机制:
代码修改为:
// 双重检测锁 DCL懒汉式模式
public static Lazy getInstance() {
if (instance == null) {
synchronized (Lazy.class){
if (instance == null) {
instance = new Lazy();
}
}
}
return instance;
}
虽然双重检测锁能保证线程的顺序性,但是又会衍生出一个新的问题。在 instance = new Lazy();的时候,可能并不会保证一个原子性的一个操作安全。
new Lazy():有三个步骤:
- 在堆中开辟一个内存空间,分配内存地址
- 执行Lazy的构造方法,初始化对象
- 将堆中对象的地址赋值给栈中定义的Lazy instance引用变量 instance
在多线程并发的情况下,有可能出现指令重排的情况 (执行顺序有123 132)如果是132 就会出现第二个线程进来获取instance 此时instance !=null就会出现问题。
解决办法:
对instance类实例加volatile关键字就可以防止指令重排序
高阶玩法:
此时如果在main线程中 利用反射对Lazy的private构造方法进行破坏,通过构造方法的方式去new实例就会出现多个实例的情况。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Lazy instance = Lazy.getInstance();
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Lazy instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
此时 解决办法:
// 无参构造私有化
private Lazy() {
synchronized (Lazy.class) {
if (instance != null) {
throw new RuntimeException("请不要破坏私有的构造方法");
}
}
}
这样的话控制台就会抛出咱们自定义的一个运行时异常的语句
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.harry.demo.Lazy.main(Lazy.java:41)
Caused by: java.lang.RuntimeException: 请不要破坏私有的构造方法
at com.harry.demo.Lazy.<init>(Lazy.java:17)
... 5 more
三、枚举类
/** 探究枚举类为啥是线程安全的
* @author harry
* @date 2021/11/30 0:24
*/
public enum Single {
INSTANCE;
public static Single getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Single instance = Single.getInstance();
Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Single instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
这里也是直接根据枚举获取一个instance和通过反射去破坏private构造器去获取另外一个instance实例
但是当前控制台报
Exception in thread "main" java.lang.NoSuchMethodException: harry.demo.Single.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
Disconnected from the target VM, address: '127.0.0.1:49927', transport: 'socket'
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.harry.demo.Single.main(Single.java:18)
没有无参构造 通过反编译工具 查看后 其实jdk1.5以后枚举默认就有一个
private Single(String s,int i){
super(s,i);
}
的一个有参构造方法,于是修改我们的代码。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Single instance = Single.getInstance();
Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
Single instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
此时控制台输出的结果为:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.harry.demo.Single.main(Single.java:20)
这句话的意思就是无法以反射方式创建枚举对象。所以枚举对象是线程安全的。