自定义博客皮肤VIP专享

*博客头图:

格式为PNG、JPG,宽度*高度大于1920*100像素,不超过2MB,主视觉建议放在右侧,请参照线上博客头图

请上传大于1920*100像素的图片!

博客底图:

图片格式为PNG、JPG,不超过1MB,可上下左右平铺至整个背景

栏目图:

图片格式为PNG、JPG,图片宽度*高度为300*38像素,不超过0.5MB

主标题颜色:

RGB颜色,例如:#AFAFAF

Hover:

RGB颜色,例如:#AFAFAF

副标题颜色:

RGB颜色,例如:#AFAFAF

自定义博客皮肤

-+
  • 博客(335)
  • 资源 (3)
  • 收藏
  • 关注

原创 干货--并发编程提高-线程池要点(三十三)

"数据竞争"术语很容易与另外一个相关术语"竞态条件"混淆,"数据竞争"是指,如果在访问共享的非final类型的域时没有采用同步来进行协调,那么就会出现数据竞争,当一个数据写入一个变量而另一个线程接下来读取这个变量,或者读取一个之前由另一个线程写入的变量时,并且在这两个线程之间没有使用同步,那么就可能出现数据竞争。竞态条件出现的场景:比较经典的场景就是单例模式,单例模式首先判断对象是否初始化,如果没有则初始化,并创建一个新的对象并返回一个引用,从而在后台的调用中无须再执行这段高开销的代码。

2025-03-31 14:52:38 426

原创 干货--并发编程提高-线程池痛点(三十二)

也就是说当future的状态>COMPLETING时候调用get方法才会返回,而明显DiscardPolicy策略在拒绝元素的时候并没有设置该future的状态,后面也没有其他机会可以设置该future的状态,所以future的状态一直是NEW,所以一直不会返回,同理DiscardOldestPolicy策略也是这样的问题,最老的任务被淘汰时候没有设置被淘汰任务对于future的状态,也会导致一直不会返回。线程池被创建后如果没有任务过来,里面是不会有线程的。线程池被创建后里面有线程吗?

2025-03-31 14:50:14 485

原创 并发编程设计模式——Worker Thread模式(四十三)

案例承继自上一章echo服务端案例:相比于 Thread-Per-Message 模式的实现,改动非常少,仅仅是创建了一个最多线程数为 500 的线程池 es,然后通过 es.execute() 方法将请求处理的任务提交给线程池处理。优势:可以避免无限制的创建线程导致OOM;并且可以避免无限制的接收任务导致OOM。Worker Thread模式及其实现。如何避免重复创建线程?

2025-02-12 08:15:00 343

原创 并发编程设计模式——Thread-Per-Message模式(四十二)

Thread-Per-Message模式:为每个任务分配一个独立的线程。经典应用:网络编程里服务端的实现(服务端为每个客户端请求创建一个独立的线程,当线程处理完请求后,自动销毁)。用 Thread 实现 Thread-Per-Message 模式(这个实现用于对比,很坑)如何理解 Thread-Per-Message 模式(委托代办)Fiber 实现 Thread-Per-Message 模式。最简单实用的分工方法。

2025-02-12 07:45:00 250

原创 干货--并发编程提高-线程池实施设置参数的痛点(三十一)

对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;对于包含I/O操作或其他阻塞操作的任务,由于线程不会一直执行,因此线程池的规模应该更大,要正确地设置线程池的大小,需估算出任务的等待时间与计算时间的比值,这种估算不需要很精确。可以把它当做一个备份线程就好。而且,你再品一品 JDK 的源码,其实源码也体现出了有修改的含义的,两个值去做差值,只是第一次设置的时候原来的值为 0 而已。

2024-12-17 08:30:00 415

原创 干货--并发编程提高-线程池拒绝策略-补充(三十)

也就是说当future的状态>COMPLETING时候调用get方法才会返回,而明显DiscardPolicy策略在拒绝元素的时候并没有设置该future的状态,后面也没有其他机会可以设置该future的状态,所以future的状态一直是NEW,所以一直不会返回,同理DiscardOldestPolicy策略也是这样的问题,最老的任务被淘汰时候没有设置被淘汰任务对于future的状态,也会导致一直不会返回。运行后发现程序能正常退出。那么默认的AbortPolicy策略为啥没问题那?

2024-12-17 08:00:00 319

原创 干货--并发编程提高-线程池拒绝策略(二十九)

