写点什么

做多线程并发扩展,这两点你需要关注

  • 2022 年 6 月 07 日
  • 本文字数:3647 字

    阅读完需:约 12 分钟

本文分享自华为云社区《【高并发】多线程并发扩展》,作者:冰 河 。

死锁

死锁-必要条件


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相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 */@Slf4jpublic 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 等这些实现线程同步的方法了。


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 4
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
做多线程并发扩展,这两点你需要关注_spring_华为云开发者联盟_InfoQ写作社区