大厂面试题-Java并发编程基础篇(一)

news2024/9/23 3:28:36

目录

一、什么是守护线程,它有什么特点

二、谈谈你对AQS的理解

三、AbstractQueuedSynchronized为什么采用双向链表

四、lock和synchronized区别

五、线程池如何知道一个线程的任务已经执行完成

六、什么叫做阻塞队列的有界和无界

七、ConcurrentHashMap底层具体实现知道吗?实现原理是什么?

    ConcurrentHashMap 的整体架构

    ConcurrentHashMap的基本功能

   ConcurrentHashMap 在性能方面做的优化 

八、谈一下CAS机制

九、死锁的发生原因和怎么避免

十、volatile关键字有什么用?它的实现原理是什么?


一、什么是守护线程,它有什么特点

下面用最简单的方式让大家彻底搞懂守护线程。

简单说,守护线程就是一种后台服务线程,他和我们在Java里面创建的用户线程是一模一样的。

守护线程和用户线程的区别有几个点,这几个点也是守护线程本身的特性:

1.在线程创建方面,对于守护线程,我们需要主动调用setDaemon()并且设置成true

2.我们知道,一个Java进程中,只要有任何一个用户线程还在运行,那么这个java进程就不会结束,否则,这个程序才会终止。

注意,Java进程的终止与否,只和用户线程有关。如果当前还有守护线程正在运行,也不会阻止Java程序的终止。

因此,守护线程的生命周期依赖于用户线程

举个例子,JVM垃圾回收线程就是一个典型的守护线程,它存在的意义是不断的处理用户线程运行过程中产生的内存垃圾。

一旦户线程全部结束了,那垃圾回收线程也就没有存在的意义了。

由于守护线程的特性所以它它适合用在一些后台的通用服务场景里面。

但是守护线程不能用在线程池或者一些IO任务的场景里面,因为一旦JVM退出之后,守护线程也会直接退出。

就会可能导致任务没有执行完或者资源没有正确释放的问题。

二、谈谈你对AQS的理解

AQS是多线程同步器,它是J.U.C包中多个组件的底层实现,如Lock、CountDownLatch、Semaphore等都用到了AQS.

从本质上来说,AQS提供了两种锁机制,分别是排它锁和共享锁。

排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如Lock中的ReentrantLock重入锁实现就是用到了AQS中的排它锁功能。

共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch和Semaphore都是用到了AQS中的共享锁功能。

三、AbstractQueuedSynchronized为什么采用双向链表

下面从两个方面给大家解释一下这个问题:

个方面,双向链表的优势:

    1、双向链表提供了双向指针,可以在任何一个节点方便向前或向后进行遍历,这种对于有反向遍历需求的场景来说非常有用。

    2、双向链表可以在任意节点位置实现数据的插入和删除,并且这些操作的时间复杂度都是O(1),不受链表长度的影响。这对于需要频繁对链表进行增删操作的场景非常有用。

第二个方,说一下AQS采用双向链表的原因

    1、存储在双向链表中的线程,有可能这个线程出现异常不再需要竞争锁,所以需要把这些异常节点从链表中删除,而删除操作需要找到这个节点的前驱结点,如果不采用双向链表,就必须要从头节点开始遍历,时间复杂度就变成了O(n)。

    2、新加入到链表中的线程,在进入到阻塞状态之前,需要判断前驱节点的状态,只有前驱节点是Sign状态的时候才会让当前线程阻塞,所以这里也会涉及到前驱节点的查找,采用双向链表能够更好的提升查找效率。

    3、线程在加入到链表中后,会通过自旋的方式去尝试竞争锁来提升性能,在自旋竞争锁的时候为了保证锁竞争的公平性,需要先判断当前线程所在节点的前驱节点是否是头节点。这个判断也需要获取当前节点的前驱节点,同样采用双向链表能提高查找效率。

总而言之,采用单向链表不支持双向遍历,而AQS中存在很多需要双向遍历的场景来提升线程阻塞和唤醒的效率。

四、lock和synchronized区别

下面从4个方面来回

1.从功能角度来看Lock和Synchronized都是Java中用来解决线程安全问题的工具

2.从特性来看

a.SynchronizedJava中的同步关键,Lock是J.U.C包中提供的接口,这个接口有很多实现类,其中就包括ReentrantLock重入锁

b.Synchronized可以通过两种方式来控制锁的粒度,如图:

