写点什么

性能优化、线程、锁

用户头像
ashuai1106
关注
发布于: 2020 年 07 月 22 日
性能优化、线程、锁

性能优化

从前面我们了解到,软件开发的最终目标是满足用户的需求,不只要有功能需求,而且要有品质需求-满足高扩展、高可用、高性能等指标。

架构设计的宗旨是降本增效,当系统有问题时,有2个技术方向进行优化,垂直伸缩和水平伸缩。

从上图中,可以看出我们在前期,首先要以垂直伸缩为主要出发点,优化基础设施-网络、硬件、操作系统等。经过测试,单台的最大并发量是多少,是否已经达到最大负载,同时也为容量规划打好基础。

其次,由于垂直伸缩受限于硬件的技术水平,是有上限的,然后考虑再进行水平伸缩。

在优化资源配置之前,首先要进行的就是性能测试。因为

  • 不能优化一个没有测试的软件

  • 不能优化一个你不了解的软件

性能测试

概念

性能测试是性能优化的前提和基础,也是性能优化结果的检查和度量标准。不同视角下的网站性能有不同的标准,也有不同的优化。细分为

  • 性能测试:以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统在资源可接受范围内,是否能达到性能预期。

  • 负载测试:对系统不断地增加并发请求增加系统压力,直到系统的某项或多项性能指标达到安全临界值,如某种资源已经呈饱和状态,这时候继续对系统施加压力,系统的处理能力不能提高,反而会下降。

  • 压力测试:超过安全负载的情况下,对系统继续施加压力,直到系统崩溃或不能再处理任何请求,以此获得系统最大压力承受能力。

  • 稳定性测试:被测试系统在特定硬件、软件、网络环境条件下,给系统加载一定业务压力,使系统运行一段较长时间,以此检测系统是否稳定。在生产环境,请求压力不均匀的,呈波浪线。因此为了更好地模拟生产环境,稳定性测试也应不均匀地对系统施加压力。

  • 全链路压测:就是在特定的业务场景下,将相关的链路完整的串联起来同时施压,尽可能模拟出真实的用户行为,当系统整站流量都被打上来的时候,必定会暴露出性能瓶颈,才能探测出系统整体的真实处理能力,以及有指导的在大促前进行容量规划和性能优化,这便是线上实施全链路压测的真正目的。

手段上有:数据构造、数据隔离(逻辑隔离、虚拟隔离、物理隔离)、流量构造、全链路平台化等。

关系图如下:



  1. 主观视角:用户感受到的性能

  2. 客观视角:性能指标衡量的性能



系统吞吐量几个重要参数:QPS(TPS)、并发数、响应时间

        QPS(TPS):每秒钟request/事务的数量

        并发数: 系统同时处理的request/事务数

        响应时间:  一般取平均响应时间

关系图如下:



公式

QPS(TPS)= 并发数/平均响应时间  

并发数 = QPS*平均响应时间

吞吐量= (1000/响应时间ms ) * 并发数



一个系统吞吐量通常由QPS(TPS)、并发数两个因素决定,每套系统这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降。

  1. 并发数不变:QPS不断增加,QPS超过最大吞吐量后,会有大量请求等待,平均响应时间急剧下降

  2. QPS不变:增加并发数,会导致CPU并发线程过多,线上上下文切换频繁,内存消耗增加,从而使平均响应时间下降



优化思路

性能优化的一般方法

  • 性能测试,获得性能指标

  • 指标分析,发现瓶颈点

  • 架构与代码分析

  • 优化关键技术点

  • 性能测试,进入性能优化闭环

分层思想

  • 网络性能优化-异地多活机房架构、专线网络与自主CDN建设

  • 硬件性能优化-cpu、磁盘、内存、网卡,数量级的,举例 spark作业需要传输大量数据,资源瓶颈分析,大量消耗在网络传输——升级网卡

  • 操作系统性能优化- 大量cpu操作为sys类型,消耗大量计算资源,部分linux版本tranparent huge page 缺省打开导致的。

  • 虚拟机性能优化-jvm中fgc的频繁出现,可以作为优化点

  • 基础组件性能优化-对容器jetty等的升级,可以大幅度提升性能,tomcat并发线程数增加、增加/减少数据库连接数、创建索引、http长链接等

  • 软件架构性能优化-充分利用缓存、异步、集群,以及池化思想、减少调用链等

  • 软件代码性能优化-spark stage时间长,初始化应用代码,每个excutor都加载一次,导致开销巨大,启用本地文件缓存模式来解决。



操作系统

操作系统由一系列具有不同控制和管理功能的程序组成,它是直接运行在计算机硬件上的、最基本的系统软件,是系统软件的核心。

在计算机发展初期,每台计算机是串行地执行任务的,如果碰上需要IO的地方,还需要等待长时间的用户IO,后来经过一段时间有了批处理计算机,其可以批量串行地处理用户指令,但本质还是串行,还是不能并发执行 60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。 因此在80年代,出现了能独立运行的基本单位——线程(Threads)



程序是静态的。程序运行起来以后,被称作进程。



