应用服务器性能优化——代码优化-解决线程安全

版权声明:转载请随意! https://blog.csdn.net/qq_41723615/article/details/89019982

网站的业务逻辑实现代码主要部署在应用服务器上, 需要处理复杂的并发事务。合理优化业务代码,可以很好地改善网站性能。不同编程语言的代码优化手段有很多, 这里我们概要地关注比较重要的几个方面。

1 .多线程

多用户并发访问是网站的基本需求, 大型网站的并发用户数会达到数万,单台服务器的并发用户也会达到数百。CG1 编程时代, 每个用户请求都会创建一个独立的系统进程去处理。由于线程比进程更轻量, 更少占有系统资源, 切换代价更小, 所以目前主要的Web 应用服务器都采用多线程的方式响应并发用户请求, 因此网站开发天然就是多线程编程。

从资源利用的角度看, 使用多线程的原因主要有两个: IO阻塞与多CPU 。当前线程进行 IO处理的时候, 会被阻塞释放CPU 以等待 IO操作完成, 由于 IO 操作(不管是磁盘 IO 还是网络 IO )通常都需要较长的时间, 这时CPU 可以调度其他的线程进行处理。
前面我们提到,理想的系统Load 是既没有进程(线程)等待也没有CPU 空闲,利用多线程 IO阻塞与执行交替进行, 可最大限度地利用CPU 资源。使用多线程的另一个原因是服务器有多个CPU , 在这个连手机都有四核CPU 的时代, 除了最低配置的虚拟机,一般数据中心的服务器至少16 核CPU , 要想最大限度地使用这些CPU , 必须启动多线程。网站的应用程序→般都被Web 服务器容器管理,用户请求的多线程也通常被Web 服务器容器管理,但不管是Web 容器管理的线程,还是应用程序自己创建的线程, 一台服务器上启动多少线程合适呢?假设服务器上执行的都是相同类型任务,针对该类任务启动的线程数有个简化的估算公式可供参考:
启动线程数= [任务执行时间/ (任务执行时间- IO等待时间) ] xCPU 内核数
最佳启动线程数和CPU 内核数量成正比,和 IO 阻塞时间成反比。如果任务都是CPU计算型任务,那么线程数最多不超过CPU 内核数, 因为启动再多线程, CPU 也来不及;调度; 相反如果是任务需要等待磁盘操作, 网络响应, 那么多启动线程有助于提高任务并发度, 提高系统吞吐能力, 改善系统性能。多线程编程一个需要注意的问题是线程安全问题, 即多线程并发对某个资源进行修改, 导致数据混乱。这也是缺乏经验的网站工程师最容易犯错的地方章而线程安全Bug又难以测试和重现, 网站故障中, 许多严斤谓偶然发生的"灵异事件" 都和多线程并发问题有关。对网站而言, 不管有没有进行多线程编程, 工程师写的每一行代码都会被多线程执行,因为用户请求是并发提交的, 也就是说, 所有的资源一一对象、内存、文件、数据库, 乃至另一个线程都可能被多线程并发访问。

将对象设计为无状态对象:

所谓无状态对象是指对象本身不存储状态信息(对象无成员变量, 或者成员变量也是无状态对象) , 这样多线程并发访问的时候就不会出现状态不一致, Java Web 开发中常用的Servlet 对象就设计为无状态对象, 可以被应用服务器多线程并发调用处理用户请求。而Web 开发中常用的贫血模型对象都是些无状态对象。不过从面向对象设计的角度看, 无状态对象是一种不良设计。

使用局部对象:

即在方法内部创建对象, 这些对象会被每个进入该方法的线程创建,除非程序有意识地将这些对象传递给其他线程, 否则不会出现对象被多线程并发访问的情形。

并发访问资源时使用锁:

即多线程访问资源的时候, 通过锁的方式使多线程并发操作转化为顺序操作, 从而避免资源被并发修改。随着操作系统和编程语言的进步, 出现各种轻量级锁,使得运行期线程获取锁和释放锁的代价都变得更小, 但是锁导致线程同步顺序执行, 可能会对系统性能产生严重影响。

2. 资源复用

系统运行时, 要尽量减少那些开销很大的系统资源的创建和销毁, 比如数据库连接、网络通信连接、线程、复杂对象等。从编程角度, 资源复用主要有两种模式: 单例( Singleton )和对象池( Object Pool )。

单例虽然是GoF 经典设计模式中较多被话病的一个模式, 但由于目前Web 开发中主要使用贫血模式, 从Service 到Dao 都是些无状态对象, 无需重复创建, 使用单例模式也就自然而然了。事实上, Java 开发常用的对象容器Spring 默认构造的对象都是单例( 需要注意的是Spring 的单例是Spring 容器管理的单例, 而不是用单例模式构造的单例)。对象池模式通过复用对象实例, 减少对象创建和资源消耗。对于数据库连接对象,每次创建连接, 数据库服务端都需要创建专门的资源以应对, 因此频繁创建关闭数据库连接, 对数据库服务器而言是灾难性的, 同时频繁创建关闭连接也需要花费较长的时间。因此在实践中, 应用程序的数据库连接基本都使用连接池( Connection Pool )的方式。数据库连接对象创建好以后, 将连接对象放入对象池容器中, 应用程序要连接的时候, 就从对象池中获取一个空闲的连接使用, 使用完毕再将该对象归还到对象池中即可, 不需要创建新的连接。

前面说过, 对于每个Web 请求( HTTP Request ), Web 应用服务器都需要创建一个独立的线程去处理, 这方面, 应用服务器也采用线程池( Thread Pool )的方式。这些所谓的连接池、线程池, 本质上都是对象池, 即连接、线程都是对象, 池管理方式也基本相同。

3. 数据结构

前面缓存部分已经描述过Hash 表的基本原理, Hash 表的读写性能在很大程度上依赖HashCode 的随机性, 即HashCode 越随机散列, Hash 表的冲突就越少, 读写性能也就越高, 目前比较好的字符串Hash 散列算法有Time33 算法,即对字符串逐字符迭代乘以33 ,
求得Hash 值, 算法原型为:
hash (i) = hash(i - 1) * 33 + str[i]

Time33 虽然可以较好地解决冲突, 但是有可能相似字符串的HashCode 也比较接近,如字符串"AA" 的HashCode 是2210 , 字符串"AB" 的HashCode 是2211 。这在某些应用场景是不能接受的, 这种情况下, 一个可行的方案是对字符串取信息指纹, 再对信息、指纹求HashCode , 由于字符串微小的变化就可以引起信息、指纹的巨大不同, 因此可以获得较好的随机散列:

4. 垃圾回收

如果Web 应用运行在只叫等具有垃圾回收功能的环境中,那么垃圾回收可能会对系统的性能特性产生巨大影响。理解垃圾回收机制有助于程序优化和参数调优,以及编写内存安全的代码。

猜你喜欢

转载自blog.csdn.net/qq_41723615/article/details/89019982