从运行结果看在任务one阻塞的5s内,主线程执行到了代码(5)等待任务one执行完毕,当任务one执行完毕后代码(5)返回,主线程打印出task one finish null。之后线程池的唯一线程会去队列里面取出任务two并执行所以输出start runable two然后代码(6)会返回,这时候主线程输出task two finish null,然后执行代码(7)等待任务three执行完毕,从执行结果看代码(7)会一直阻塞不会返回。再向线程池提交了一个任务two,这时候会把任务two放入到阻塞队列;

2024-12-12 08:30:00 409

原创 干货--并发编程提高-线程池预热(二十八)

线程池被创建后如果没有任务过来,里面是不会有线程的。allowCoreThreadTimeOut 该值默认为 false。核心线程数会被回收吗?

2024-12-12 08:00:00 253

原创 干货--并发编程提高-线程池的阻塞队列和工作流程(二十七)

2024-12-02 08:15:00 99

原创 干货--并发编程提高-死锁怎么避免续(二十六)

前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;只有当开始一轮垃圾收集时,才会开始调用finalize()方法;

2024-12-02 08:00:00 154

原创 干货--并发编程提高-死锁怎么避免(二十五)

还有一种方式,破坏循环等待方式,破坏这个条件,需要对资源进行排序,然后按序申请资源。这个实现非常简单,我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。4.循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。2.占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;必须同时获取到XY两个资源,才能进行同步,这样便解决了循环依赖等待的情况。

2024-11-27 08:30:00 416

原创 干货--并发编程提高-关闭线程池(二十四)

最后,一定要记得,shutdownNow和shuwdown调用完,线程池并不是立马就关闭了,要想等待线程池关闭,还需调用awaitTermination方法来阻塞等待。如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。

2024-11-27 08:00:00 355

原创 并发编程设计模式——Copy-On-Write模式(四十一)

Java 提供了 CopyOnWriteArrayList,为什么没有提供 CopyOnWriteLinkedList 呢?Copy-on-Write 模式的应用领域。

2024-11-14 08:30:00 477

原创 并发编程设计模式——Immutability模式(四十)

问题:如果具备不可变性的类,需要提供类似修改的功能,创建一个新的不可变对象就行了,这是与可变对象的一个重要区别,可变对象往往是修改自己的属性。所有的修改操作都创建一个新的不可变对象:这样会不会造成创建的对象太多了,有点太浪费内存呢?Foo 具备不可变性,线程安全,但是类 Bar 并不是线程安全的,类 Bar 中持有对 Foo 的引用 foo,对 foo 这个引用的修改在多线程中并不能保证可见性和原子性。本意是 A 用锁 al,B 用锁 bl,各自管理各自的,互不影响。快速实现具备不可变性的类。

2024-11-14 08:15:00 811

原创 并发编程设计模式——Balking模式(三十九)

多线程下,维护一个共享状态满足某个条件时,执行业务逻辑;当不满足时则立即放弃。通常用互斥锁来确保共享状态线程安全,如果不需要保证共享状态原子性,也可以用 volitle 修饰,替换互斥锁。Balking 模式本质上是一种规范化地解决“多线程版本的 if”的方案,其实就是对if判断的加锁设计。并抽象一个方法出来。使用 volatile 的前提是对原子性没有要求。用 volatile 实现 Balking 模式。Balking 模式的经典实现。这样的好处是将并发处理逻辑和业务逻辑分开。Balking 模式。

2024-11-11 08:30:00 432

原创 并发编程设计模式——Guarded Suspension模式(三十八)

概述:多线程下,A线程有个受保护的操作,在执行过程中需要等待满足某个条件,这个条件由B线程控制,条件不满足,A线程则挂起,直到条件满足才继续执行。例子:核心是:get() 方法通过条件变量的 await() 方法实现等待,onChanged() 方法通过条件变量的 signalAll() 方法实现唤醒功能。单线程场景中,if 语句是不需要等待的,因为在只有一个线程的条件下,如果这个线程被阻塞,那就没有其他活动线程了,这意味着 if 判断条件的结果也不会发生变化了。”来理解这个模式会更简单。

2024-11-11 08:15:00 215

原创 干货--并发编程提高-配置线程池(二十三)

1.任务性质不同的任务可以用不同规模的线程池分开处理,程序密集型任务尽量配置少的线程数量,入Ncpu+1的线程,Ncpu是cpu核心数。混合任务,如果可以拆分,拆分成一个程序密集型任务和一个io密集型任务,只要两任务的执行时间不是相差太大,拆分后的吞吐率更高,用各自的线程池去做;4.依赖数据库连接的任务,因为线程提交sql后需要等待数据库返回执行结果(数据库io,数据网络传输),如果时间越长,cpu空闲时间越长,此线程堵塞时间越长,那么就应该设置更多的线程,充分利用cpu。3.任务的执行时间:长、中、短。

