`
DevDiv.net
  • 浏览: 23400 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
社区版块
存档分类
最新评论

Symbian中singleton的实现(多线程)

阅读更多
作者:Vincent
转载地址:http://www.devdiv.net/home/space-1-do-blog-id-311.html
我正在将我的代码移植到Sybian OS,但是我听说在Symbian OS中全局数据的使用有所限制,这是真的吗?如果我尝试使用全局 数据会发生什么呢?这会妨碍我移植采用单例设计模式的代码吗?

Anxious About Singleton

亲爱的Anxious About Singleton,

首先,不要慌!是的,在Symbian OS中有一些情况是不建议使用全局数据,但是这种限制只存在于DLL中——如果你编写的是EXE,那么程序是不会受到影响的。

我们这里讨论的“可修改全局数据”指的是任何非常量的全局域变量或任何非常量的函数域静态变量。例如:


TBufC<20> fileName;   // 可修改全局数据
void SetFileName()
  {
  static TInt iCount; // 静态变量
  ...
  }

为了简略表示,Symbian通常将这类变量称为WSD,代表可写静态数据。

坏消息
在包含最初内核结构,EKA1的手机上如果要运行DLL,那么该DLL中是不允许使用WSD的。这意味着在Symbian OS v8.1a以前(对应于S60和UIQ参见下面的表格)的DLL中是不存在WSD的。

随着新的内核结构(EKA2,在Symbian OS v8.1b和Symbian OS v9中采用)的出现,这种局面得到改善,在DLL中使用WSD成为可能。然而,由于WSD在内存是使用方面开销很大,在编写会被很多进程载入的共享DLL 时,不建议使用WSD。这是因为对于每个载入DLL的进程,DLL中的WSD通常需要消耗4KB的内存。

当进程载入第一个包含WSD的DLL时,它创建一个单独的虚拟内存区域来保存该WSD。一个虚拟内存区域最小为4KB,同时消耗的内存与现实需要的静态数据量无关。任何供WSD使用但是没有被使用的内存都浪费了(然而,如果随后的DLL载入该进程并且也使用WSD,那么相同的虚拟内存区域可供使用,而不用为每个使用WSD的DLL都分割4KB的虚拟内存区域)。

既然内存是针对进程而言,那么潜在的内存浪费量为:

(4KB – WSD 字节) × 客户端进程数举例来说,如果一个DLL被4个进程使用,那么潜在的“隐形”内存开销为16KB(减去WSD自身占用的内存)。I

此外,运行于Windows PC上的Symbian OS仿真器并不完全支持使用WSD的DLL。在单独的正在运行的进程中,仿真器只能载入一个使用WSD的DLL(原因是Symbian OS仿真器在Windows上层的实现方式)。如果在仿真器中有第二个进程试图载入相同的DLL,该操作会失败并返回错误代码 KErrNotSupported。Symbian OS v9.4引入该问题的解决方法。更多信息可以从 [R1]获得。

在很多场合,在DLL中使用WSD获得的好处要大于其缺点(比如,当移植大量使用WSD的代码时,以及使用那些只会被一个进程载入一次的DLL)。作为一个第三方开发者,你可能觉得WSD的内存开销是可接受的,比如,如果你创建一个只被一个应用程序载入一次的DLL。但是注意,当前支持的GCC-E编译器版本有个缺陷,即使用静态数据的DLL可能会在载入时导致严重错误。这个问题,以及其解决方法,在Symbian开发者网站的FAQ1574中得到讨论。

然而,如果你的工作是设备创造(比如,创造用于Symbian OS,某一个UI平台,或手机设备上的共享DLL),那么折中标准就不同了。你的DLL可能被许多进程使用,带来的内存开销和限制会使得WSD的使用缺乏理由。

好消息
如果你的工作针对Symbian OS v9而不是Symbian OS的早期版本,那么你受到WSD限制的影响就会较小。在Symbian OS v9之前,应用程序编译为DLL,开发者如果需要移植使用WSD的应用程序,就不得不寻找解决方法。在Symbian OS v9中,应用程序框架结构的改变意味着现在所有的应用程序是EXE而不是DLL。在EXE中总是允许使用可写静态数据,所以如果需要,应用程序现在可以使用WSD。

下表总结不同Symbian UI平台中DLL和应用程序对WSD的支持情况:

UI 平台 Symbian OS 版本 Symbian OS 内核 运行于该设备上的DLL支持使用WSD? 应用程序允许使用WSD?
UIQ 2.x
S60 1st 和 2nd 版本 v7.0 (UIQ)
v7.0s, v8.0a, v8.1a (S60) EKA1 不支持 不允许
UIQ 3 S60 3rd 版本 v9 EKA2 支持,但是不建议在不理解Windows仿真器限制和内存开销要求的前提下使用 允许——应用程序是EXE

用法
至此,我已经回答了你关于在Symbian OS DLL中可修改全局数据所受限制的问题,需要更多信息请参见[R1]。让我们进一步看看使用WSD会发生什么。以下讨论的前提假设是你的工作只针对Symbian OS v9。

首先,有时候你会发现自己不自觉地使用WSD。一些Symbian OS类具有非平凡的构造函数,这意味着其对象必须在运行时构造。你可能认为自己没有使用WSD,但是由于一个对象直到其构造函数执行才被实例化,该对象被认为是可修改的,而不是常量。这里是一些示例:

static const TPoint KGlobalStartingPoint(50, 50);
static const TChar KExclamation('!');
static const TRgb KDefaultColour(0, 0, 0);
在大部分的Symbian OS版本中,如果你试图编译使用了WSD的DLL,不论你是否有意这样做,你在针对手机硬件编译DLL时都会遇到错误。该错误与如下类似:

错误: Dll 'TASKMANAGER[1000C001].DLL' 含有未初始化的数据
你会发现使用WSD的DLL总是针对Windows仿真器编译。只有当代码针对手机硬件编译时,使用WSD才会标记为错误。参考文献[R2]说明了无意使用WSD时如何对其进行追踪。如果你确定要使用WSD,你可以通过在MMP文件中加入关键字EPOCALLOWDLLDATA来实现。

然而,值得注意的是有些Symbian OS版本(例如,Symbian OS v9.3,发现于S60 3rd版本FP2)无论MMP文件中是否存在 EPOCALLOWDLLDATA,都不将DLL中的WSD标记为错误。这是由于默认情况下特殊的编译标志位被开启。

不存在WSD的Singleton实现
开发者从其它平台移植代码通常遇到的问题是Symbian OS DLL中对WSD的限制会影响经典单例设计模式的实现。单例设计模式无疑是经典设计模式书籍[R3]中最流行的设计模式。它是一种最简单的设计模式,只调用一个类来提供全局指针访问某一实例,该示例自己完成实例化。



在C++中使用WSD的单例设计模式的经典实现如下所示:

class Singleton
  {
public:
  static Singleton* Instance();
  ... // Singleton提供的操作
private: // 为了表示清楚,这些函数没有实现
  Singleton();
  ~Singleton();
private: // 静态Singleton成员变量
  static Singleton* pInstance_;
  };

/*static*/ Singleton* Singleton::pInstance_ = NULL;

/*static*/ Singleton* Singleton::Instance()
  {
  if (!pInstance_)
    pInstance_ = new Singleton();
   
  return (pInstance_);
  }
  如前所述,在Symbian OS v9的DLL中实现Singleton是可能的,这可以通过在MMP文件中显式使能WSD来实现。你可以按照上述代码,使用Symbian C++命名规范和处理实例化过程中异常退出的标准用语来定义一个Singleton类。

然而,如果你希望避免潜在的额外内存消耗和仿真器测试限制,还有一种可供选择的机制可用:即线程本地存储(TLS)。TLS可用于在所有Symbian OS版本的DLL中实现Singleton(如果需要也可用于EXE中)。TLS是单独的线程存储区域,大小为一个机器字(在Symbian OS v9中为32比特)。一个指向本地的,堆存储的Singleton对象的指针保存于TLS区域,一旦需要访问该Singleton对象,就可以使用TLS 中的该指针。

访问TLS的操作位于类Dll中,该类位于e32std.h:

static TInt SetTls(TAny* aPtr); // 设置TLS数据
static TAny* Tls(); // 获得保存于TLS中的指针
static void FreeTls(); // 清除TLS数据
我们马上来看一下使用TLS的Singleton的典型实现。但是首先,这么做有什么缺点呢?简而言之,是运行时性能的降低。从TLS中获取数据比直接访问慢大约30倍,这是因为该查找过程通过执行调用转移至内核执行程序(更多信息参见[R4])。

此外,每个线程中TLS只有一个空位。如果线程中TLS移为他用,那么所有的TLS数据必须放置一个单独的类中,并且通过TLS指针访问。这可能很难维持(尽管对于应用程序开发者来说有解决方案,正如随后部分Singleton for Application Developers所描述的)。

在Symbian OS中,Singleton的实现必须考虑实例化失败的可能性(例如,没有足够的内存分配给Singleton实例时,就会发生异常退出)。一种可选方法是提供两个分离的函数:

一个工厂函数,NewL(),用于实例化Singleton实例并将其保存在TLS空位中。这可能失败,所以调用者必须处理所有可能发生的异常退出。
一个分离的不会发生异常退出的函数,Instance(),该函数用于在Singleton实例化之后通过从TLS中获取对象位置来访问该实例。
该方法为调用者提供更多的灵活性。为了实例化Singleton实例,只需要调用可能发生失败的函数。在实例化过程中,Singleton保证以引用形式返回,所以不要求指针检查或安全退出代码。

class CSingleton : public CBase
  {
public:
  // 创建Singleton实例
  IMPORT_C static void NewL();
  // 访问Singleton实例
  IMPORT_C static CSingleton& Instance();
private: // 为了表示清楚,这些函数没有实现
  CSingleton(); 
  ~CSingleton(); 
  void ConstructL();
  };

EXPORT_C /*static*/ void CSingleton::NewL()
  {
  if (!Dll::Tls()) // 不存在Singleton实例。创建一个。
    {
    CSingleton* singleton = new(ELeave) CSingleton();
    CleanupStack::PushL(singleton);
    singleton->ConstructL();
    User::LeaveIfError( Dll::SetTls(static_cast<TAny*>(singleton)) );
    CleanupStack::Pop(singleton);
    }
  }

EXPORT_C /*static*/ CSingleton& CSingleton::Instance()
  {
  CSingleton* singleton = static_cast<CSingleton*>(Dll::Tls());
  ASSERT(singleton); // 调试编译下出现严重错误
  return (*singleton);
  }
为了更加符合经典模式,Singleton的一部分实现更趋向于提供单独的可能发生异常退出的访问函数InstanceL()。

class CSingleton : public CBase
  {
public:
  // 访问/创建Singleton实例
  IMPORT_C static CSingleton& InstanceL();
private: // 为了表示清楚,这些函数没有实现
  CSingleton(); 
  ~CSingleton(); 
  void ConstructL();
  };

EXPORT_C /*static*/ CSingleton& CSingleton::InstanceL()
  {
  CSingleton* singleton = static_cast<CSingleton*>(Dll::Tls());
  if (!singleton) // 不存在Singleton实例。创建一个。
    {
    singleton = new(ELeave) CSingleton();
    CleanupStack::PushL(singleton);
    singleton->ConstructL();
    User::LeaveIfError( Dll::SetTls(static_cast<TAny*>(singleton)) );
    CleanupStack::Pop(singleton);
    }
  return (*singleton);
  }
该方法的优点在于实现部分可以定制,例如,执行引用计数。然而,每次调用InstanceL()都必须考虑异常退出的可能性,这为调用者增加了负担,并且由于大量使用TRAP活在更复杂的代码中使用清理栈而潜在地降低效率。

在Symbian OS v9之前,应用程序都是DLL,都无法使用WSD。Singleton基于TLS的实现被认为是在经典模式下使用WSD的一种直接的替代方式。为了保证该过程的简易型,Symbian OS为应用程序开发者提供额外的机制,这在下面的Singleton:应用程序开发者必读部分得到讨论。

从Symbian OS v9开始,应用程序是EXE而不是DLL,因此在应用程序代码中使用WSD不再受限制。

多线程代码
注意,以上所示的TLS实现正常工作的前提是只有一个线程需要访问Singleton。正如“线程本地存储”这个名字本身的含义,TLS使用的存储字相对于线程来说是本地的;一个进程中的每一个线程都有自己的存储位置。在多线程环境中如果要使用TLS访问一个Singleton对象,那么引用该 Singleton对象位置的指针必须传递给每一个线程,并且保存在TLS空位中。这可以在每个新线程创建的时候使用RThread::Create() 的适当参数实现。如果该操作没有实现,当新线程调用Dll::Tls()获得Singleton位置时,该函数会返回一个NULL指针。

因此Singleton的创建必须由父线程来管理。父线程在其他线程存在之前创建Singleton,并且在其它线程创建的时候将Singleton的位置进行传递。这些线程必须使用Dll:SetTls()来保存Singleton的位置。

让我们来看看下面的代码,它对以上机制的工作方式进行说明。首先,CSingleton类输出两个附加函数,通过调用代码,这两个函数可以用来从主线程获取Singleton实例指针(SingletonPtr()),然后将其在创建的线程中进行设置(InitSingleton())。

class CSingleton : public CBase
  {
public:
  IMPORT_C static void NewL();
  IMPORT_C static CSingleton& Instance();
  // 传递Singleton位置至新线程
  IMPORT_C static TAny* SingletonPtr();
  // 在新线程中初始化对Singleton的访问
  IMPORT_C static TInt InitSingleton(TAny* aLocation);
private:
  CSingleton();
  ~CSingleton();
  void ConstructL();
  };

EXPORT_C TAny* CSingleton::SingletonPtr()
  {
  return (Dll::Tls());
  }

EXPORT_C TInt CSingleton::InitSingleton(TAny* aLocation)
  {
  return (Dll::SetTls(aLocation));
  }

// 为了表示清楚,忽略其它函数
// NewL()和Instance()可参见前述代码

为了解释清楚,这里附上进程主线程的基本代码,该主线程创建一个Singleton实例,然后创建此线程,并把Singleton的位置传递给它。


// 主(父)线程创建Singleton
CSingleton::NewL();
// 创建次线程       
RThread childThread;
User::LeaveIfError(childThread.Create(_L("childThread"),
                   ChildThreadEntryPoint, KDefaultStackSize,
                                 KMinHeapSize, KMaxHeapSize,
                               CSingleton::SingletonPtr()));

CleanupClosePushL(childThread);
       
// 恢复thread1,等等...

注意,线程创建函数以CSingleton::SingletonPtr()的返回值作为参数值。该参数值必须传入位于子线程进入点函数的CSingleton::InitSingleton()中。

TInt ChildThreadEntryPoint(TAny* aParam)
  {// 在TLS中保存Singleton的位置
  if ( CSingleton::InitSingleton(aParam)==KErrNone )
    {// 成功,正常运行
    ...       
    }
       
  return (0);
  }
Singleton:应用程序开发者必读
Symbian OS提供类CCoeStatic来帮助应用程序开发者将其它平台中使用WSD的应用程序代码进行移植。在Symbian OS的早期(Symbian OS v9之前),应用程序都是DLL并且不允许使用WSD,该类是非常有用的。现在,在Symbian OS v9中,已经没有必要使用这个类,因为应用程序是EXE并且可以使用WSD。然而,如果你决定使用TLS,移植工作也很简单,并且允许不止一个DLL线程使用一个TLS空位。

该方法很直接——只需要从CCoeStatic继承你的Singleton类。例如:

class CAppSingleton : public CCoeStatic
  {
public:
  static CAppSingleton& InstanceL();
  static CAppSingleton& InstanceL(CCoeEnv* aCoeEnv);
private:
  CAppSingleton();
  ~CAppSingleton();
  };
  该类的实现必须将自身与UID关联起来,以允许Singleton实例“注册”到应用程序框架中(类CCoeEnv)。当Singleton对象实例化后,CCoeStatic基类构造函数将该对象添加至CCoeEnv保存的Singleton列表中。在内部,CCoeEnv使用TLS来保存每个注册 Singleton对象的指针(使用包含指向CCoeStatic派生对象指针的双向链表)。因此,对于类CAppSingleton:

const TUid KUidMySingleton = {0x10204232};

// "register"singleton
CAppSingleton::CAppSingleton()
: CCoeStatic(KUidMySingleton, CCoeStatic::EThread)
  {}

// 使用CCoeStatic::Static()访问Singleton
CAppSingleton& CAppSingleton::InstanceL()
  {
  CAppSingleton* singleton =
    static_cast<CAppSingleton*>(CCoeStatic::Static(KUidMySingleton));
  if (!singleton)
    {// 忽略二阶段构造
    singleton = new(ELeave) CAppSingleton();
    }
  return (*singleton);
  }

// 使用CCoeStatic::FindStatic()访问Singleton
CAppSingleton& CAppSingleton::InstanceL(CCoeEnv* aCoeEnv)
  {
  CAppSingleton* singleton = static_cast<CAppSingleton*>
    (aCoeEnv->FindStatic(KUidMySingleton));
 
  if (!singleton)
    {// 忽略二阶段构造
    singleton = new(ELeave) CAppSingleton();
    }
  return (*singleton);
  }

一旦CAppSingleton类完成实例化,就可以通过InstanceL()函数访问它,也可以直接调用CCoeEnv::Static()或 CCoeEnv::FindStatic()进行访问。注意前者是静态函数,所以可以在CCoeEnv指针不可用的应用程序中使用。


// 来自coemain.h
static CCoeStatic* Static(TUid aUid);
CCoeStatic* FindStatic(TUid aUid);

这些函数遍历双向链表,试图将CCoeStatic派生对象和应用程序UID进行匹配。CCoeStatic只能被运行于应用程序框架内部的代码使用,例如控件环境(CONE)。

Singleton清理
本文中讨论的所有实现都声明Singleton类的析构函数为私有函数,并且返回的是Singleton引用而不是指针。这是因为,如果析构函数是公共的,并且返回的是Singleton实例的指针,那么它可能被其它调用者无意销毁,使得Instance()函数处理已经删除示例的“虚引用”。给出的实现防止了这种情况的发生,并且让Singleton类负责Singleton实例的创建、所有权,以及最终的清理。

清理的通常方法是使用标准C程序库提供的atexit函数与清理函数进行注册,这些清理函数在进程终结的时候被显式调用。清理函数可以是Singleton类的成员函数,简单地删除Singleton实例。更多详情请参见 [R5]。

然而,你也许希望在进程终结之前销毁Singleton(例如,如果该对象不再需要,就可以释放其占有的内存空间)。在这种情况下,你必须考虑引用计数,以避免由于过早删除造成的“虚引用”,正如[R5]讨论的一样。

更多信息
本次讨论部分选自即将出版的Symbian出版社书籍,Common Design Patterns on Symbian OS中的单例模式说明部分。需要关于该书的更多信息,请查看维基主页的插入URL/FURL。

参考书目
[R1] Symbian OS对DLL中可写静态数据的支持,Hamish Willee,2008年1月。

[R2] 如何发现WSD的疏忽使用?

[R3] 设计模式:可复用面向对象软件的基础,Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides,Addison Wesley,1995年。

[R4] Symbian OS内核结构,Jane Sales et al,John Wiley & Sons,2005年。

[R5] C++设计新思维:泛型编程与设计模式之应用,Andrei Alexandrescu,Addison-Wesley Professional 2001年。

贡献者
我要感谢Adrian Issott,Mark Jacobs,Antti Juustila,Hamish Willee和Tim Williams,他们为本文的很多方面提供反馈意见。
Online: 2 users, 20 guests

原帖地址http://www.devdiv.net/viewthread.php?tid=5377&extra=page%3D1%26amp%3Bfilter%3Dtype%26amp%3Btypeid%3D20
分享到:
评论

相关推荐

    Java线程安全的Singleton模式:深入分析与实现

    在Java中创建线程安全的Singleton模式是一项重要的任务,尤其是在多线程环境中。通过使用饿汉式、懒汉式与双重检查锁定、静态内部类或枚举等方式,可以确保Singleton实例的唯一性和线程安全。每种方式都有其适用场景...

    多线程下的singleton

    总结起来,多线程下的Singleton设计需要考虑线程安全问题,通过如双重检查锁定、静态内部类或枚举等方式实现线程安全的单例模式。同时,要注意单例模式可能带来的潜在问题,并根据实际情况选择合适的设计策略。

    singleton设计模式java实现及对比

    在Java中,Singleton模式的实现有多种方式,每种方式都有其优缺点,我们将详细探讨这些实现方法并进行对比。 ### 1. 饿汉式(Static Final Field) 这是最简单的Singleton实现方式,通过静态初始化器在类加载时就...

    C++中多线程与Singleton的那些事儿

     前段时间在网上看到了个的面试题,大概意思是如何在不使用锁和C++11的情况下,用C++实现线程安全的Singleton。  看到这个题目后,第一个想法是用Scott Meyer在《Effective C++》中提到的,在static成员函数中...

    C++完美实现Singleton模式

    在多线程环境中,上述实现可能会导致竞态条件,即多个线程可能同时尝试创建Singleton对象。为了解决这一问题,可以采用双重检查锁定(Double-Checked Locking)模式来确保线程安全性: ```cpp class Singleton { ...

    C++ 实现的singleton 模式

    在多线程环境中,如果没有适当的同步机制,`getInstance()`可能会在不同线程中同时创建多个实例。在C++11及更高版本中,我们可以使用`std::call_once`和`std::once_flag`来确保实例只在第一次调用时创建: ```cpp #...

    Java多线程编程环境中单例模式的实现

    然而,在多线程环境中实现单例模式时,需要特别注意线程安全问题。本文将详细介绍如何在Java多线程编程环境中正确实现单例模式。 #### 单例模式的惰性加载 在设计单例模式时,我们经常需要考虑的一个问题是何时...

    单例实现源码singleton-C++

    总结来说,C++中的单例模式实现多种多样,选择哪种方式取决于具体的应用场景,如是否考虑多线程、内存占用、初始化时机等。通过理解这些实现方式,我们可以更好地设计和使用单例模式,以满足软件的高效、稳定和灵活...

    完美Singleton实现

    3. **线程安全问题**:上述实现方式在多线程环境中可能会出现问题,即多个线程同时进入`if (_instance == nullptr)`判断时,可能会导致创建多个Singleton实例。 ##### 2.2 引入智能指针改进 为了改进这些问题,...

    Java并行(4):线程安全前传之Singleton1

    在多线程环境中,实现线程安全的Singleton至关重要,因为不正确的实现可能导致多个实例的创建,违背了Singleton的基本原则。 1. 寂寞的Singleton Singleton通常采用静态内部类、枚举或懒汉式(Lazy Initialization...

    .Net 多线程详解

    .doc 格式 详细解析多线程技术 基础篇 • 怎样创建一个线程 • 受托管的线程与 Windows线程 • 前台线程与后台线程 • 名为BeginXXX和EndXXX的方法是做什么用的 • 异步和多线程有什么关联 WinForm多线程编程...

    C++类中创建多线程实现本地和远程打印

    在C++编程中,创建多线程是一种常见的方式,用于实现并发执行多个任务,比如这里的本地和远程打印。本示例中的代码可能涉及到以下几个关键知识点: 1. **多线程**:C++11及更高版本引入了`std::thread`库来支持线程...

    多线程服务器的几种常用模型

    在多线程环境中实现Singleton时需要注意线程安全性。 一种简单的线程安全Singleton实现方法是使用双重检查锁定(double-checked locking)技术: ```cpp class Singleton { private: static Singleton* instance; ...

    多线程单例模式并发访问

    - **懒汉式**:通过静态内部类或者双重检查锁定的方式来实现线程安全的懒加载单例模式,即在第一次使用时才初始化。 #### 十、线程安全的懒汉式单例 懒汉式单例在实现线程安全性时,通常采用双重检查锁定技术: ``...

    C# 多线程讲解的基础概念

    线程安全是指在多线程环境中,如何保护共享资源免受其他线程的干扰。线程安全可以使用锁定机制来实现,例如使用 lock 语句、Mutex 对象等。 六、锁定机制 锁定机制是指在多线程环境中,如何保护共享资源免受其他...

    Loki库中SingletonHolder的多线程改进

    多线程情况下对该singleton对象创建操作的串行化,没有对singleton对象访问的操作进行串行化。 这个包就是修正这个问题的。只不过访问方式要从 CMyclass::instance().DoSomething() 改成 CMyclass::instance()-&gt;...

    .NET多线程详解及源码

    .NET框架中的多线程是构建高性能、响应迅速的应用程序的关键技术。本文将深入探讨.NET多线程的基础知识、WinForm多线程编程、线程池的使用以及同步机制,同时涉及Web和IIS中线程的运用。 1. **基础篇 - 创建线程** ...

    单例模式下,使用多线程实现

    1. **双重检查锁定(Double-Check Locking)**:这是在多线程环境下实现线程安全单例的常用方法。代码中,首先进行一次实例的检查,如果实例不存在,才进行同步块内的创建。这样可以减少不必要的同步开销,提高效率...

Global site tag (gtag.js) - Google Analytics