线程池初识
简单来说,就是对执行线程进行有效控制的同时,也减少性能损耗;具体的优势:
- a. 重用存在的线程,减少线程对象创建、消亡的开销,性能佳。
- b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- c. 提供定时执行、定期执行、单线程、并发数控制等功能。
几个重要概念
- 线程对象:指占用cpu等资源的对象,也就是调用start执行过的任务
- 任务:Thread,runnable对象,run方法内的内容属于具体执行动作
- 核心线程:就是创建后,其存活时间与其依附进程一致的线程
- 缓存线程:创建后,执行任务完毕,在一定时间内仍没有任务需要执行,则会被销毁的线程
1、线程池的创建
创建线程池的关键有类Executors、ThreadPoolExecutor、ScheduledThreadPoolExecutor;前者提供了几种常用线程池,能满足日常需求所需,后者提供了具体的实现
1.1 Executors.newFixedThreadPool
固定核心线程个数的线程池;LinkedBlockingQueue默认容器大小 Integer.MAX_VALUE;
特点:
- 优先使用已创建线程对象
- 已创建线程不够使用时,若不超过核心线程数,则进行创建新线程对象
- 达到最大值,则不进行创建,新任务存放在队列中;核心线程任务执行完毕,则从队列中取出任务继续执行
- 创建线程对象不销毁
使用场景:任务密集型
public static ExecutorService newFixedThreadPool(int nThreads[, ThreadFactory threadFactory]) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
[, threadFactory]);
}
1.2 Executors.newSingleThreadExecutor
只有一个核心线程的线程池:只有一个线程池,相当于单线程,具有顺序执行的效果; 特点:
- 创建仅且仅有一个核心线程、并且不会销毁
- 核心线程未创建时,任务到来,创建线程,并执行
- 核心线程已经创建,任务到来时,存放在队列中;核心线程执行当前任务完毕后,从队列中取出任务继续执行
使用场景:互斥任务
public static ExecutorService newSingleThreadExecutor([ThreadFactory threadFactory]) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
[,threadFactory]);
}
1.3 Executors.newCachedThreadPool
创建只有缓存线程的线程池,存活时间1分钟;SynchronousQueue size大小为0的队列;
特点:
- 创建的线程,有存活时间限制,1分钟后就会被回收
- 任务到来时,如果没有存活的且空闲的线程,则进行创建一个线程
- 一个线程执行完成任务后,如果队列中没有任务,则进入空闲状态; 进入空闲状态的线程,若1分钟之内没有任务执行则进行销毁
使用场景任务稀疏类型
public static ExecutorService newCachedThreadPool([ThreadFactory threadFactory]) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()
[, threadFactory]);
}
1.4 Executors.newSingleThreadScheduledExecutor
单核心多缓存可定时的线程池;DelayedWorkQueue:一个动态扩容数组容器;
特点:
- 仅有一个核心线程作为执行任务,无可缓存线程
- 任务具有定时、延时特性
public static ScheduledExecutorService newSingleThreadScheduledExecutor([ThreadFactory threadFactory]) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1[, threadFactory]));
}
1.5 Executors.newScheduledThreadPool
多核心多缓存可定时的线程池; DelayedWorkQueue:一个动态扩容数组容器;
特点:
- 有多个核心线程,但无可缓存线程
- 任务具有定时、延时特性
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize[, ThreadFactory threadFactory]) {
return new ScheduledThreadPoolExecutor(corePoolSize[, threadFactory]);
}
2、ThreadPoolExecutor原理
2.1 整体处理策略
- 如果任务到来,核心线程数,小于设定值,则创建核心线程,立即执行当前任务
- 核心数已满,如果队列未满,加入队列,进行排队(特例:无核心线程时,且当前执行线程数为0,则创建个可缓存线程,执行队列任务)
- 如果队列已满,则创建可缓存线程,立即执行当前任务
- 如果创建可缓存线程失败,或者不是可运行状态,则进行拒绝任务处理,默认为丢弃策略
几个比较重要的成员变量
corePoolSize:核心线程个数
maximumPoolSize:最大线程个数,其减去核心线程个数为缓存线程个数
workQueue:存放阻塞任务的集合
keepAliveTime:缓存线程存活时间
threadFactory:线程创建工厂
handler:任务被拒绝处理对象
ctl:AtomicInteger对象,保存着线程数、线程池状态;地位29为线程数,高位3位为线程状态
2.2 位运算逻辑
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
ctl:高位代表状态,低位代表线程数;runStateOf 计算状态,workerCountOf计算线程数,ctlOf合成ctl值
2.3 提交任务逻辑
可以提交一个Runnable或者Callable对象的任务;newTaskFor对传入任务进行包装为FutureTask,使用execute方法进行执行
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
execute方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
- 首先对任务进行判空处理
- 获取线程数,如果小于核心线程数,添加任务,并进行执行,当前任务执行完毕后,默认不销毁,继续从队列中取出任务执行
- 大于核心线程数:
- 处于运行状态,且添加任务成功,如果当前执行线程数为0,则开启一个可缓存线程,有执行线程,则不做任何处理
- 处于运行状态,但添加任务到队列失败,则开启可缓存线程运行任务,如果开启可缓存线程失败,则拒绝任务
- 不在运行状态,拒绝任务
2.4 addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
从上述代码可以看出,线程使用锁的粒度很细,在开始时使用自旋+CAS操作达到增加线程数,后续开启线程执行过程才枷锁进行保护,具体情况如下:
- retry标签的两个for循环,也就是自旋;第一层for循环对状态进行判断:
- 如果运行状态大于showdown,则返回false,表示创建线程执行失败;因为用户调用了shutdownNow方法
- 如果处在showdown状态(也就是用户调用shutdown方法),当前队列为空或者任务不为空,则返回false 白话理解就是:如果你让立即关闭线程池,那么拒绝创建线程;如果只是关闭线程池,那么不接受后续任务,但是如果队列不为空,可以创建线程,队列为空,则拒绝创建线程
- 第二层for循环,增加线程数:
- 线程数增加成功,则两个for循环结束
- 线程数增加未成功,如果当前运行状态发生改变,从第一层for循环重新开发,状态未发生变化,则继续当前for循环
- 加锁,添加Worker,开启线程进行执行任务
- 进行状态检查,如果,状态不是预期,则返回false
- 状态没有问题,则添加worker到worker队列,并执行任务,返回true
2.5 Worker内部类
类继承了同步器(AQS),并实现了Runnable接口
成员变量:当前线程对象thread,启动时线程时的任务firstTask,当前线程总共执行任务数completedTasks
方法:实现了同步的语义,并有打断线程的方法
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
其run方法是执行外部类runWorker方法
2.6 runWorker方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
- Worker中firstTask不为空,或者getTask()获取任务不为空(也可以不为空时一直等待)时,则进行循环,也即是只要队列有任务,当前线程会一直执行下去
- beforeExecute(wt, task),afterExecute(task, thrown);任务执行前后跟踪方法
- 线程销毁的最后处理 processWorkerExit方法
- 在执行任务前会判断当前线程池状态,如果不是立即关闭或者之后状态,则不打断当前线程
2.7 getTask 方法
主要是自旋,检查状态,并根据状态和队列选择任务
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
- 如果运行状态大于stop,或者是showdown状态且队列为空,则返回null任务
- 第二个条件,返回null任务的条件比较复杂,下面条件必须同时满足:
- 线程数大于最大线程数 或者 线程存活时获取队列任务超时且当前线程大于核心线程数或者允许核心线程销毁
- 线程数大于1 或者 工作队列为空
- CAS操作线程数成功 【如果CAS操作失败,说明状态不对,重新循环】
- 如果线程数大于核心线程(如果核心线程可以销毁,其等同与可缓存线程,也就是核心线程数为0),则进行超查找数据poll,否则直接拿数据take,如果拿到的任务不为空,则返回,如果为空,则取任务超时,超时标志进入下个循环
2.8 原理小结
- 线程池需要队列满了之后才会建立可缓存线程,所以需要可缓存线程的,均要选择有限容量队列;
- 线程池,如果不选择存在可缓存线程,必须使用无容量大小的队列,否则就要处理拒绝策略
- 可以做到对任务执行进行监听,查看runWorker方法分析
- 可以让核心线程销毁,也就是等同于可缓存线程,设置allowCoreThreadTimeOut为true即可
3、ScheduledThreadPoolExecutor
继承ThreadPoolExecutor实现ScheduledExecutorService;主要原理和ThreadPoolExecutor类保持一致,执行方法来自ScheduledExecutorService接口;队列为DelayedWorkQueue,自动扩容的队列,也就是容量无限制
3.1 特点
- 可以执行延时、定时任务
- 可以执行一次性任务;
- 无核心线程数时,最多可创建一个非缓存线程,这个可缓存线程池,也是不会销毁的
3.2 执行定时任务方法delayedExecute
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
- 首先检查状态,如果已经shutdown了,则拒绝任务
- 当前状态再运行状态,但是添加任务后,状态改变了,如果满足下面条件,则取消任务
- 已经不在运行状态
- 任务不可再当前状态运行
- 移除任务成功
- 不在运行状态,且状态未发生变化,则确保线程启动
3.3 ensurePrestart方法
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
创建核心线程,或者 核心线程数为0时创建一个可缓存线程
3.4 周期任务执行策略
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
boolean canRunInCurrentRunState(boolean periodic) {
return isRunningOrShutdown(periodic ?
continueExistingPeriodicTasksAfterShutdown :
executeExistingDelayedTasksAfterShutdown);
}
final boolean isRunningOrShutdown(boolean shutdownOK) {
int rs = runStateOf(ctl.get());
return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
}
周期任务默认控制变量值false,延迟任务默认变量值为true;也就是shutdow后,延迟任务要执行,而周期任务不执行;
3.5 ScheduledFutureTask类
定时线程池,任务类
private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {
private final long sequenceNumber;
private volatile long time;
private final long period;
RunnableScheduledFuture<V> outerTask = this;
int heapIndex;
ScheduledFutureTask(Runnable r, V result, long triggerTime, long sequenceNumber) {
super(r, result);
this.time = triggerTime;
this.period = 0;
this.sequenceNumber = sequenceNumber;
}
ScheduledFutureTask(Runnable r, V result, long triggerTime,
long period, long sequenceNumber) {
super(r, result);
this.time = triggerTime;
this.period = period;
this.sequenceNumber = sequenceNumber;
}
ScheduledFutureTask(Callable<V> callable, long triggerTime,
long sequenceNumber) {
super(callable);
this.time = triggerTime;
this.period = 0;
this.sequenceNumber = sequenceNumber;
}
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.nanoTime(), NANOSECONDS);
}
public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
public boolean isPeriodic() {
return period != 0;
}
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}
public boolean cancel(boolean mayInterruptIfRunning) {
boolean cancelled = super.cancel(mayInterruptIfRunning);
if (cancelled && removeOnCancel && heapIndex >= 0)
remove(this);
return cancelled;
}
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
super.run();
else if (super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
}
run 方法
- 首先判断状态,如果不必继续运行,则取消
- 如果不是周期任务,则直接运行
- 如果需要重新运行,则设置下次运行时间,重新排队
reExecutePeriodic 方法
外部类方法
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
super.getQueue().add(task);
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
可以运行,则添加到队列,再次检查状态,不可以运行且移除队列成功,则取消任务,否则开启线程确认
3.5 DelayedWorkQueue任务队列
详细代码可以自行查看,主要讲解offer,poll,take方法
offer方法
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
if (i >= queue.length)
grow();
size = i + 1;
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
siftUp(i, e);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
结果返回必为true;第一接入数组第一个位置,后续通过siftUp加入
siftUp 方法
private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
int parent = (k - 1) >>> 1;
RunnableScheduledFuture<?> e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}
这个可以看出,采用完全二叉树,数组来存数据;采用堆排序思想,通过和父元素比较大小来确定位置;这里是最小堆
take方法
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0L)
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
- 自旋获取队列数据
- 如果队列头元素为空,则等待加入任务唤醒,从头继续执行
- [如果对头元素不为空,且]延迟时间小于0,则从堆中去除,并返回此任务
- [如果对头元素不为空,且]延迟时间大于0,如果之前已经有在执行带时间等待,则直接等待,等待结束后,从新循环处理
- [如果对头元素不为空,且]延迟时间大于0,如果之前未有在执行带时间等待,则等待delay时间,然后唤醒步骤4中等待处理
poll方法就不分析了
拥有此中任务队列的线程池,如果没有核心线程池,那么,创建的可缓存线程池,也不会销毁,也就是其就是核心线程
4、 线程池拒绝策略
策略类实现RejectedExecutionHandler接口,实现rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法;系统提供了四种基本的拒绝策略类;线程池默认使用AbortPolicy类策略
AbortPolicy类
直接抛出异常
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
DiscardPolicy类
不做任何处理
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
DiscardOldestPolicy类 丢弃队列头部元素,然后重新执行丢弃任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
CallerRunsPolicy类 直接继续运行
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
5、线程工厂ThreadFactory类
ThreadFactory接口,方法Thread newThread(Runnable r);目的提供有名字的线程,对线程属性统一处理,有利于后续线程问题排查
线程池中默认使用下面工厂类
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
这个线程工厂类主要做一下几个事
- 分组管理
- 递增创建线程名字
- 产生线程均非守护线程
- 线程优先级中等
技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!