shared_ptr实现和线程安全分析

shared_ptr实现和线程安全分析,第1张

使用该智能指针(或者其他两种)需要导入头文件 #include <memory>

除此之外还可以对 shared_ptr 赋值,通过重写 operator= 实现。 需要注意 ,对于 p1=p2 (均为智能指针)这种, p2 所指对象由于被 p1 指向,所以该引用计数会加一, p1 原本指向的资源的引用计数会减一。这也会引出下面关于 shared_ptr 指针的 线程安全 问题。

运行结果如下:

这里实现的还有一些问题,因为 shared_ptr 源码中关于引用计数是 原子 *** 作 ,不需要考虑资源使用冲突的问题,可以在自己实现的时候加锁。

首先什么是线程安全?

简单来说就是 多个线程 *** 作一个共享数据,都能按照预期的行为进行,无论多个线程的运行次序如何交织。

对于 shared_ptr ,其内部有两个变量,引用计数和真正的对象类型指针。其中引用计数是原子 *** 作,所以 对于 shared_ptr 的读 *** 作是线程安全的。

但是对于 shared_ptr 中赋值如 ptr1 = ptr2 ,需要两个步骤, 1、 ptr1 的内部对象指针 Obj1 替换成 ptr2 内部对象 Obj2 指针;2、 ptr1 的对于 Obj1 的引用计数缓存 Obj2 的引用计数。

这两步并不是原子的,如果一个线程需要对 shared_ptr 进行赋值 *** 作 ptr1 = ptr2 ,刚完成第一步,就切换到其他线程又对ptr2进行 *** 作,如 ptr2 = ptr3 ,就有可能造成析构了引用计数。而继续之前线程的第二步,就会出错。

总之:对于 shared_ptr 的读 *** 作是线程安全的。

对于 shared_ptr 读写 *** 作不是线程安全的,需要加锁。

tips:为什么 shared_ptr 的引用计数能够同步到不同的指针中?

有人回答可能使用的是static变量,这是不可能的,因为一个类中只有一个静态变量,只能记录对于一个对象的引用次数,这在包含两个 shared_ptr 以上的程序中是不可行的。

个人认为是引用计数是用指针实现的,指向一个记录引用次数的对象。

程序(program)是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合。为实现预期目的而进行 *** 作的一系列语句和指令。一般分为系统程序和应用程序两大类。 计算机中的程序在港澳台地区称为程式。程序就是为使电子计算机执行一个或多个 *** 作,或执行某一任务,按序设计的计算机指令的集合

进程是 *** 作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体;由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元。

线程(thread, 台湾称 执行绪)是"进程"中某个单一顺序的控制流。也被称为轻量进程(lightweight processes)。计算机科学术语,指运行中的程序的调度单位。

Java 中线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。合理的使用线程池可以带来多个好处:

(1) 降低资源消耗 。通过重复利用已创建的线程降低线程在创建和销毁时造成的消耗。

(2) 提高响应速度 。当处理执行任务时,任务可以不需要等待线程的创建就能立刻执行。

(3) 提高线程的可管理性 。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程池的处理流程如上图所示

线程池中通过 ctl 字段来表示线程池中的当前状态,主池控制状态 ctl 是 AtomicInteger 类型,包装了两个概念字段:workerCount 和 runState,workerCount 表示有效线程数,runState 表示是否正在运行、正在关闭等状态。使用 ctl 字段表示两个概念,ctl 的前 3 位表示线程池状态,线程池中限制 workerCount 为(2^29 )-1(约 5 亿)个线程,而不是 (2^31)-1(20 亿)个线程。workerCount 是允许启动和不允许停止的工作程序的数量。该值可能与实际的活动线程数暂时不同,例如,当 ThreadFactory 在被询问时未能创建线程时,以及退出线程在终止前仍在执行记时。用户可见的池大小报告为工作集的当前大小。 runState 提供主要的生命周期控制,取值如下表所示:

runState 随着时间的推移而改变,在 awaitTermination() 方法中等待的线程将在状态达到 TERMINATED 时返回。状态的转换为:

RUNNING -> SHUTDOWN 在调用 shutdown() 时,可能隐含在 finalize() 中

(RUNNING 或 SHUTDOWN)-> STOP 在调用 shutdownNow() 时