进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。与内核线程1:1的线程模型。计算机的cpu核心数是有限的,但服务器可以处理成百上千的并发用户请求,计算机是如何做到的?进程(线程)的分时执行。

进程的状态包含

  • 运行

  • 就绪

  • 阻塞

三种状态的关系如下图:



线程

线程:一个进程可以包含多个线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。与内核线程N:1线程模型。

操作系统与线程的关系

cpu时间片:cpu分配给每个线程执行的时间段,一般几十毫秒,决定了线程占用cpu运行的时长

上下文切换:CPU的控制权CPU时间片用完一个线程自身或被迫暂停运行,另一个线程(就绪并等待)被选中开始或继续运行的过程,Java中调用sleep、wait、yield、join、park、synchronized、lock方法或关键字引发上下文切换

上下文:切入和切出过程中,操作系统要保存和恢复响应的进度信息(寄存器计数器存储内容)

操作系统与线程的关系如下图:

Java中进程与线程的关系:

  1. 运行一个程序会产生一个进程,进程至少包含一个线程。

  2. 每个进程对应一个JVM实例,多个线程共享JVM中的堆。

  3. Java采用单线程编程模型,程序会自动创建主线程 。

  4. 主线程可以创建子线程,原则上要后于子线

java中多线程运行时视图



线程的生命周期

线程的生命周期:创建、运行(就绪和真运行)、阻塞和终止。在java中定义了以下状态。

enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}



java中各个状态的转变关系如下图



  • 临界资源

各进程或线程采取互斥的方式,实现共享的资源称作临界资源,比如局部变量等

  • 临界区

每个进程或线程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程或线程进入。比如操作局部变量的语句块。

  • 线程安全

多个线程想要同时访问临界资源时,就会产生竞争条件(由于事件次序异常而造成对同一资源的竞争,从而导致程序无法正常运行时,就会出现“竞争条件”)。

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。反之则是线程不安全的。

 

锁不是开销的根源,竞争锁才是。

避免阻塞引起的崩溃的方式

  • 限流:控制进入计算机的请求数,减少创建的线程数。

  • 降级:关闭部分功能程序的执行,尽早释放线程。

  • 异步IO;无临界区(反应式编程,基于akka的actor编程等)



如何解决竞争条件?

  • 多线程并发协同

  • 线程安全(锁)

  • 原子类

为了解决线程安全问题引入了锁,但由于锁的存在,引发了竞争,为了优化竞争锁所带来的开销,就对锁进行了分类处理,这样让开销更小,线程执行的更快。



  • 总线锁:使用处理器的LOCK#信号,当一个处理器在内存总线上输出此信号的时候,其它处理器的请求将被阻塞,该处理器独占内存。

  • 缓存锁:指内存区域如果被缓存区在处理器的缓存命中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声明LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其它处理器回写已被锁定的缓存行数据时,会使缓存无效。



  • 偏向锁:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

  • 轻量级锁:指当锁是偏向锁时,被另一个线程访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能

  • 重量级锁:当锁是轻量级锁时,另一个线程虽然自旋,但自旋不会一直持续下去,当自旋到一定次数时,还没获取到锁,就会进入阻塞,该锁膨胀为重量级锁,重量级锁就会让其它申请的线程进入阻塞,性能降低。java中通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。



  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。优点是所有的线程都能得到资源,不会饿死在队列中。缺点是吞吐量下降很多,队列里面除了第一个线程,其它都会阻塞,cpu唤醒阻塞线程开销很大。



  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。



  • 可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,优点是可一定程度避免死锁。比如synchronized 和ReentrantLock锁,ReentrantLock一定要手动释放锁,并且加锁次数和释放次数要一样,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。



  • 独享锁/互斥锁:该锁一次只能被一个线程持有



  • 共享锁:该锁可以被多个线程锁持有

  • 读写锁:多个读线程之间并不互斥,而写线程则要求与任何线程互斥



  • 悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采用加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。比如数据库中的行锁、表锁等,读锁、写锁等,都是在操作之前加锁,java中sychronized和ReentrantLock等独占锁的实现也是悲观锁思想的实现。



  • 乐观锁:认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,检查是否已经被修改过,如果修改过,就放弃。比如java中 java.util.concurrent.atomic包下面的原子变量类使用了基于乐观锁机制的CAS实现。



  • 分段锁:设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组的一段进行加锁操作。比如 JDK1.7 ConcurrentHashMap是通过分段锁来实现高并发操作的。



  • 自旋锁: 让不满足条件的线程等待一会看能不能获得锁,通过占用处理器的时间来避免线程切换带来的开销,缺点是循环会消耗CPU。



以下为java中锁的体现:





jvm锁升级的过程



通过了解进程、线程、锁,可以更好帮助我们深入理解代码的深层实现逻辑,也有利于我们写出健壮高效的代码,对我们的软件开发工作有着不可或缺的指导意义。



reference

https://www.cnblogs.com/wangmo/p/8074879.html

用户头像

ashuai1106

关注

还未添加个人签名 2017.10.20 加入

还未添加个人简介

评论

发布
暂无评论
性能优化、线程、锁