一种是把synchronized关键字修饰在方法层面,另一种是修饰在代码块上,并且我们可以通过Synchronized加锁对象的声明周期来控制锁的作用范围,比如锁对象是静态对象或者类对象,那么这个锁就是全局锁。

如果锁对象是普通实例对象,那这个锁的范围取决于这个实例的声明周期

Lock锁的粒度是通过它里面提供的lock()和unlock()方法决定的(如图),包裹在这个方法之间的代码能够保证线程安全性。而锁的作用域取决于Lock实例的生命周期。

c.Lock比Synchronized的灵活性更高,Lock可以自主决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()这两个方法就行,同时Lock还提供了非阻塞的竞争锁方法tryLock()方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁。

Synchronized由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外,Synchronized锁的释放是被动的,就是当Synchronized同步代码块执行完以后或者代码出现异常时才会释放。

d.Lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁Synchronized只提供了一种非公平锁的实现。

3.从性能方面来看,Synchronized和Lock在性能方面相差不大,在实现上会有一些区别,Synchronized引入了偏向锁、轻量级锁、重量级锁以及锁升级的方式来优化加锁的性能,Lock中则用到了自旋锁的方式来实现性能优化。

五、线程池如何知道一个线程的任务已经执行完成

从两个方面来回答

在线程池内部,当我们把一个任务丢给线程池去执行,线程池会调度工作线程来执行这个任务的run方法,run方法正常结束,也就意味着任务完成了。

所以线程池中的工作线程是通过同步调用任务的run()方法并且等待run方法返回后, 再去统计任务的完成数量。

1.  如果想在线程池外部去获得线程池内部任务的执行状态 ,有几种方法可以实现。

a.   线程池提供了一个isTerminated()方法,可以判断线程池的运行状态,我们可以循环判断isTerminated()方法的返回结果来了解线程池的运行状态,一旦线池的运行状态是Terminated,意味着线程池中的所有任务都已经执行完了。

要通过这个方法获取状态的前提是,程序中主动调用了线程池的shutdown()方法。在实际业务中,一般不会主动去关闭线程池,因此这个方法在实用性和灵活性方面都不是很好。

b.   在线程池中,有一个submit()方法,它提供了一个Future的返回值,我们通Future.get()方来获得任务的执行结果,当线程池中的任务没执行完之前,future.get()方法会一直阻塞,直到任务执行结束。因此,只要future.get()方法正常返回,也就意味着传入到线程池中的任务已经执行完成了!

c.    可以引入一个CountDownLatch计数器,它可以通过初始化指定一个计数器进行倒计时,其中两个方法分别是await()阻塞线程,以及countDown()进行倒计时,一旦倒计时归零,所以被阻塞在await()方法的线程都会被释放。

基于这样的原理,我们可以定义一个CountDownLatch对象并且计数器为1,接着在线程池代码块后面调用await()方法阻塞主线程,然后,当传入到线程池中的任务执行完成后,调用countDown()方法表示任务执行结束。

最后,计数器归零0,唤醒阻塞在await()方法的线程。

2.  基于这个问题,简单总结一下,不管是线程池内部还是外部,要想知道线程是否执行结束,我们必须要获取线程执行结束后的状态,而线程本身没有返回值,所以只能通过阻塞-唤醒的方式来实现,future.get和CountDownLatch都是这样一个原理。

六、什么叫做阻塞队列的有界和无界

  1. 阻塞队列,是一种特殊的队列 ,它在普通队列的基础上提供了两个附加功能

a.   当队列为空的时候,获取队列中元素的消费者线程会被阻塞 ,同时唤醒生产者 线程。

b.   当队列满了的时候 ,向队列中添加元素的生产者线程被阻塞 ,同时唤醒消费者 线程。

2.   其中,阻塞队列中能够容纳的元素个数,通常情况下是有界的,比如我们实例化一个ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数。这种就是有界队列。

3.   而无界队列,就是没有设置固定大小的队列,不过它并不是像我们理解的那种元素没有任何限制,而是它的元素存储量很大,像LinkedBlockingQueue,它的默认队列长度是Integer.Max_Value,所以我们感知不到它的长度限制。

4.   无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制的添加任务,容易导致内存溢出的问题!

七、ConcurrentHashMap底层具体实现知道吗?实现原理是什么?

个问题从这三个方面来回答:

1.   ConcurrentHashMap 的整体架

2.   ConcurrentHashMap 的基本功

3.   ConcurrentHashMap 在性能方面的优化

    ConcurrentHashMap 的整体架构

这个ConcurrentHashMap在JDK1.8中的存储结构,它是由数组、单向链表、红黑树组成。