2024-11-01 08:30:00 183

原创 干货--并发编程提高-计算CPU利用率(二十二)

输出解释 CPU以及CPU0、CPU1、CPU2、CPU3每行的每个参数意思(以第一行为例)为: 参数解释 user (432661) 从系统启动开始累计到当前时刻,用户态的CPU时间(单位:jiffies),不包含nice值为负的进程。“intr”这行给出中断的信息,第一个为自系统启动以来,发生的所有的中断的次数;它的单位随硬件平台的不同而不同。在Linux/Unix下,CPU利用率分为用户态,系统态和空闲态,分别表示CPU处于用户态执行的时间,系统内核执行的时间,和空闲系统进程执行的时间。

2024-11-01 08:00:00 767

原创 干货--并发编程提高-CPU 使用率低高负载的原因-下(二十一)

上面说过,cpu的工作效率要高于磁盘,而进程在cpu上面运行需要访问磁盘文件,这个时候cpu会向内核发起调用文件的请求,让内核去磁盘取文件,这个时候会切换到其他进程或者空闲,这个任务就会转换为不可中断睡眠状态。当在数据特别大的时候,如果执行的sql语句没有索引,就会造成扫描表的行数过大导致I/O阻塞,或者是语句中存在死锁,也会造成I/O阻塞,从而导致不可中断睡眠进程过多,导致负载过大。在上面的进程状态变换过程中,除了running状态,其他都是等待状态,那么其他状态都会加入到负载等待进程中吗?

2024-10-25 08:45:00 609

原创 干货--并发编程提高-CPU 使用率低高负载的原因-上(二十)

产生的原因一句话总结就是:等待磁盘I/O完成的进程过多,导致进程队列长度过大,但是cpu运行的进程却很少,这样就体现到负载过大了,cpu使用率低。下面内容是具体的原理分析:在分析负载为什么高之前先介绍下什么是负载、多任务操作系统、进程调度等相关概念。什么是负载:负载就是cpu在一段时间内正在处理以及等待cpu处理的进程数之和的统计信息,也就是cpu使用队列的长度统计信息,这个数字越小越好(如果超过CPU核心*0.7就是不正常)负载分为两大部分:CPU负载、IO负载。

2024-10-25 08:00:00 858

原创 干货--并发编程提高-线程池三问(十九)

如果我们需要处理的是一个长连接,即个连接上都是一个执行时间很长的任务,甚至就是一个不会停止的任务,这样我们就没有必要使用线程池技术。最后,一定要记得,shutdownNow和shuwdown调用完,线程池并不是立马就关闭了,要想等待线程池关闭,还需调用awaitTermination方法来阻塞等待。shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。

2024-10-24 08:45:00 748

原创 干货--并发编程提高-Condition(十八)

原因是:当未分配会员较多时,可能需要5秒才能分配完,然而executor.submit是异步操作,当休眠1秒钟后,马上又进入下一个循环,队列里又将插入重复的会员,这会导致队列长度不断增长,此外,会导致1个会员被分配后,又继续被分配,导致异常产生。这个问题我综合下来看,感觉还是不是很优雅,到不如这样设计感觉很有问题,当然我是站在了我的角度,可能没有业务的束缚,我感觉这种形式可以使用MQ来解决,可以一次发一批次的数据来处理,MQ天然的幂等性就能解决重新分配的问题,也无需自旋写个死循环来处理。

2024-10-24 08:15:00 687

原创 干货--并发编程提高-ReentrantLock(十六)

如果没有成功,则进入acquire方法以进一步获取锁,公平锁则是直接进入acquire方法。在Lock方法执行的时候非公平锁是先CAS尝试获取锁,未获取到后进入尝试获取锁,紧接着如果发现此时锁已释放又CAS尝试获取锁,如果两次都没有则跟公平锁一样进入等待队列。公平锁此时会判断内部维持的队列是否轮到了自己参考上图,如果轮到自己才会CAS尝试获取锁,修改成功了,则将当前线程设置为独占线程并返回。非公平锁就是当占用锁的线程释放锁时,所有等待的线程都会竞争锁。默认是非公平锁,可选参数配置是否公平锁。

2024-10-23 08:45:00 1007

