EffectiveJava读书笔记- 第3条:用私有构造器或者枚举类型强化Singleton属性

用私有构造器或枚举类型强化Singleton属性

单例模式(Singleton Pattern)无疑是笔试面试中被问得最多的问题之一。单例模式虽然看似简单,但是仍有很多东西值得思考。

GOF是这么定义单例模式的:

确保一个类只有一个实例,并提供一个全局访问点。

通常实现单例都需要我们私有化构造器,让对象无法在外部创建,同时提供一个外部访问的方法返回这个单例对象。

通常单例分为两大类实现:饿汉式和懒汉式。

饿汉式单例

所谓“饿汉式单例”就是在类加载器加载这个类的时候就立马创建这个类的单例对象

1. 使用静态常量域提供外部访问

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton() {/* 私有化构造器 */}
    public void doSomething() {
        ...
    }
}

2. 使用静态工厂方法提供外部访问

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {/* 私有化构造器 */}
    public static Singleton getInstance() {
        return INSTANCE;
    }
    public void doSomething() {
        ...
    }
}

静态工厂方法相对于静态常量域的好处是可以在不改变API的前提下,可以改变该类是否为单例的想法。

3. 防止反射调用私有构造器

上面的私有构造方法仍有缺少保护,外部的调用者仍可以使用反射机制AccessibleObject.setAccessible()方法来访问私有构造方法:

public class SingletonTest {
    @Test
    public void testReflect()
            throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton newInstance = constructor.newInstance();
        Assert.assertNotEquals(Singleton.getInstance(), newInstance);
    }
}

所有我们要对构造方法更狠一点:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {
        if(INSTANCE != null)
          throw new IllegalStateException("The object can only be created once");
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
    public void doSomething() {
        ...
    }
}

4. 防止反序列化导致的多个实例

如果我们的Singleton类实现了Serializable接口,上面构造器检测抛异常的方式也无法阻止反序列化创建新实例。

public class SingletonTest {

    @Test
    public void testSeriliable() {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("D:/singleton.obj"))) {
            oos.writeObject(Singleton.getInstance());
        } catch (Exception ignore) {}

        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("D:/singleton.obj"))) {
            Object newInstance = ois.readObject();
            Assert.assertNotEquals(Singleton.getInstance(), newInstance);
        } catch (Exception ignore) {}
    }
}

我们只需要定义一个readResolve即可:

public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
        if (INSTANCE != null)
            throw new RuntimeException("The object can only be created once");
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public void doSomething() {
        System.out.println("do something");
    }

    // 访问修饰符可以任意
    private Object readResolve() {
        return INSTANCE;
    }
}

这个方式是《Effective Java》中推荐的做法。关于readResolve的原理,可以参考Java对象序列化规范或者StackOverflow对这个问题的讨论:用Java如何高效的实现单例

5. 使用单元素枚举类实现单例

使用枚举类实现单例是《Effective Java》中推荐的最佳方法:

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("do something");
    }
}

这种方式和最开始的使用静态常量域的方式差不多,但是它更简洁;由于枚举类的特性,它能绝对地防止多次实例化,并且无偿地提供了序列化的机制,这种方式完全避免了前面的反射和反序列化的问题。

懒汉式单例

所谓“懒汉式单例”就是在加载这个类的时候不立即创建对象,而是等到第一次用到单例对象的时候临时创建单例对象。对于一些大对象来说,懒加载还是很有必要的。

1. 使用静态工厂方法实现懒汉式单例

很显然为了能实现懒汉式单例,我们肯定不能直接使用静态常量了,所以只能用静态工厂方法实现懒汉式单例了。

public class Singleton {
    private static Singleton INSTANCE;
    private Singleton() {/* 私有化构造器 */}
    public static Singleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new Singleton()
        }
        return INSTANCE;
    }
    public void doSomething() {
        ...
    }
}

2. 同步方法解决多线程问题