当我们初始化一ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题。ConcurrentHashMap用链式寻址法来解决hash冲突。

hash冲突较多的时候,会造成链表长度较长,这种情况会使得ConcurrentHashMap中数据元素的查询复杂度变成O(n)。因此在JDK1.8中,引入了红黑树的机制。

当数组长度大于64并且链表长度大于等于8的时候,单项链表就会转换为红黑树。另外,随着ConcurrentHashMap的动态扩容,一旦链表长度小于8,红黑树会退化成单向链表。

    ConcurrentHashMap的基本功能

ConcurrentHashMap本质上是一HashMap,因此功能和HashMap一样,但是ConcurrentHashMapHashMap的基础上,提供了并发安全的实现。

并发安全的主要实现是通过对指定的Node节点加锁,来保证数据更新的安全性

   ConcurrentHashMap 在性能方面做的优化 

如果在并发性能和数据安全性之间做好平衡,在很多地方都有类似的设计,比如cpu的三级缓存、mysql的buffer_pool、Synchronized的锁升级等等。ConcurrentHashMap也做了类似的优化,主要体现在以下几个方面:

1、 JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。

2、引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)。

3、(如图所示),当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。

4、ConcurrentHashMap中有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。ConcurrentHashMap在这个方面的优化主要体现在两个点:

a.当线竞争不激烈时,直接采用CAS来实现元素个数的原子递增。

b.如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直从数组中随机选择一个,再通过CAS实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。

八、谈一下CAS机制

CASJavaUnsafe类里面的方,它的全称是CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。

举个例子比如说有这样一个场景(如图),有一个成员变量state,默认值是0,了一个方法doSomething(),这个方法的逻辑是判断state是否为0,如果为0就修改成1。

这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read-Write的操作。

一般情况下,我们会在doSomething()这个方法上加同步锁来解决原子性问题。

但是,加同步锁,会带来性能上的损耗,所以,对于这类场景,我们就可以使用CAS机制来进行优化这个是优化之后的代码(如图)

在doSomething()方法中,我们调用了unsafe类中的compareAndSwapInt()方法来达到同样的目的,这个方法有四个参数,分别是:当前对象实例、成员变量state在内存地址中的偏移量、预期值0、期望更改之后的值1。

CAS会比较state内存地址偏移量对应的值和传入的预期值0是否相等,如果相等,就直接修改内存地址中state的值为1否则,返回false,表示修改失败,而这个过程是原子的,不会存在线程安全问题。

CompareAndSwap是一个native,实际上它最终还是会面临同样的问题,就是先从内存地址中读取state的值,然后去比较,最后再修改。

这个过程不管在什么层面上实现,都会存在原子性问题。

所以呢,CompareAndSwap的底实现中,在多核CPU环境下,会增加一个Lock指令缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。

CAS主要用在并发场景中,比较典型的使用场景有两个

1.   第一个是J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。

2.   第二个是实现多线程对共享资源竞争的互斥性质,比如在AQS、ConcurrentHashMap、ConcurrentLinkedQueue等都有用到。

九、死锁的发生原因和怎么避免

(如图),死锁,简单来说就是两个或者两个以上的线程在执行的过程中,争夺同一个共享资源造成的相互等待的现象。

如果没有外部干预,线程会一直阻塞无法往下执行,这些一直处于相互等待资源的线程就称为死锁线程。

导致死锁的条件有四个,也就是这四个条件同时满足就会产生死锁:

   1、 互斥条件,共享资源XY只能被一个线程占用;

    2、请求和保持条件,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;

    3、不可抢占条件,其他线程不能强行抢占线程T1占有的资源;

    4、循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。

导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。

所以,只能在写代码的时候,去规避可能出现的死锁问题。

按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互斥条件是没办法破坏,因为这是互斥锁的基本约束,其他三方条件都有办法来破坏:

    1、对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。

    2、对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

    3、对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

十、volatile关键字有什么用?它的实现原理是什么?

volatile关键字有两个作用:

1.   可以保证在多线程环境下共享变量的可见性。

2.   通过增加内存屏障防止多个指令之间的重排序。

可见性指当某一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值。其实这个可见性问题,本质上是由几个方面造成的:

  3.  (如图)CPU层面的高速缓存,在CPU里面设计了三级缓存去解决CPU运算效率和内存IO效率问题,但是带来的就是缓存的一致性问题,而在多线程并行执行的情下,缓存一致性就会导致可见性问题。