原创 干货--并发编程提高-硬件同步原语-CAS&FAA(十五)

只能看当前操作系统是否支持了,很类似于NIO中的非阻塞算法,优先考虑使用更高效的,实在没得选再使用select方法一样。如果CAS更新失败了,则说明有其他线程也在更新此值,此时线程不阻塞,而是不停的尝试更新,直到成功。语义:先获取变量p当前的值value,然后给变量p增加inc (值),最后返回变量p之前的值。此操作也是原子性操作,FAA的性能要优于CAS,但FAA仅限于简单的加减法。因其CAS的原理,所以产生了ABA的问题。此外,lock-free作用于单个变量,导致大部分基于CAS的实现都比较复杂。

2024-10-23 08:30:00 545

原创 并发编程工具集——Fork/Join-下(三十七)

这个共享的 ForkJoinPool 默认的线程数是 CPU 的核数。用 Fork/Join 计算斐波那契数列。模拟 MapReduce 统计单词数量。ForkJoinPool 工作原理。

2024-09-26 10:18:02 750

原创 我的创作纪念日-干货-并发编程提高——Synchronized&AQS(十四)

及一些列私有的操作。如果是单核处理器,不建议使用自旋锁。在于当一个在等待队列中的共享节点成功获取到锁以后(它获取到的是共享锁),既然是共享,那它必须要依次唤醒后面所有可以跟它一起共享当前锁资源的节点,毫无疑问,这些节点必须也是在等待共享锁(这是大前提,如果等待的是独占锁,那前面已经有一个共享节点获取锁了,它肯定是获取不到的)。跟独占锁相比,共享锁的。自旋是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

2024-09-26 10:16:30 741

原创 并发编程工具集——Fork/Join-上(三十六)

Fork/Join 的使用。

2024-09-24 08:00:00 444

原创 并发编程工具集——CompletionService(三十五)

例如你需要提供一个地址转坐标的服务,为了保证该服务的高可用和性能,你可以并行地调用 3 个地图服务商的 API,然后只要有 1 个正确返回了结果 r,那么地址转坐标这个服务就可以直接返回 r 了。通过调用 cs.take().get(),我们能够拿到最快返回的任务执行结果,只要我们拿到一个正确返回的结果,就可以取消所有任务并且返回最终结果了。Dubbo 中有一种叫做 Forking 的集群模式,这种集群模式下,支持并行地调用多个查询服务,只要有一个成功返回结果,整个服务就可以返回了。

2024-09-24 07:15:00 1616

原创 干货-并发编程提高——重谈 RUNNABLE-下篇(十五)

cpu 与硬盘间是并发的。如果把线程视作为一个 job,这一 job 由 cpu 与硬盘交替协作完成,当在 cpu 上是 waiting 时,在硬盘上却处于 running,只是我们在操作系统层面讨论线程状态时通常是围绕着 cpu 这一中心去述说的。如果让 cpu 去等 I/O 的操作,很可能时间片都用完了,I/O 操作还没完成呢,不管怎样,它会导致 cpu 的利用率极低。至少我们看到了,进行传统上的 IO 操作时,口语上我们也会说 “阻塞”,但这个 “阻塞” 与线程的 BLOCKED 状态是两码事!

2024-09-19 08:30:00 698

原创 干货-并发编程提高——重谈 RUNNABLE-上篇(十四)