上面的单例在单线程环境下确实没啥毛病,但是在多线程环境下可能就会出现问题:可能会有多个进程同时通过 (INSTANCE == null)的条件检查,于是,多个实例就创建出来,如果在C++里面创建的对象没有销毁就会导致内存泄漏(多线程的世界真可怕(╯︵╰)),不过好在java天生支持多线程同步,我们可以在静态工厂方法上添加synchronized关键字实现线程同步访问:

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

    public void doSomething() {
        ...
    }
}

3. 使用同步代码块减小锁粒度

线程同步问题是解决了,但是每次调用getInstance方法的时候都去检查同步锁肯定会影响程序执行的效率,虽然现在JVM对synchronized的优化做的越来越好,但是调用次数多了整体效率肯定下降,所以我们有必要减小锁粒度。

第一步:

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }

上面的做法可行吗,很显然是不可以滴,多个线程仍然会进入 (INSTANCE == null)条件,这里的同步只是让多个线程排队去创建对象而已。

第二步:

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (INSTANCE == null) {
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }

这种做法,和使用静态代码块差不多,每次调用getInstance方法的时候仍然会去检查同步锁。

第三步:

    public static Singleton getInstance() {
        // DCL
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

这个Double Checked Locking总该差不多了吧,不好意思,还不够。

多处理器共享内存(shared memory multiprocessors)或者编译器优化(optimizing compilers)进行指令重排的情况下仍有可能会导致创建多个对象。

对于这个问题我这有两种解释:

1. 多处理器共享内存:

处理器p1创建完Singleton对象,并把它赋值给INSTANCE变量走出了同步代码块,但是INSTANCE变量并没有立即反映到内存上(处理器直接操作cache高速缓存,并不直接操作内存),这时处理器p2进入同步代码块后发现INSTANCE仍为null,就会创建另一个Singleton对象。

2. 编译器优化时进行指令重排:

INSTANCE = new Singleton();这句话大概会分三步走:

  1. new:要求操作系统进行内存分配
  2. Singleton():调用类的构造函数对分配的内存进行初始化
  3. =:将新创建的对象的地址赋值给INSTANCE变量

但是JVM在将字节码翻译成机器码的过程中可能会对指令进行重新排列(学过编译原理的应该都知道编译器会对指令进行优化重排),这个时候第2步和第3步的先后顺序就不确定了,如果线程1按照1->3->2的顺序执行,线程2得到的就是一个还未初始化的实例对象,然后就报错了。

这个时候我们用上volatile关键字就能解决了。

关于volatile关键字的一些用法我之前也写过一篇文章:http://blog.csdn.net/Holmofy/article/details/73824757

第四步:

public class Singleton {
    // 使用volatile关键字
    private static volatile Singleton INSTANCE;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

    public void doSomething() {
        ...
    }
}

4. 使用私有静态内部类保存单例

上面的方法也太麻烦了吧,一个单例都要搞老半天,有没有更简单的方法。

《Effective Java》第一版推荐的方法:

public class Singleton {

    // 静态内部类包装实例
    private static class InstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return InstanceHolder.INSTANCE;
    }

    public void doSomething() {
        System.out.println("do something");
    }

}

因为使用了私有静态内部类,所以只有当第一次调用getInstance方法时才会加载这个静态内部类,然后才会去创建对象;读取的时候又没有进行线程同步不影响性能(简直完美了)。

什么时候单例不是单例

之前在StackOverflow中看到有讨论不同类加载器下单例模式会出现问题,然后在Oracle官网找到了这篇文章

1. 两个或多个JVM中有多个单例对象

由于程序在不同的JVM上运行,很明显每个JVM都会有自己的Singleton实例。但是在基于分布式技术的系统(如EJB,RMI和Jini)可以让不同的JVM中的两个对象保持相同的状态。

2. 不同的类加载器会创建多个单例对象

一个JVM可以有多个ClassLoader,当两个ClassLoader加载一个类时,实际上有两个class副本,然后每个class都有它自己的Singleton实例。有一些Servlet容器(比如iPlanet)每个Servlet都有自己的类加载器,那么两个不同的Servlet将访问不同的Singleton对象。如果你的程序中也有自定义ClassLoader,那么务必注意这个问题。

参考链接:

StackOverflow: https://stackoverflow.com/questions/70689/what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java/71399#71399

The “Double-Checked Locking is Broken” Declaration:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