所以,对于增加了volatile关键字修饰的共享变量,JVM虚拟机会自动增加一个#Lock汇编指令,这个指令会根据CPU型号自动添加总线锁或/缓存锁

单说一下这两种锁

a. 总线锁是定了CPU的前端总线,从而导致在同一时刻只能有一个线程去和内存通信,这样就避免了多线程并发造成的可见性。

b. 缓存锁是对总线锁的优化,因为总线锁导致了CPU的使用效率大幅度下降,所以缓存锁只针对CPU三级缓存中的目标数据加锁,缓存锁是使用MESI缓一致性来实现的。

4.   指令重排序,所谓重排序,就是指令的编写顺序和执行顺序不一致,在多线程环境下导可见性问题。指令重排序本质上是一种性能优化的手段,它来自于几个方面

a. CPU面,针对MESI协议的更进一步优化去提升CPU的利用率,引入了StoreBuffer机制,而这一种优化机制会导致CPU的乱序执行。当然为了避免这样的问题,CPU提供内存屏障指令,上层应用可以在合适的地方插入内存屏障来避免CPU指令重排序问题。

b. 编译器的优化,编译器在编译的过程中,在不改变单线程语义和程序正确性的前提下,对指令进行合理的重排序优化来提升性能

所以,如果对共享变量增加了volatile关键字,那么在编译器层面,就不会去触发编译器优化,同时再JVM里面,会插入内存屏障指令来避免重排序问题。

当然,除了volatile以外,从JDK5开始,JMM就使用了一种Happens-Before模型去描述多线程之间的内存可见性问题。

如果两个操作之间具备Happens-Before关系,那么意味着这两个操作具备可见性关系,不需要再额外去考虑增加volatile关键字来提供可见性保障。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1143948.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

拓扑排序基础详解,附有练习题

介绍 拓扑排序是一种对有向无环图(DAG)进行排序的算法。在一个有向图中,如果存在一条从节点 A 到节点 B 的路径,那么节点 A 就依赖于节点 B。 有向无环图如下 什么是入度,出度? 入度:有多少个…

10款轻量型的嵌入式GUI库分享

LVGL LittlevGL是一个免费的开源图形库,提供了创建嵌入式GUI所需的一切,具有易于使用的图形元素、漂亮的视觉效果和低内存占用。 特点: 强大的构建模组 按钮、图表、列表、滑块、图像等 ​先进的图形 动画、反锯齿、半透明、平滑滚动 多样…

【VR开发】【Unity】【VRTK】1-无代码VRVR开发介绍

本篇开始精简讲解VRTK相关的知识。 VRTK是基于Unity的一套提供无代码VR开发的插件,这套插件开源,可商用,集合了目前可能的VR体验组件,可以让不会C#编程但想要开发VR体验的人在不写一行代码的前提下开发出心仪的VR作品。 这套组件问世后也很受欢迎,目前已经进化到了第四代…

2023阿里云双十一优惠活动「云上聚·创未来」价格和代金券领取

2023阿里云双十一优惠活动「金秋云创季」开始啦,10月27日到10月31日可以领满减优惠,到11月1日和11月11日之间可以购买云服务器等产品,11.12到11.30日赢最高百万上云抵扣金,阿里云百科aliyunbaike.com分享2023阿里云双十一优惠活动…

合成数据的好处和用途

在不断变化的数据科学和人工智能环境中,合成数据集的概念成为具有多种用途的强大工具。 假设您是一名数据科学家,并分配了为电子商务网站创建尖端推荐系统的任务。为此,您需要大量的用户交互数据。但是,您面临着保护用户隐私和处…

基本微信小程序的外卖点餐订餐平台

项目介绍 餐饮行业是一个传统的行业。根据当前发展现状,网络信息时代的全面普及,餐饮行业也在发生着变化,单就点餐这一方面,利用手机点单正在逐步进入人们的生活。传统的点餐方式,不仅会耗费大量的人力、时间&#xf…

世界前沿技术发展报告2023《世界航空技术发展报告》(三)民用飞机技术

(三)民用飞机技术 1.干线飞机1.1 中国C919客机获得型号合格证并交付使用1.2 空客公司A321XLR超远程型窄体客机完成首飞1.3 NASA持续开展下一代民机技术研究1.4 欧洲开展“超高性能机翼”演示验证项目 2.支线飞机2.1 德国航宇中心完成“电动飞机概念及技术…

世界前沿技术发展报告2023《世界航空技术发展报告》(四)无人机技术

