单例模式
保证了一个类只有一个实例,并且提供了一个全局访问点。单例模式的主要作用是节省公共资源,方便控制,避免多个实例造成的问题。
实现单例模式的三点:
- 私有构造函数
- 私有静态变量维护对象实例
- 公有静态方法提供获取实例对象
七种单例模式实现
1.静态类:第一次运行初始化,全局使用
2.懒汉模式(线程不安全):懒汉模式是指在第一次获取实例时才创建对象,实现了延迟加载,构造函数返回当前对象实例,但多个访问者同时获取对象实例,就会有多个同样的实例并存,不满足单例
publicclassLazySingleton{// 私有化构造器privateLazySingleton(){}// 定义一个静态变量存储唯一的LazySingleton对象privatestaticLazySingleton instance =null;// 提供一个公共的静态方法获取LazySingleton对象publicstaticLazySingletongetInstance(){// 如果instance为空,则创建一个新的对象if(instance ==null){
instance =newLazySingleton();}// 返回instance对象return instance;}}
懒汉模式(线程安全):加锁,保证线程安全,但是锁占用,导致资源浪费
3.饿汉模式(线程安全):与第一种方式基本一致,在程序启动时直接运行加载,但是可能造成资源浪费。
publicclassHungrySingleton{// 私有化构造器privateHungrySingleton(){}// 定义一个静态变量存储唯一的HungrySingleton对象,并且直接初始化privatestaticHungrySingleton instance =newHungrySingleton();// 提供一个公共的静态方法获取HungrySingleton对象publicstaticHungrySingletongetInstance(){// 直接返回instance对象return instance;}}
4.使用类的静态内部类:既保证线程安全,又保证懒汉模式,没有加锁影响性能(推荐)
publicclassSingletonInnerStatic{privatestaticclassSingletonHoler{// 静态初始化器,由JVM来保证线程安全privatestaticSingletonInnerStatic instance =newSingletonInnerStatic();}// 私有构造函数privateSingletonInnerStatic(){}// 获取对象实例的方法publicstaticSingletonInnerStaticgetInstance(){returnSingletonHoler.instance;}}
5.双重锁校验:
publicclassSingletonDoubleCheck{// 类引用privatestaticvolatileSingletonDoubleCheck singletonDoubleCheck;// 构造函数私有化privateSingletonDoubleCheck(){}// 双重校验 + 锁实现单例publicstaticSingletonDoubleCheckgetInstance(){// 第一次校验是否为nullif(singletonDoubleCheck ==null){// 不为空则加锁synchronized(SingletonDoubleCheck.class){// 第二次校验是否为nullif(singletonDoubleCheck ==null){
singletonDoubleCheck =newSingletonDoubleCheck();}}}return singletonDoubleCheck;}}
第一次校验是否为null:
主要是为了实现返回单例,避免多余的加锁操作,以及锁的等待和竞争,如果条件不成立就说明已经生成实例,直接返回即可,提高程序执行的效率。
第二次校验是否为null:
第二次校验是关键,这里防止了多线程创建多个实例(一般为两个),这里的特殊情况是这样的:在未创建实例的情况下,A线程和B线程都通过了第一次校验(
singletonDoubleCheck
为空),这时如果通过竞争B线程拿到了锁就会执行一次
new
操作,生成一个实例,然后B执行完了A就会拿到资源的锁,如果没有第二次判断的话,这时A线程也会执行一次
new
操作,这里就出现了第二个类实例,违背了单例原则。所以说两次校验都是必不可少的。
提一下上述代码中类引用中的
volatile
关键字是不能少的:
常见的,该关键字能够实现变量在内存中的可见性(告诉JVM在使用该关键字修饰的变量时在内存中取值,而不是用特定内存区域的副本,因为真实的值可能已经被修改过了),它的另外一种作用是防止JVM对指令进行重排。
其实,在
new
一个对象的时候会有如下步骤(指令):
1. 分配内存空间 2. 初始化引用 3. 将引用指向内存空间
正常的逻辑肯定以为是这样执行的 1 -> 2 -> 3,但是偏偏JVM拥有指令重排的能力,所以说执行顺序是随机的,可能是 1 -> 3 -> 2,这样的话在多线程环境下可能会拿到空引用:线程A先执行了1,3步骤,紧接着线程B执行
getInstance
,发现不为null(这里的==是判断实际的值,即引用指向的内存空间),就会返回引用,然而此时引用未初始化。所以说
volatile
在这里保证指令的执行顺序,在多线程情况下不可少。
6.AtomicReference是一个支持原子操作的对象引用变量,它可以利用CAS(比较并交换)技术来保证线程安全和高效性。一个基本的AtomicReference单例的代码示例如下:
importjava.util.concurrent.atomic.AtomicReference;publicclassSingleton{privatestaticfinalAtomicReference<Singleton> INSTANCE =newAtomicReference<>();privateSingleton(){}publicstaticSingletongetInstance(){for(;;){Singleton singleton = INSTANCE.get();if(singleton !=null){return singleton;}
singleton =newSingleton();if(INSTANCE.compareAndSet(null, singleton)){return singleton;}}}}
要使用这个单例,只需调用
Singleton.getInstance()
即可。
7.枚举类实现单例模式是一种简洁、安全、有效的方法,它可以防止反射和序列化攻击,保证线程安全和唯一性。一个基本的枚举单例的代码示例如下:
publicenumSingleton{
INSTANCE;publicvoiddoSomething(){// ...}}
要使用这个单例,只需调用
Singleton.INSTANCE.doSomething()
即可。
测试:
有以下几种方法可以测试单例的有效性,即是否能保证在多线程环境下,只有一个对象实例被创建和返回。:
- 使用反射机制,尝试创建多个单例对象,检查它们的内存地址是否相同。
- 使用序列化和反序列化机制,尝试创建多个单例对象,检查它们的内存地址是否相同。
- 使用多线程并发调用getInstance()方法,检查返回的对象是否都是同一个实例。
- 使用断言或者日志打印等方式,验证getInstance()方法返回的对象是否符合预期。
安全:
三种攻击方式:
- 反射攻击:利用jdk反射API,修改单例类构造函数的访问权限,然后调用构造函数;
- 序列化攻击:将单例对象实例以字节流的方式写入到文件中,然后再读取文件字节流,反序列化生成对象实例;
- 调用对象的克隆方法。
publicclassSingletonAtack{publicstaticvoidmain(String[] args)throwsException{//正常单例对象Singleton single1 =Singleton.getInstance();//1. 反射攻击Class clazz =Singleton.class;Constructor cons = clazz.getDeclaredConstructor(null);
cons.setAccessible(true);Singleton single2 =(Singleton) cons.newInstance(null);//2. 序列化攻击FileOutputStream fos =newFileOutputStream("a.txt");ObjectOutputStream oos =newObjectOutputStream(fos);
oos.writeObject(single1);
oos.flush();
oos.close();FileInputStream fis =newFileInputStream("a.txt");ObjectInputStream ois =newObjectInputStream(fis);Singleton single3 =(Singleton) ois.readObject();//3. clone攻击Singleton single4 =(Singleton) single1.clone();}}
防止攻击代码示例:
- 防止反射攻击:在单例类的构造函数中添加判断逻辑,如果已经存在实例对象,就抛出异常。例如:
publicclassSingleton{privatestaticvolatileSingleton instance;privateSingleton(){// 防止反射攻击if(instance !=null){thrownewRuntimeException("单例模式不允许多个实例");}}publicstaticSingletongetInstance(){if(instance ==null){synchronized(Singleton.class){if(instance ==null){
instance =newSingleton();}}}return instance;}}
- 防止序列化攻击:在单例类中实现readResolve方法,返回已有的实例对象。例如:
publicclassSingletonimplementsSerializable{privatestaticfinallong serialVersionUID =1L;privatestaticvolatileSingleton instance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance ==null){synchronized(Singleton.class){if(instance ==null){
instance =newSingleton();}}}return instance;}// 防止序列化攻击privateObjectreadResolve()throwsObjectStreamException{return instance;}}
- 防止克隆攻击:在单例类中重写clone方法,返回已有的实例对象。例如:
publicclassSingletonimplementsCloneable{privatestaticvolatileSingleton instance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance ==null){synchronized(Singleton.class){if(instance ==null){
instance =newSingleton();}}}return instance;}// 防止克隆攻击@OverrideprotectedObjectclone()throwsCloneNotSupportedException{return instance;}}
场景:
- 数据库连接池:为了避免频繁地创建和销毁数据库连接,可以使用单例模式来管理数据库连接池,保证只有一个连接池对象存在。
- 配置文件读取器:为了提高配置文件的读取效率,可以使用单例模式来缓存配置文件的内容,保证只有一个配置文件读取器对象存在。
- 日志记录器:为了统一管理日志的输出和格式,可以使用单例模式来创建日志记录器对象,保证只有一个日志记录器对象存在。
单例模式应用示例代码:
- 日志对象,可以使用java.util.logging.Logger类来创建和获取单例的日志对象。例如:
publicclassLogTest{// 使用LogTest类的名称作为日志对象的标识符privatestaticfinalLogger logger =Logger.getLogger(LogTest.class.getName());publicstaticvoidmain(String[] args){// 使用日志对象记录一条信息级别的消息
logger.info("This is a log message");}}
- 驱动对象,可以使用java.sql.DriverManager类来注册和获取单例的驱动对象。例如:
publicclassDriverTest{publicstaticvoidmain(String[] args)throwsSQLException{// 注册MySQL驱动DriverManager.registerDriver(newcom.mysql.jdbc.Driver());// 获取MySQL驱动实例Driver driver =DriverManager.getDriver("jdbc:mysql://localhost:3306/test");// 使用驱动实例连接数据库Connection conn = driver.connect("jdbc:mysql://localhost:3306/test",null);}}
- 缓存对象,可以使用一个静态变量和一个私有构造器来实现单例的缓存对象。例如:
publicclassCache{// 创建并初始化一个缓存实例作为静态变量privatestaticfinalCache INSTANCE =newCache();// 创建一个Map用于存储键值对数据privateMap<String,Object> data;// 私有化构造器,防止外部创建新的缓存实例privateCache(){
data =newHashMap<>();}// 提供一个公共方法用于获取缓存实例publicstaticCachegetInstance(){return INSTANCE;}// 提供一个公共方法用于向缓存中添加数据publicvoidput(String key,Object value){
data.put(key, value);}// 提供一个公共方法用于从缓存中获取数据publicObjectget(String key){return data.get(key);}}
- 线程池对象,可以使用java.util.concurrent.Executors类来创建和获取单例的线程池对象。例如:
publicclassThreadPoolTest{// 创建并初始化一个固定大小为10的线程池作为静态变量 privatestaticfinalExecutorService executor =Executors.newFixedThreadPool(10);publicstaticvoidmain(String[] args){for(int i =0; i <20; i++){// 向线程池提交20个任务,并由线程池分配给空闲线程执行
executor.execute(newRunnable(){@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+" is running");}});}// 关闭线程池,不再接受新任务,并等待已提交任务完成后退出
executor.shutdown();}}
- Runtime对象,可以使用java.lang.Runtime类的getRuntime方法来获取单例的Runtime对象。例如:
publicclassRuntimeTest{publicstaticvoidmain(String[] args)throwsIOException{// 获取单例的Runtime实例 Runtime runtime =Runtime.getRuntime();// 使用Runtime实例执行一个命令 Process process = runtime.exec("notepad.exe");}}
- Desktop对象,可以使用java.awt.Desktop类的getDesktop方法来获取单例的Desktop对象。例如:
publicclassDesktopTest{publicstaticvoidmain(String[] args)throwsIOException{// 获取单例的Desktop实例 Desktop desktop =Desktop.getDesktop();// 使用Desktop实例打开一个文件 File file =newFile("test.txt");
desktop.open(file);}}
版权归原作者 飞天葫芦侠 所有, 如有侵权,请联系我们删除。