常见的并发面试题
一.进程与线程的区别?
- 进程是操作系统进行资源分配的最小单元,线程是操作系统进行运算调度的最小单元。
- 进程中包含了线程,线程属于进程。
- 进程的内存和资源是该进程下的线程所共享的。
二.创建线程的方式以及区别?
-
继承Thread类:需要实现 run() 方法。通过 Thread 调用 start() 方法来启动线程。
-
实现Runnable接口:同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
-
实现Callable 接口:与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
-
使用ExecutorService、Callable、Future实现有返回结果的多线程。
实现接口会更好一些,因为:Java 不支持多继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;类可能只要求可执行即可,继承整个 Thread 类开销会过大。
Thread和Runable的区别和联系
-
Thread类实现了Runable接口。都需要重写里面Run方法。
-
不同:实现Runnable的类更具有健壮性,避免了单继承的局限。
-
Runnable更容易实现资源共享,能多个线程同时处理一个资源。
三.线程的状态有哪些?
新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态
当线程对象调用了**start()**方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
四.sleep与wait区别?
- sleep是线程类(Thread)的方法;wait是Object类的方法
- sleep是使线程休眠,不会释放对象锁;wait是使线程等待,释放锁;
sleep让出的是cpu,如果此时代码是加锁的,那么即使让出了CPU,其他线程也无法运行,因为没有得到锁;wait是让自己暂时等待,放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。 - 调用sleep进入阻塞状态;调用wait进入等待状态,调用notify进入就绪状态。
五.线程通信方式?volatile关键字的理解
- 线程通信的第一种方式:[volatile]关键字基于volatile关键字来实现线程间通信是基于共享内存的思想:多个线程同时监听某个变量,当该变量发生变量的时候,线程能够感知并执行相应的业务。
- volatile关键字保证了共享变量的可见性,任何线程需要读取时都要到内存中读取(确保获得最新值)。synchronized关键字确保只能同时有一个线程访问方法或者变量,保证了线程访问的可见性和排他性。
synchronized底层是基于监视器(Monitor)的获取,每个对象都有自己的监视器,线程必须获得监视器才能继续执行内容。 - ThreadLocal()
ThreadLocal,即线程本地变量(每个线程唯一),每个线程只能访问自己的,底层是一个ThreadLocalMap来存储信息,以ThreadLocal对象为键、任意对象为值,key是弱引用,value是强引用,所以使用完毕后要及时清理(尤其使用线程池时)。
六.保证线程安全的方式有哪些?
-
第一种实现线程安全的方式:同步代码块,即用synchronized关键字
-
第二种方法:同步方法,也是用synchronized关键字,只是这个关键字用在方法上了,把线程共享的数据块抽象成方法,在方法上加了同步锁。
-
第三种方法:使用Lock锁机制,对线程不安全的代码块采用lock()加锁,使用unlock()解锁。
ps:①synchronized
是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock
是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()
。
②synchronized
适合并发量小,并发量高使用ReentrantLock
。
③在使用synchronized 代码块时,可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。
④wait()方法会释放占有的对象锁,当前线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序;线程的sleep()方法则表示,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码。
wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会释放对象锁。
七.Synchronized与lock的区别?
lock是一个接口,主要有以下几个方法:
-
lock():获取锁,如果锁被暂用则一直等待
-
unlock():释放锁
-
tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
-
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,操作这个时间期限拿不到锁就返回false。
1.synchronized
是关键字,Lock是接口;
2.synchronized
是隐式的加锁,lock是显式的加锁;
3.synchronized
可以作用于方法上,lock只能作用于方法块;
4.synchronized
底层采用的是objectMonitor
,lock采用的AQS
;
5.synchronized
是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;
6.synchronized
在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;
7.synchronized
只支持非公平锁,lock支持非公平锁和公平锁;
8.synchronized
使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal);
9.lock
支持个性化定制, 使用了模板方法模式,可以自行实现lock方法;
10.一旦synchronized 块结束,就会自动释放对someObject
的占用。 lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行;
八.Synchronized底层实现原理,锁升级过程?
Synchronized底层通过⼀个monitor的对象来完成,每个对象有⼀个监视器锁(monitor)。当monitor被占⽤时就会处于锁定状态,线程执⾏monitorenter指令时尝 试获取monitor的所有权,过程如下:
(1)如果monitor的进⼊数为0,则该线程进⼊monitor,然后将进⼊数设置为1,该线程即为monitor的所有者。
(2)如果线程已经占有该monitor,只是重新进⼊,则进⼊monitor的进⼊数加1。
(3)如果其他线程已经占⽤了monitor,则该线程进⼊阻塞状态,直到monitor的进⼊数为0,再重新尝试获取monitor的所有权。
执⾏monitorexit的线程必须是object所对应的monitor的所有者。指令执⾏时,monitor的进⼊数减1,如果减1 后进⼊数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获 取这个monitor的所有权。
Synchronized是可重入锁。
简单理解:Synchronized基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,通过进入与退出对象的Monitor来实现方法与代码块同步。对象的JDK1.5之后 ,Synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁。
锁升级
对象头:每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。
- 偏向锁(无锁)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
- 轻量级锁(CAS):
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)
- 重量级锁:
如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
九.悲观锁与乐观锁
悲观锁(悲观并发控制)
当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发的发生。
为什么叫做悲观锁呢?因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。
数据库中的行锁,表锁,读锁,写锁,以及 syncronized 实现的锁均为悲观锁。
乐观锁
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。
十.乐观锁的实现方式?
-
版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
-
CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数:
需要读写的内存值 V 进行比较的值 A 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
十一.什么是ABA问题?
ABA 问题是乐观锁一个常见的问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
十二.i++是线程安全的吗?如何解决线程安全性?
1、i++作用域在局部方法中是不会出现线程安全问题的,只有在全局变量中才会出现线程安全问题
2、volatile只能保证变量对其他线程的可见性,并不能保证原子性操作
3、可以对i++操作使用同步锁,或者使用Atomic*包修饰共享变量,来保证原子性操作
如果i是全局变量,则会出现安全问题;如果i是局部变量,则是线程安全的
java中对共享变量的操作原理
共享变量存储在主内存中,当某个线程需要对共享变量进行操作时,需要将共享变量拷贝一份到自己的工作内存中(线程私有),操作完成之后,就会将最新的结果刷新到主存中。这里的线程安全问题就在于,当一个线程将主存中的数据读取到自己的工作内存之后,没来操作完成,那另一个线程又将主存数据读取并操作,很显然,前者对于主存变量的操作就会被覆盖,从而引发线程安全问题。
解决方案
-
使用volatile字段对共享变量进行修饰。
volatile字段的作用是让改变量对其他所有线程可见,但是并不能保证操作的原子性。仍然会出现多个线程同时读取主内存变量的情况。 -
加同步锁,比如使用synchronized关键字修饰,保证只有一个线程可以对主存变量进行操作。
public class demo { private int value; public synchronized void increase() { value++; } }
-
使用Atomic*类修饰来保证原子性
public class demo { private AtomicInteger value; public void increase() { value.incrementAndGet(); } }
十三.ThreadLocal原理,应用场景以及会产生的问题(key弱引用)
ThreadLocal
,即线程本地变量。如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地[内存]里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。
// 创建一个ThreadLocal变量
private static ThreadLocal<String> localVariable = new ThreadLocal<>();
Thread
类中,有个ThreadLocal.ThreadLocalMap
的成员变量。ThreadLocalMap
内部维护了Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型对象值。
原理:
- Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
- ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
- 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。
- 强引用:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
- 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
- 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
- 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
内存泄漏
- 如果Key使用强引用:当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。
- 如果Key使用弱引用:当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
ThreadLocal的应用场景和使用注意点
hreadLocal的很重要一个注意点,就是使用完,要手动调用remove()。
ThreadLocal的应用场景主要有以下这几种:
- 使用日期工具类,当用到
SimpleDateFormat
,使用ThreadLocal
保证线性安全 - 全局存储用户信息(用户信息存入
ThreadLocal
,那么当前线程在任何地方需要时,都可以使用) - 保证同一个线程,获取的数据库连接Connection是同一个,使用
ThreadLocal
来解决线程安全的问题 - 使用
MDC
保存日志信息。
十四.分布式锁的实现方式以及运用场景?
https://editor.csdn.net/md/?articleId=128195242
十五.线程池的原理,执行流程?
-
线程池七大参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {}
ThreadPoolExecutor 构造器
-
CorePoolSize: 核心线程数,不会被销毁
-
MaximumPoolSize : 最大线程数 (核心+非核心) ,非核心线程数用完之后达到空闲时间会被销毁
-
KeepAliveTime: 非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
-
Unit: 空闲时间单位
-
WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队
-
SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务;
-
LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
-
ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小
-
ThreadFactory:使用ThreadFactory创建新线程。 推荐使用Executors.defaultThreadFactory
-
Handler: 拒绝策略,任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
- AbortPolicy丢弃任务并抛出RejectedExecutionException异常;
- DiscardPolicy丢弃任务,但是不抛出异常;
- DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
- CallerRunsPolicy由调用线程处理该任务
可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。
线程池执行流程
corePoolSize,maximumPoolSize,workQueue之间关系。
-
当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程(使用核心)执行任务,即使此时线程池中存在空闲线程。
-
当线程池中线程数达到corePoolSize时(核心用完),新提交任务将被放入workQueue中,等待线程池中任务调度执行 。
-
当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程(非核心)执行任务。
-
当workQueue已满,且提交任务数超过maximumPoolSize(线程用完,队列已满),任务由RejectedExecutionHandler处理。
-
当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。
-
当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。
线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略
-
十六.jdk提供的常见4种线程池,以及带来的问题,为什么阿里规范
建议自定义线程池
-
Executors.newFixedThreadPool(10)
:固定大小 core = 自定义的线程数,但阻塞[队列]是无界队列,会OOM
内存溢出它的核心线程数 和 最大线程数是一样,都是nThreads变量的值,该变量由用户自己决定,所以说是固定大小线程池。此外,它的每隔0毫秒回收一次线程,换句话说就是不回收线程,因为它的核心线程数 和 最大线程数是一样,回收了没有任何意义。此外,使用了LinkedBlockingQueue队列,该队列其实是有界队列,很多人误解了,只是它的初始大小比较大是integer的最大值。
-
Executors.newCachedThreadPool():
它的核心线程数是0,最大线程数是integer的最大值,每隔60秒回收一次空闲线程,使用
SynchronousQueue
队列。SynchronousQueue
队列比较特殊,内部只包含一个元素,插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。 -
Executors.newSingleThreadExecutor();
单线程的线程池,后台从队列里取,挨个执行。阻塞队列是无界队列,会OOM
内存溢出。-
有且仅有一个工作线程执行任务,核心线程数固定是1
-
所有任务按照指定顺序执行,即遵循队列的入队出队规则。
-
适用:一个任务一个任务执行的场景。 如同队列
-
-
Executors.newScheduledThreadPool();
带有定时任务的线程池。它的核心线程数是corePoolSize变量,需要用户自己决定,最大线程数是integer的最大值,同样,它的每隔0毫秒回收一次线程,换句话说就是不回收线程。使用了DelayedWorkQueue队列,该队列具有延时的功能。
十七.线程池最大线程数怎么配?
-
经验值
配置线程数量之前,首先要看任务的类型是 IO密集型,还是CPU密集型?
什么是IO密集型? 比如:频繁读取磁盘上的数据,或者需要通过网络远程调用接口。 什么是CPU密集型? 比如:非常复杂的调用,循环次数很多,或者递归调用层次很深等。
IO密集型配置线程数经验值是:2N,其中N代表CPU核数。
CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。
如果获取N的值?
int availableProcessors = Runtime.getRuntime().availableProcessors();
2.最佳线程数目算法
除了上面介绍是经验值之外,其实还提供了计算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
很显然线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 虽说最佳线程数目算法更准确,但是线程等待时间和线程CPU时间不好测量,实际情况使用得比较少,一般用经验值就差不多了。再配合系统压测,基本可以确定最适合的线程数。
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
十八.Synchronized与Volatile的区别
1、volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;volatile只能修饰变量,而synchronized可以修饰方法,代码块。随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用synchronized的比率还是很大的。
2、多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。
3、volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以保证可见性。
4、关键字volatile解决的是变量在多个线程之间的可见性;synchronized关键字解决多个线程之间访问公共资源的同步性。
十九.线程三大特性
1、原子性:
多线程中的原子性,即一个操作或多个操作要么全部执行并且执行过程不能被打断,或者要么全部不执行。
2、可见性:
可见性是指多线程在访问一个变量时,一个线程修改了这个变量值,其他线程能够立刻看得到想修改指,显然对于单线程来说,可见性问题是不存在的。
3、有序性:
有序性指程序执行的顺序按照代码的先后顺序执行。
ynchronized的比率还是很大的。
2、多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。
3、volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以保证可见性。
4、关键字volatile解决的是变量在多个线程之间的可见性;synchronized关键字解决多个线程之间访问公共资源的同步性。
十九.线程三大特性
1、原子性:
多线程中的原子性,即一个操作或多个操作要么全部执行并且执行过程不能被打断,或者要么全部不执行。
2、可见性:
可见性是指多线程在访问一个变量时,一个线程修改了这个变量值,其他线程能够立刻看得到想修改指,显然对于单线程来说,可见性问题是不存在的。
3、有序性:
有序性指程序执行的顺序按照代码的先后顺序执行。