4

单例模式_wx60caebe96213b的技术博客_51CTO博客

 2 years ago
source link: https://blog.51cto.com/u_15273722/5448625
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

单例模式_wx60caebe96213b的技术博客_51CTO博客

yorklh 2022-07-06 19:13:06 博主文章分类:设计模式 ©著作权

文章标签 java 初始化 单例模式 volatile 重排序 文章分类 软件设计 软件研发 阅读数126

单例模式保证在一个软件系统中,对一个类采用一定手段使其有且只有一个实例对象存在。

  1. 应用场景与意义

在spring容器管理中,spring bean默认采用单例模式(需要自行保证单例类的线程安全)。单例模式使得系统每次被请求时,不需要重复创建对象(分配内存空间,初始化对象,指向引用均为耗时操作(相对))。可提高系统执行效率,节约资源(CPU运行资源,内存分配、回收资源)。

  1. 饿汉式单例模式
package com.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton {
// 通过该类加载时就创建实例对象,创建时,存在初始化锁,所以是线程安全的 (推荐方式)
private static Singleton instance = new Singleton();
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static Singleton getInstance(){
return instance;
}

public static void main(String[] args) {
//使用10个线程,模拟并发
int threadCount = 10;
// 线程池,初始化10个线程
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

for (int i = 0; i < threadCount; i++) {
executorService.submit(()->{// 并发获取单例打印单例对象地址
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
});
}
executorService.shutdown();
}

}

/**
* 运行结果如下 : 获取的单例,是同一个对象
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
* com.test.Singleton@17f2e0c9
*/
  1. 懒汉式单例模式
  • 线程不安全的单例模式(简单方式)(线程不安全,不推荐使用该方式)

解析:假设存在A、B两个线程同时获取单例对象,当A、B同时执行到代码21行时,都认为单例对象为空后自行创建对象返回。所以线程不安全。

package com.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton {
// 初始化引用为空
private static Singleton instance;
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}

public static void main(String[] args) {
//使用10个线程,模拟并发
int threadCount = 10;
// 线程池,初始化10个线程
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

for (int i = 0; i < threadCount; i++) {
executorService.submit(()->{// 并发获取单例打印单例对象地址
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
});
}
executorService.shutdown();
}

}

/**
* 运行结果如下 : 获取的单例可能出现不一致的情况,属于线程不安全的。
* com.test.Singleton@42970371
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
* com.test.Singleton@10ff8bb1
*/
  • 线程安全的单例模式(加锁)(高并发下存在严重性能问题,不推荐)

解析:该例通过synchronized加锁保证临界区(20-25行代码)顺序执行保证线程安全。假设有A、B两个线程同时获取单例对象,执行到20行时,A先获取到锁,B需要等待A执行完毕临界区释放锁后,才能获得锁执行20-25行代码。此时,instance变量已经不为空,于是获取到与A一致的对象。

注: 高并发下,假设存在10万并发,那么每个线程执行getInstance()方法,均需要获取锁、释放锁且其他线程需要阻塞。性能低下,不推荐。

package com.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton {
// 初始化引用为空
private static Singleton instance;
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}

public static void main(String[] args) {
//使用10个线程,模拟并发
int threadCount = 10;
// 线程池,初始化10个线程
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

for (int i = 0; i < threadCount; i++) {
executorService.submit(()->{// 并发获取单例打印单例对象地址
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
});
}
executorService.shutdown();
}

}

/**
* 运行结果如下 : 获取的单例可能出现不一致的情况,属于线程不安全的。
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
* com.test.Singleton@42970371
*/
  • 双重检查锁(提高性能但线程不安全,不推荐使用)

解析:代码24行的CPU执行执行语句可分为如下三步(重排序可能打乱执行步骤)

  1. 24-1 : 分配单例对象的内存空间。
  2. 24-2 : 初始化单例对象。
  3. 24-3 : 将变量指向引用地址。