How to Simply Singleton:https://www.javaworld.com/article/2073352/core-java/simply-singleton.html

When is a Singleton not a Singleton? http://www.oracle.com/technetwork/articles/java/singleton-1577166.html

//-------------------------------------------------------------------- // SimpleThreadPool.cpp. // 08/20/2025. created. // 08/20/2025. last modified. //-------------------------------------------------------------------- #include "SimpleThreadPool.h" #include <thread> // 线程库 //-------------------------------------------------------------------- // 初始化静态成员变量(类外初始化) //-------------------------------------------------------------------- std::shared_ptr<SimpleThreadPool> SimpleThreadPool::_thread_pool; std::mutex SimpleThreadPool::_singleton_mx; //-------------------------------------------------------------------- // 单例模式:获取智能指针形式的实例(双重检查锁定,线程安全) inline std::shared_ptr<SimpleThreadPool> SimpleThreadPool::instance_ptr(void) { // 第一次检查:避免每次调用都加锁(提高效率) if (!_thread_pool) { // 加锁:确保多线程下只有一个线程创建实例 std::unique_lock lck{ _singleton_mx }; // 第二次检查:防止加锁期间已有线程创建了实例 if (!_thread_pool) // 创建实例,并指定自定义删除器 _thread_pool.reset(new SimpleThreadPool, &SimpleThreadPool::deleter); } return _thread_pool; } //-------------------------------------------------------------------- // 单例模式:获取引用形式的实例(通过智能指针间接获取) SimpleThreadPool& SimpleThreadPool::instance(void) { return *instance_ptr(); } //-------------------------------------------------------------------- // 销毁单例实例(通过重置智能指针释放资源) void SimpleThreadPool::destroy_instance() { if (_thread_pool) { _thread_pool = nullptr; // 智能指针计数归零,触发deleter删除实例 } } //-------------------------------------------------------------------- // 向任务队列添加任务 void SimpleThreadPool::add_task(std::shared_ptr<ISimpleThreadTask> task) { // 忽略空任务 if (!task) return; // 加锁保护任务队列,防止并发修改 std::unique_lock lck{ _task_access_mx }; // 将任务加入队列尾部 _task_queue.emplace_back(task); // 如果当前活跃线程数小于最大线程数,唤醒一个线程处理任务 if (_alive_num < _Max_Num) { _sema.release(); // 信号量计数+1,唤醒一个等待的线程 ++_alive_num; // 活跃线程数+1 } } //-------------------------------------------------------------------- // 构造函数:创建固定数量的工作线程(10个) SimpleThreadPool::SimpleThreadPool() { // 创建10个工作线程(与_Max_Num一致) for (int i = 0; i < 10; ++i) { // 创建线程并立即 detach(脱离主线程控制,后台运行) std::jthread([this](void) -> void { // 线程主循环:持续等待并处理任务 while (true) { // 等待信号量(若计数为0则阻塞,否则计数-1并继续) _sema.acquire(); // 若线程池已标记退出,当前线程退出 if (_is_exit) { --_alive_num; // 活跃线程数-1 break; } // 循环处理任务队列中的所有任务 while (_task_queue.size() > 0) { // 加锁获取任务 std::unique_lock lck{ _task_access_mx }; // 再次检查队列(防止解锁期间任务被其他线程取完) if (_task_queue.size() > 0) { // 取出队列头部的任务 auto task = _task_queue.front(); _task_queue.pop_front(); // 从队列移除任务 lck.unlock(); // 提前解锁,避免执行任务时占用锁 // 执行任务 task->run_task(); } } // 任务处理完毕,活跃线程数-1 --_alive_num; } }).detach(); } } //-------------------------------------------------------------------- // 析构函数:(目前为空,可扩展为设置退出标志唤醒所有线程) SimpleThreadPool::~SimpleThreadPool() {} //-------------------------------------------------------------------- // 自定义删除器:销毁线程池实例(智能指针释放时调用) void SimpleThreadPool::deleter(SimpleThreadPool* p) { if (p) { delete p; // 调用析构函数 } } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // SimpleThreadPool.h. // 08/20/2025. created. // 08/20/2025. last modified. //-------------------------------------------------------------------- #pragma once #include <memory> // 智能指针 #include <semaphore> // 信号量(C++20) #include <mutex> // 互斥锁 #include <list> // 任务队列(链表) #include <vector> #include "ISimpleThreadTask.h" // 任务接口 //-------------------------------------------------------------------- // 线程池类:单例模式,管理固定数量的工作线程和任务队列 class SimpleThreadPool { public: // 获取单例的智能指针 static std::shared_ptr<SimpleThreadPool> instance_ptr(void); // 获取单例的引用 static SimpleThreadPool& instance(void); // 销毁单例实例 static void destroy_instance(); // 向线程池添加任务(接受任务接口的智能指针) void add_task(std::shared_ptr<ISimpleThreadTask> task); //批量添加任务) // void add_task(std::vector<std::shared_ptr<ISimpleThreadTask>> tasks); private: // 私有构造函数(单例模式禁止外部创建) SimpleThreadPool(); // 私有析构函数(单例模式禁止外部销毁) ~SimpleThreadPool(); // 禁止拷贝构造和移动构造(单例模式唯一性保证) SimpleThreadPool(SimpleThreadPool const&) = delete; SimpleThreadPool(SimpleThreadPool&&) noexcept = delete; // 自定义删除器(供智能指针销毁实例时调用) static void deleter(SimpleThreadPool* p); private: // 最大工作线程数(固定为10) constexpr static long long _Max_Num{ 10 }; // 单例实例(智能指针管理) static std::shared_ptr<SimpleThreadPool> _thread_pool; // 单例创建的互斥锁(防止多线程同时创建实例) static std::mutex _singleton_mx; // 信号量:控制工作线程的唤醒(最大计数为_Max_Num) std::counting_semaphore<_Max_Num> _sema{ 0 }; // 任务队列:存储待执行的任务(链表适合频繁增删) std::list<std::shared_ptr<ISimpleThreadTask>> _task_queue; // 任务队列的互斥锁(保护队列的并发访问) std::mutex _task_access_mx; // 当前活跃的工作线程数(正在执行任务或被唤醒的线程) long long _alive_num{ 0 }; // 线程池退出标志(用于通知工作线程退出) bool _is_exit{ false }; }; //-------------------------------------------------------------------- //-------------------------------------------------------------------- // ISimpleThreadTask.h. // 08/20/2025. created. // 08/20/2025. last modified. //-------------------------------------------------------------------- #pragma once // 防止头文件重复包含 //-------------------------------------------------------------------- // 线程任务接口类:所有线程池任务必须继承此类并实现run_task方法 class ISimpleThreadTask { public: // 纯虚函数:任务执行的入口,子类需实现具体逻辑 virtual void run_task(void) = 0; }; //-------------------------------------------------------------------- #include <iostream> #include "SimpleThreadPool.h" #include <chrono> // 时间相关函数 using namespace std::chrono_literals; // 简化时间单位(如1s、100ms) // 自定义任务类:继承任务接口,实现具体任务 class CustomTask : public ISimpleThreadTask { public: CustomTask(int v) : _v{ v } {} // 构造函数:传入任务编号 // 实现任务执行逻辑 void run_task(void) override { std::wcout << L"CustomTask: " << _v << std::endl; // 打印开始信息 std::this_thread::sleep_for(1s); // 模拟任务耗时1秒 std::wcout << L"CustomTask Exit: " << _v << std::endl; // 打印结束信息 } private: int _v; // 任务编号(用于区分不同任务) }; int main(void) { // 向线程池添加50个任务 for (int i = 1; i <= 50; ++i) { // 创建CustomTask实例(智能指针管理),添加到线程池 SimpleThreadPool::instance_ptr()->add_task( std::shared_ptr<CustomTask>(new CustomTask(i))); } // 休眠60秒:等待所有任务执行完毕(实际应根据任务完成状态判断) std::this_thread::sleep_for(60s); // 销毁线程池实例 SimpleThreadPool::destroy_instance(); return 0; }给我讲一下这几段代码 这就是设计了一个线程池 我现在要根据他做笔记
最新发布
08-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值