单例模式及其应用场景

单例模式是一种常见的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于协调系统整体的行为。例如在服务器程序中,该服务器的配置信息存放在一个文件中,这些配置信息由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这样能够简化复杂环境下的配置管理

如何实现一个单例模式

一个类能返回对对象的一个引用(永远是同一个)和一个获得该实例的方法(该方法是静态方法,通常使用GetInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空,就返回这个应用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

此外,我们还需要将将该类的构造函数定义为私有,这样其他地方的代码就无法通过调用该类的构造函数来实例化这个类对象,只有通过该类提供的静态方法来得到该类的唯一实例。

多线程下的单例模式

当唯一实例没有创建时,有两个线程同时调用创建方法,即都没有检测到唯一的实例存在,并各自创建了一个实例

,违反了单例模式中实例唯一的原则。解决办法就是为指示类是否已经实例化的变量提供互斥锁(牺牲一部分效率)

应用场景

1.外部资源:一台计算机连接多台打印机,但只能有一个PrinterSpooler,避免两个打印作业同时输出到打印机。

2.内部资源:大多数的软件都有一个属性文件存放系统配置,这样的系统应该有一个实例对象来管理这些属性文件。

3.Windows下的回收站和任务管理器都是典型的单例模式。

4.网站的计数器通常也是单例模式,否则无法实现同步。(时序合理性)

5.Linux只能有一个文件系统,因次文件系统也是单例模式。

6.多线程的线程池设计通常也是单例模式,方便线程池对池中的线程进行控制。

线程池

线程是程序执行流的最小单位,进程是一个动态的过程,是一个活动的实体,简单来说一个应用程序的运行就可以被看做是一个进程,而线程试运行中的实际的任务执行者,进程中包含了多个线程。

线程的生命周期

1.使用new thread方法创建一个线程,线程创建完毕进入Runnable(就绪状态),此时创建出来的线程抢占CPU资源的状态,当线程抢占到CPU的执行权后,线程进入运行状态(Running),当线程执行结束或是非正常的调用stop()方法后线程进入死亡状态。

2.线程阻塞的几种情况:

  • 当线程自身主动调用sleep()方法是,线程进入阻塞状态
  • 当线程调用了阻塞IO中的方法时,这个方法会有一个返回参数,参数返回之前线程是阻塞状态。
  • 当线程正在等待某个通知时,会进入阻塞状态

为何会有阻塞状态,由于CPU的资源十分宝贵,当线程正在执行某种不确定时间长度的任务时,CPU会收回执行权,从而合理分配CPU资源。当线程阻塞时间过去后会从新进入就绪状态,抢夺CPU资源(Sleep()时间过了,阻塞IO返回了参数等)。

多线程

创建多条线程同时执行多种任务,这种方式在我们的生活中比较常见。但是在多线程的使用过程中,我们需要理解并行与并发的区别。

并行与并发

并发:不同的代码块交替执行(单核CPU 时间片轮转)

并行:不同的代码块同时执行(多核CPU同时执行多个线程)

线程安全:如果多个线程同时对同一份资源进程操作,导致一份资源被一个线程操作后状态还没有来得及变化就被另一个线程拿走操作,造成数据的二义性。

构造线程池

由于线程的创建需要消耗内存资源和时间资源,为了解决频繁创建和销毁内存引入了内存池的概念。即创建一定数量的线程,在线程执行完某一个任务之后,不是立即销毁这个线程,而是重复利用该线程去执行其他的任务。

线程池的组成主要有三个部分:

  • 线程队列
    1.用于存放被创建的线程
    (1)正在执行任务(运行态)
    (2)等待分配任务(阻塞态)
  • 任务队列
    将新的任务添加到任务队列的最后,并通知空闲线程从队列的最前端取任务执行
  • 控制器
    管理着队列锁和信号量
    1.队列锁–多线程对同一个任务队列进行操作是会产生竞争,所以对任务队列执行存,取操作是都需要加锁
    2.信号量–在任务队列获取到新任务时,会产生信号量,某一个阻塞状态的线程会同时获取到队列锁和信号量从而解除阻塞状态,并取用任务执行。
    主要方法
    1.添加任务到任务队列–add_task()
    2.通知线程有新的任务–run_task()
    3.销毁线程池开辟的所有资源并且结束此次多任务–destory_pool()