假设存在A、B两个线程。A线程执行到24行时。CPU语句执行顺序为24-1,24-2,24-3。A执行到24-2时,B线程执行到21行,判断instance变量不为空,实际上instance变量指向的内存空间对象尚未初始化完毕。所以B线程获取到的单例对象史记上是空的。存在问题。

备注:该模式线程不安全,属于重排序方面问题,很多时候不会出现了。不容易测试到结果,了解原理即可

package com.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton {
// 初始化引用为空
private static Singleton instance;
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}

public static void main(String[] args) {
//使用10个线程,模拟并发
int threadCount = 10;
// 线程池,初始化10个线程
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

for (int i = 0; i < threadCount; i++) {
executorService.submit(()->{// 并发获取单例打印单例对象地址
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
});
}
executorService.shutdown();
}

}
  • 双重检查锁(线程安全方式,推荐使用)

解析: volatile关键字,可以阻止多线程场景下的CPU执行语句重排序,也就是上文的24-1,24-2,24-3 可以保证按照线程安全的方式执行。由于使用双重检查,只有instance对象初始化之前,才存在锁竞争,之后就在第一个空判断就执行完毕了,所以相较于synchronized提高了性能。

注:本方式与上一节只增加一个关键字 ,就不贴代码了,

private static volatile Singleton instance;
  • 内部类线程安全的方式(推荐方式)

解析:该方式有点类似于饿汉式的方式,只是将初始化单例对象迁移到了静态内部类。

package com.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton {
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static Singleton getInstance(){
return Inner.getInstance();
}

private static class Inner{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
public static void main(String[] args) {
//使用10个线程,模拟并发
int threadCount = 10;
// 线程池,初始化10个线程
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

for (int i = 0; i < threadCount; i++) {
executorService.submit(()->{// 并发获取单例打印单例对象地址
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
});
}
executorService.shutdown();
}

}

/**
* 运行结果如下 : 获取的单例可能出现不一致的情况,属于线程不安全的。
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
* com.test.Singleton@15acd58d
*/
  • 单例模式的反射获取

解析:虽然存在这些获取方式,但是日常编码中,不可能如此考虑问题。不过我们应当知道这种获取方式。

package com.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton {
private static volatile Singleton instance ;
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}


public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

Thread t1 = new Thread(()->{
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
},"t1");
t1.start();


try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton s1 = Singleton.getInstance();
// 通过单例类接口 getInstance获取,所以与线程1是一样的对象
System.out.println(s1);

Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(null);
//通过反射获取,获取到不一样的对象。
System.out.println(o);
}

}

/**
* 运行结果如下 :
* com.test.Singleton@76724124
* com.test.Singleton@76724124
* com.test.Singleton@685f4c2e
*/
  • 单例模式的序列化/反序列化获取(针对单例对象实现了Serializable接口)

解析:反序列化获取需要单例类实现Serializable接口。通过将对象转成二进制再拷贝到内存中转成对象完成对象的深度复制。

package com.test;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @program: miego-micro
* @description:
* @author: yorklh
* @create: 2022-07-06 15:47
**/
public class Singleton implements Serializable {
private static volatile Singleton instance ;
//一般来说,需要通过私有化构造方法,使得外部不能通过new创建对象。
private Singleton(){}

// 向外部暴露获取单例对象的方法
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}


public static void main(String[] args) throws IOException, ClassNotFoundException {

Thread t1 = new Thread(()->{
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
},"t1");
t1.start();


try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton s1 = Singleton.getInstance();
// 通过单例类接口 getInstance获取,所以与线程1是一样的对象
System.out.println(s1);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(s1);
ByteArrayInputStream ios = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
Singleton o = (Singleton)ois.readObject();
System.out.println(o);
}

}

/**
* 运行结果如下 :
* com.test.Singleton@344d0fd0
* com.test.Singleton@344d0fd0
* com.test.Singleton@7a07c5b4
*/

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK