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

x33g5p2x  于2022-06-08 转载在 其他  
字(4.1k)|赞(0)|评价(0)|浏览(497)

摘要: 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等这些实现线程同步的方法了。

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

相关文章