SHUTDOWN -> TIDYING 当队列和线程池都为空时

STOP -> TIDYING 当线程池为空时

TIDYING -> TERMINATED 当 terminate() 方法完成时

开发人员如果需要在线程池变为 TIDYING 状态时进行相应的处理,可以通过重载 terminated() 函数来实现。

结合上图说明线程池 ThreadPoolExecutor 执行流程,使用 execute() 方法提交任务到线程池中执行时分为4种场景:

(1)线程池中运行的线程数量小于 corePoolSize,创建新线程来执行任务。

(2)线程池中运行线程数量不小于 corePoolSize,将任务加入到阻塞队列 BlockingQueue。

(3)如果无法将任务加入到阻塞队列(队列已满),创建新的线程来处理任务(这里需要获取全局锁)。

(4)当创建新的线程数量使线程池中当前运行线程数量超过 maximumPoolSize,线程池中拒绝任务,调用 RejectedExecutionHandlerrejectedExecution() 方法处理。

源码分析:

线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后,还会循环获取工作队列里的任务来执行。

创建线程池之前,首先要知道创建线程池中的核心参数:

corePoolSize (核心线程数大小):当提交任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,直到需要执行的任务数大于核心线程数时就不再创建。

runnableTaskQueue (任务队列):用于保存等待执行任务的阻塞队列。一般选择以下几种:

ArrayBlockingQueue:基于数组的有界阻塞队列,按照 FIFO 原则对元素进行排序。

LinkedBlockingQueue:基于链表的阻塞队列,按照 FIFO 原则对元素进行排序。

SynchronousQueue:同步阻塞队列,也是不存储元素的阻塞队列。每一个插入 *** 作必须要等到另一个 线程调用移除 *** 作,否则插入 *** 作一直处于阻塞状态。

PriorityBlockingQueue:优先阻塞队列,一个具有优先级的无限阻塞队列。

maximumPoolSize (最大线程数大小):线程池允许创建的最大线程数,当队列已满,并且线程池中的线程数小于最大线程数,则线程池会创建新的线程执行任务。当使用无界队列时,此参数无用。

RejectedExecutionHandler (拒绝策略):当任务队列和线程池都满了,说明线程池处于饱和状态,那么必须使用拒绝策略来处理新提交的任务。JDK 内置拒绝策略有以下 4 种:

AbortPolicy:直接抛出异常

CallerRunsPolicy:使用调用者所在的线程来执行任务

DiscardOldestPolicy:丢弃队列中最近的一个任务来执行当前任务

DiscardPolicy:直接丢弃不处理

可以根据应用场景来实现 RejectedExecutionHandler 接口自定义处理策略。

keepAliveTime (线程存活时间):线程池的工作线程空闲后,保持存活的时间。

TimeUnit (存活时间单位):可选单位DAYS(天)、HOURS(小时)、MINUTES(分钟)、MILLISECONDS(毫秒)、MICROSECONDS(微妙)、NANOSECONDS(纳秒)。

ThreadFactory (线程工厂):可以通过线程工厂给创建出来的线程设置有意义的名字。

创建线程池主要分为两大类,第一种是通过 Executors 工厂类创建线程池,第二种是自定义创建线程池。根据《阿里java开发手册》中的规范,线程池不允许使用 Executors 去创建,原因是规避资源耗尽的风险。

创建一个单线程化的线程池

创建固定线程数的线程池

以上两种创建线程池方式使用链表阻塞队列来存放任务,实际场景中可能会堆积大量请求导致 OOM

创建可缓存线程池

允许创建的线程数量最大为 IntegerMAX_VALUE,当创建大量线程时会导致 CPU 处于重负载状态和 OOM 的发生

向线程池提交任务可以使用两个方法,分别为 execute() 和 submit()。

execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。execute() 方法中传入的是 Runnable 类的实例。

submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get() 方法来获取返回值。get() 方法会阻塞当前线程直到任务完成,使用 get(long timeout, TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完。

可以通过调用线程池的 shutdown() 或shutdownNow() 方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用 interrupt() 方法来中断线程,所以无法响应中断任务可能永远无法终止。

shutdown() 和 shutdownNow() 方法的区别在于 shutdownNow 方法首先将线程池的状态设置为 STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

线程池使用面临的核心的问题在于: 线程池的参数并不好配置 。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO 密集型和 CPU 密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。

(1)以任务型为参考的简单评估:

假设线程池大小的设置(N 为 CPU 的个数)

如果纯计算的任务,多线程并不能带来性能提升,因为 CPU 处理能力是稀缺的资源,相反导致较多的线程切换的花销,此时建议线程数为 CPU 数量或+1;----为什么+1?因为可以防止 N 个线程中有一个线程意外中断或者退出,CPU 不会空闲等待。

如果是 IO 密集型应用, 则线程池大小设置为 2N+1 线程数 = CPU 核数 目标 CPU 利用率 (1 + 平均等待时间 / 平均工作时间)

(2)以任务数为参考的理想状态评估:

1)默认值

2)如何设置 需要根据相关值来决定 - tasks :每秒的任务数,假设为500~1000 - taskCost:每个任务花费时间,假设为01s - responsetime:系统允许容忍的最大响应时间,假设为1s

以上都为理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器 cpu load 已经满了,则需要通过升级硬件和优化代码,降低 taskCost 来处理。

(仅为简单的理想状态的评估,可作为线程池参数设置的一个参考)

与主业务无直接数据依赖的从业务可以使用异步线程池来处理,在项目初始化时创建线程池并交给将从业务中的任务提交给异步线程池执行能够缩短响应时间。

严禁在业务代码中起线程!!!

当任务需要按照指定顺序(FIFO, LIFO, 优先级)执行时,推荐创建使用单线程化的线程池。

本文章主要说明了线程池的执行原理和创建方式以及推荐线程池参数设置和一般使用场景。在开发中,开发人员需要根据业务来合理的创建和使用线程池达到降低资源消耗,提高响应速度的目的。

原文链接:>

首先我们搞清楚什么是程序、什么是进程、什么是线程?然后我们罗列一下可能的规划。

程序:代码实现了功能,就是程序,是静态的;程序是存储在磁盘上。

进程:执行中的程序就是进程,是动态的;

线程:进程内的一个执行单元,也是进程内的可调度实体,可以并发执行提高了进程的效率。

逻辑是静态的代码, 线程 进程是逻辑执行单元实体 ,cpu硬件才是真正的执行器

客户端触发者也需要进程线程cpu

人 浏览器

call rpc

调度

通讯层

业务逻辑层

数据层

简单示例

curl tomcat

进程分析 两个

客户端curl进程

服务端jvm进程

线程分析 一对多

curl 单进程单线程

服务端jvm进程里面线程就多了去了 简简单单也要一二十个

jvm线程组

tomcat线程组

worker线程组

为什么服务端程序这么多线程,都是干什么用的?

真实场景线程更多

精灵辅助线程

连接池维护

心跳维护

业务处理线程池

批处理线程池

如果需要更多组件

正向反向代理

缓存加速

数据库

消息服务器 日志归集 报警监控

启动更多的进程 线程 那就需要更多的计算资源cpu

Java线程堆栈是一个运行中的Java应用程序的所有线程的一个快照。它会显示一些像当前的堆栈跟踪、状态以及线程名称之类的信息。线程列表中包括由JVM本身创建的线程(负责垃圾收集、信号处理等管理工作)和由应用程序创建的线程。

通过给JVM发送一个SIGQUIT信号,您可以得到一个线程堆。在Unix *** 作系统(Solaris/Linux/HP-Unix等)中,通过kill-3<pid>命令可以得到线程堆,(在启动脚本中将输出重定向到文件中是一个很好的习惯,startsh>tracelog 2>&1)。在Windows *** 作系统中,您可以在命令窗口键入ctrl-break得到线程堆。线程堆会输出到JVM的stdout或者stderr。输出出线程堆之后,应用程序继续正常运行。当您给JVM发送SIGQUIT信号时,JVM的信号处理器会通过输出线程堆来响应这一信号。当程序运行的时候,您可以在任何点得到线程堆。

以上就是关于shared_ptr实现和线程安全分析全部的内容,包括:shared_ptr实现和线程安全分析、请解释程序、进程和线程这三个概念,可以举例或比喻说明。同时写出线程的五种状态。、超详细的线程池使用解析等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/9491037.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-28
下一篇 2023-04-28

发表评论

登录后才能评论

评论列表(0条)

保存