做多线程并发扩展,这两点你需要关注
摘要: Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。
本文分享自华为云社区《【高并发】多线程并发扩展》,作者:冰 河 。
死锁
死锁-必要条件
1)互斥条件
进程对所分配到的资源进行排他性的使用,即在一段时间内某个资源只由一个进程占用,如果此时还有其他进程请求资源,那么请求者只能等待,直到占用资源的进程将资源释放。
2)请求和保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有。此时,请求进程阻塞,但又对自己已获得的其他资源保持不放。
3)不剥夺条件
进程已获得资源,在未使用完之前,不能被剥夺,只能在使用完后,自己释放相应的资源。
4)环路等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
死锁示例代码如下:
package io.binghe.concurrency.example.deadlock; import lombok.extern.slf4j.Slf4j; /** * @author binghe * @version 1.0.0 * @description 一个简单的死锁类 * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒 * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒 * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定; * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 */ @Slf4j public class DeadLock implements Runnable{ public int flag = 1; //静态对象是类的所有对象共享的 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { log.info("flag:{}", flag); if (flag == 1) { synchronized (o1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { log.info("1"); } } } if (flag == 0) { synchronized (o2) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o1) { log.info("0"); } } } } public static void main(String[] args) { DeadLock td1 = new DeadLock(); DeadLock td2 = new DeadLock(); td1.flag = 1; td2.flag = 0; //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。 //td2的run()可能在td1的run()之前运行 new Thread(td1).start(); new Thread(td2).start(); } }
注意:代码为转载示例
处理死锁的方法
- 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
- 避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
- 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
- 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
多线程并发最佳实践
(1)使用本地变量
尽量使用本地变量,而不是创建一个类或实例的变量
(2)使用不可变类
比如:String、基础类型的包装类等,一旦创建就不会改变,不可变类可以降低代码中需要的同步数量。
(3)最小化锁的作用域范围:S=1/(1-a+a/n)
a:并行计算部分所占的比例
n:并行处理的节点个数
S:加速比
当a=1时,没有串行,只有并行,此时,S=n
当a=0时,只有串行,没有并行,此时,S=1
当n趋向于无穷大时,S趋向于1/(1-a),这也是加速比S的上限
S=1/(1-a+a/n)公式也叫做:阿姆达尔定律或者安达尔定理。
(4)使用线程池的Executor,而不是直接new Thread执行
(5)宁可使用同步也不要使用线程的wait和notify
(6)使用BlockingQueue实现生产-消费模式
(7)使用并发集合而不是加了锁的同步集合
(8)使用Semaphore创建有界的访问
(9)宁可使用同步代码块,也不使用同步的方法
(10)避免使用静态变量
Spring与线程安全性
(1)Spring bean: scope有两个取值:singleton、prototype
(2)交由Spring管理大多数是无状态对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope——singleton,每个单例的无状态对象都是线程安全的。其实只要是无状态的对象,不管是单例还是多例,都是线程安全的。
实际上,Spring没有保证bean的线程安全
Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。
Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。
singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。在整个Spring IoC容器里,只有一个bean实例,所有线程共享该实例。
prototype:bean被定义为在每次注入时都会创建一个新的对象。每次请求都会创建并返回一个新的实例,所有线程都有单独的实例使用,这种方式是比较安全的,但会消耗大量内存和计算资源。
request(请求范围实例):bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。每当接受到一个HTTP请求时,就分配一个唯一实例,这个实例在整个请求周期都是唯一的。
session(会话范围实例):bean被定义为在一个session的生命周期内创建一个单例对象。在每个用户会话周期内,分配一个实例,这个实例在整个会话周期都是唯一的,所有同一会话范围的请求都会共享该实例。
application:bean被定义为在ServletContext的生命周期中复用一个单例对象。
websocket:bean被定义为在websocket的生命周期中复用一个单例对象。
globalsession(全局会话范围实例):这与会话范围实例大部分情况是一样的,只是在使用到portlet时,由于每个portlet都有自己的会话,如果一个页面中有多个portlet而需要共享一个bean时,才会用到。
我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。
无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题),用完即关(或交还给连接池)。
有人可能会认为,我使用request作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误的,因为Controller默认是单例的,一个HTTP请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把Controller的scope改成prototype,实际上Struts2就是这么做的,但有一点要注意,Spring MVC对请求的拦截粒度是基于每个方法的,而Struts2是基于每个类的,所以把Controller设为多例将会频繁的创建与回收对象,严重影响到了性能。
线程安全问题都是由全局变量及静态变量引起的
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
线程安全的几种情况:
1 ) 常量始终是线程安全的,因为只存在读操作。
2 )每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。
3 )局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。
总之,Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计没有在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
函数计算异步任务能力介绍 - 任务触发去重
作者:渐意 前言 无论是在大数据处理领域,还是在消息处理领域,任务系统都有一个很关键的能力 - 任务触发去重。这个能力在一些对准确性要求极高的场景(如金融领域)中是必不可少的。作为 Serverless 化任务处理平台,Serverless Task 也需要提供这类保障,在用户应用层面及自身系统内部两个维度具备任务的准确触发语义。本文主要针对消息处理可靠性这一主题来介绍函数计算异步任务功能的技术细节,并展示如何在实际应用中使用函数计算所提供的这方面能力来增强任务执行的可靠性。 浅谈任务去重 在讨论异步消息处理系统时,消息处理的基本语义是无法绕开的话题。在一个异步的消息处理系统(任务系统)中,一条消息的处理流程简化如下图所示: 图 1 用户下发任务 - 进入队列 - 任务处理单元监听并获取消息 - 调度到实际 worker 执行 在任务消息流转过程中,任何组件(环节)可能出现的宕机等问题会导致消息的错误传递。一般的任务系统会提供至多 3 个层级的消息处理语义: At-Most-Once:保证消息最多被传递一次。当出现网络分区、系统组件宕机时,可能出现消息丢失; At-Least-Once...
- 下一篇
OneOS接入第三方SDK实战,让你的精力聚焦于开发
由于各个芯片厂商发布SDK所使用的编译方式各不相同,开发者在开发过程中需要学习和适应不同编译环境,导致开发精力被分散。OneOS环境可以编译其他SDK应用代码,让开发者忽略编译影响,从而将精力聚焦在功能开发中。是不是很香~那么下面就跟着小编以展锐8910DM为例,学习如何把第三方SDK导入OneOS编译系统吧! 注意:以下分析基于8910_Module_V1.4和OneOS 2.0版本。 在正式导入前,让我们一起来了解第三方编译规则和OneOS Scons编译框架。 第三方编译规则 8910DM SDK使用的是cmake/ninja编译工具,它的编译脚本launch.bat首先配置了环境变量、编译链接工具、创建编译输出目录等: 为了保持SDK编译一致性,这些工具同样需要在OneOS中使用。接下来查看SDK编译链接目标及其规则。 cmake编译命令会把编译规则做初步解析,拷贝一些cmake文件,创建编译目标文件夹,并将原来的cmake编译规则转成ninja编译规则。cmake规则文件是CMakeLists.txt,而ninja 是build.ninja和rules.ninja。直接从bu...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Red5直播服务器,属于Java语言的直播服务器
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合Redis,开启缓存,提高访问速度