(四)无人机技术 1.无人作战飞机1.1 美国空军披露可与下一代战斗机编组作战的协同式无人作战飞机项目1.2 俄罗斯无人作战飞机取得重要进展 2.支援保障无人机2.1 欧洲无人机项目通过首个里程碑2.2 美国海军继续开展MQ-25无人加油机测试工作 3.微小型无人机…

Python+pytest+request 接口自动化测试!

一、环境配置 1.安装python3 brew update brew install pyenv 然后在 .bash_profile 文件中添加 eval “$(pyenv init -)” pyenv install 3.5.3 -v pyenv rehash 安装完成后,更新数据库 pyenv versions 查看目前系统已安装的 Python 版本 pyenv global 3.5…

C#WinformListView实现缺陷图片浏览器

C#&Winform&ListView实现缺陷图片浏览器 功能需求图像浏览行间距调整悬浮提示 功能需求 机器视觉检测系统中特别是缺陷检测系统,通常需要进行对已经检出的缺陷图片进行浏览查阅。主要是通过条件筛选查询出所需要的数据,进行分页再展示到界面中。…

基于SpringBoot的垃圾分类管理系统

基于SpringBootVue的垃圾分类管理系统的设计与实现~ 开发语言:Java数据库:MySQL技术:SpringBootMyBatis工具:IDEA/Ecilpse、Navicat、Maven主要功能:包括前台和后台两部分、首页列表展示、垃圾分类、垃圾图谱、查看详…

当线性规划与算法相遇:揭秘单纯形法(Simplex)的独特魅力

传统的解决线性规划问题的方法是图形法、代数法求解,但是图形法解题有极大的局限性,因为一旦变量超过3个,基本上就无法通过图形解决,而代数法虽然可以解题,但对于复杂的问题可能效果较差甚至无法求解! 相比…

嵌入式PID算法理论+实践分析

1.1 概述 比例(Proportion)积分(Integral)微分(Differential)控制器(PID控制器或三项控制器)是一种采用反馈的控制回路机制,广泛应用于工业控制系统和需要连续调制控制的…

LangChain+LLM实战---LangChain概述

LangChain介绍 LangChain是个开源的框架,它可以让AI开发人员把像GPT-4这样的大型语言模型(LLM)和外部数据结合起来。可以简单认为LangChain是LLM领域的Spring,以及开源版的ChatGPT插件系统。 LangChain的强大之处不仅能通过API调用语言模型,…

LLMs之ChatGLM3:ChatGLM3/ChatGLM3-6B的简介(多阶段增强+多模态理解+AgentTuning技术)、安装、使用方法之详细攻略

LLMs之ChatGLM3:ChatGLM3/ChatGLM3-6B的简介(多阶段增强多模态理解AgentTuning技术)、安装、使用方法之详细攻略 导读:2023年10月27日,智谱AI在2023中国计算机大会上推出了全自研的第三代基座大模型ChatGLM3及其相关系列产品,这是…

系列二十一、请描述BeanDefinition的加载过程

一、概述 BeanDefinition是用来描述bean的生产信息,决定bean如何生产,是一个定义态的bean。 二、流程 2.1、第一步:启动IOC容器 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(MySpringConfig.cla…

嵌入式系统中C++ 类的设计和实现分析

C代码提供了足够的灵活性,因此对于大部分工程师来说都很难把握。 本文介绍了写好C代码需要遵循的10个最佳实践,并在最后提供了一个工具可以帮助我们分析C代码的健壮度。 原文:10 Best practices to design and implement a C class。 1. 尽…

基于回溯搜索算法的无人机航迹规划-附代码

基于回溯搜索算法的无人机航迹规划 文章目录 基于回溯搜索算法的无人机航迹规划1.回溯搜索搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要:本文主要介绍利用回溯搜索算法来优化无人机航迹规划。 …

2023Selenium自动化测试框架入门整理(建议收藏)

本文主要针对Selenium自动化测试框架入门整理,只涉及总体功能及框架要点介绍说明,以及使用前提技术基础要求整理说明。作为开发人员、测试人员入门参考。 本文参考:Selenium框架最新技术规范及相关资料 简介 Selenium也是一款同样使用Apac…

实现分片上传、断点续传、秒传 (JS+NodeJS)(TypeScript)

一、引入及效果 上传文件是一个很常见的操作,但是当文件很大时,上传花费的时间会非常长,上传的操作就会具有不确定性,如果不小心连接断开,那么文件就需要重新上传,导致浪费时间和网络资源。 所以&#xff0…