直接看它的 Javadoc 中的说明:一个在 JVM 中执行的线程处于这一状态中。(A threadexecuting而传统的进(线)程状态一般划分如下:注:这里的进程指早期的单线程进程,这里所谓进程状态实质就是线程状态。那么 runnable 与图中的 ready 与 running 区别在哪呢?

2024-09-19 08:00:00 990

原创 并发编程工具集——CompletionStage (三十四)

CompletionStage 接口里面描述 AND 汇聚关系,主要是 thenCombine、thenAcceptBoth 和 runAfterBoth 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。applyToEither、acceptEither 和 runAfterEither。描述 AND 汇聚关系。都是靠回调函数来解决的。描述 OR 汇聚关系。

2024-09-02 08:30:00 871

原创 并发编程工具集——CompletableFuture(三十三)

前两个方法:runAsync(Runnable runnable)和supplyAsync(Supplier<U> supplier),它们之间的区别是:Runnable 接口的 run() 方法没有返回值,而 Supplier 接口的 get() 方法是有返回值的。CompletableFuture 的核心优势。如何理解 CompletionStage 接口。

2024-09-02 08:00:00 464

原创 干货-并发编程提高——线程池-Executor (十三)

(存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。线程池默认的拒绝策略。Executor 框架(java线程基础)不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

2024-08-23 08:30:00 852

原创 干货-并发编程提高——线程池(十二)

使用中的集合自动汇聚。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。2.依次借,依次挪,从Work开始往下找,如果Work节点为使用中节点,则借对象阻塞。下单的线程池与询价的线程池不要用一个,这样如果一方线程池打满出问题,便会相互影响。3.如果还对象的话,就把当前使用中的对象状态重置为正常的状态,并直接拼接到尾部。

2024-08-23 08:00:00 471

原创 干货-并发编程提高——线程池(十一)

这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态,这种不一致性是不确定的,程序也会因此而产生一些无法预知的并发错误。这个有助于避免this引用的逃逸是因为要启动一个线程,可能会有实现runnable的方式,那么实现此方式如果启动线程的话,是需要按构造参数传入thread的。Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

2024-08-20 14:25:13 435

原创 并发编程工具集——Callable 任务如何运行以及获取执行结果-下篇(三十二)

提接上文~~重点来了!看懂了几乎就明白线程池执行任务时的beforeExecute、afterExecute方法的所起的作用了(比如经常在afterExecute方法里面做一些线程池任务运行时间的统计工作)。总结以下点:Callable任务被submit时,会生成一个FutureTask对象,封装Callable,在FutureTask的run方法里面执行Callable#call方法,并且调用返回一个Future,它实际上是一个FutureTask对象,通过获取Callable任务的执行结果。

2024-08-20 08:45:00 813

原创 并发编程工具集——Callable 任务如何运行以及获取执行结果-上篇(三十一)

向线程池提交Callable任务,会创建一个新线程(执行任务的线程)去执行这个Callable任务,但是通过Future#get获取任务的执行结果是在提交任务的调用者线程中,那问题一:调用者线程如何获取执行任务的线程的结果?在JDK中,有2种类型的任务,Runnable和Callable,但是具体到线程池执行任务的方法,它只接收Runnable任务,那问题二:Callable任务是提交给线程池后是如何执行的呢?

2024-08-20 08:30:00 734

原创 并发编程工具集——非阻塞算法lock-free和wait-free-下篇(三十)

CAS实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换。尽管1号线程的CAS操作成功,但是不代表这个过程就是没有问题的。我们来看个具体例子:现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B:head→A→B后希望用CAS将栈顶替换为B在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下,而对象B此时处于游离状态:head→A→C→DB。

2024-08-20 08:00:00 282

原创 并发编程工具集——非阻塞算法lock-free和wait-free-上篇(二十九)

使用wait-free的数据结构的应用程序中,与其将原来使用互斥的算法改造为wait-free的算法,不如直接使用基于wait-free算法开发的stack、queue、set和map。这个算法是lock-free的但是不是wait-free的。如果在进行这三步的时候,第一步读出的金额和第三步更新要用新值去替换的金额不一致的情况下,命令直接失败,然后操作回滚到第一步重新执行。所以对于算法来说达到lock-free不一定能达到wait-free,但是达到wait-free的算法一定是lock-free的。

2024-08-08 09:00:00 464

原创 干货-并发编程提高——FutureTask(十)

4. Callable任务被submit时,会生成一个FutureTask对象,封装Callable,在FutureTask的run方法里面执行Callable#call方法,并且调用java.util.concurrent.FutureTask#set设置Callable任务的执行结果(结果保存在一个FutureTask的Object类型的实例变量里面:private Object outcome;然后Executors是由java1.5引入的,框架最大的优点就是将执行任务与提交任务解耦出来。

2024-08-08 08:15:00 1296

canal-master.zip

canal

2022-01-04

apache-zookeeper-3.7.0-bin.tar.gz

apache-zookeeper-3.7.0-bin.tar.gz

2022-01-04

Redis数据结构与对象总结

Redis数据结构与对象总结 数据结构与对象 简单动态字符串 SDS简介 SDS与C字符串的区别 常数复杂度获取字符串长度 O(n) O(1) 杜绝缓冲区溢出 修改字符串长度时内存重分配 空间预分配:对字符串进行增长操作时的优化,SDS API 会检查SDS当前空间是否符合修改条件,如果不符合 不但会分配修改所需空间还会分配给free属性额外的未使用空间 惰性空间释放:对字符串进行缩减操作时

2020-04-24

空空如也

TA创建的收藏夹 TA关注的收藏夹

TA关注的人

提示
确定要删除当前文章?
取消 删除