ThreadPoolExecutor源码笔记(三)线程池的实现

1、5种状态

//线程池能够接受任务,并且可以运行队列中的任务
private static final int RUNNING    = -1 << COUNT_BITS;
//不接受新的任务,但是之前队列里面的任务还是会被调用[shutDown()之后的状态]
private static final int SHUTDOWN   =  0 << COUNT_BITS;
//不接受新的任务,不会执行队列中的任务,并且尝试去中断正在运行的任务[shutDownNow()之后的状态]
private static final int STOP       =  1 << COUNT_BITS;
//所有任务都已经终止,workCount值为0(workCount可以理解成线程的个数)转到TIDYING状态的线程即将要执行terminated()钩子方法
private static final int TIDYING    =  2 << COUNT_BITS;
//terminated()方法执行结束
private static final int TERMINATED =  3 << COUNT_BITS;

2、5种状态的转换

RUNNING -> SHUTDOWN:调用了shutdown()方法,或者线程池实现了finalize方法,在里面调用了shutdown方法。 
(RUNNING or SHUTDOWN) -> STOP:调用了shutdownNow方法 
SHUTDOWN -> TIDYING:当队列和线程池均为空的时候 
STOP -> TIDYING:当线程池为空的时候 

TIDYING -> TERMINATED:terminated()钩子方法调用完毕

3、线程池/任务队列,区别

    /**这个workers可以理解为真正的线程池,对它的操作都要通过mainLock上锁,操作完毕解锁
     * Set containing all worker threads in pool. Accessed only when holding mainLock.
     */
    private final HashSet<Worker> workers = new HashSet<>();
 
这个是任务队列,超过核心线程数的任务都会先放到这个任务队列中, 没有超过核心线程的任务,是直接放到workers线程池中,直接执行的。
private final BlockingQueue<Runnable> workQueue;

4、构造器

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0) throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;//核心线程个数
        this.maximumPoolSize = maximumPoolSize;//最大线程个数 最大线程个数要大于核心线程个数
	//线程池任务队列(线程池关键地方,有时候注重任务出队的顺序或者任务有优先级都要靠他来实现)
        this.workQueue = workQueue;
	//核心线程之外的线程如果达到了这个空闲时间线程自动关闭(可以通过allowsCoreThreadTimeOut()作用于核心线程)
        this.keepAliveTime = unit.toNanos(keepAliveTime);
	//线程工厂用来创建线程的,用于设置线程的优先级,名字,debug等
        this.threadFactory = threadFactory;
	//对reject的任务的处理(当workQueue满了并且达到了最大线程的个数时,或者调用了shutdown函数之后,再加入任务也是会reject的)
        this.handler = handler;
}
5、submit(...)在父类中(AbstractExecutorService)实现的, ThreadPoolExecutor源码笔记(一)
1)把传入的Runnable、Callable等数据组装成FutureTask ThreadPoolExecutor源码笔记(二)
2)调用execute(Runnable)执行FutureTask

6、execute(Runnable command)
1)活动线程小于corePoolSize的时候创建新的线程,活动线程大于corePoolSize时都是先加入到任务队列当中
 调用addWorker方法时会原子性的检测runState、workerCount,达到线程池上限时触发拒绝策略,返回false
 addWorker方法第一个参数是任务,第二个参数表示是否核心线程
2)如果任务成功添加到任务队列中,在执行任务前还是要再次检测线程池状态,如果状态非RUNNING,从队列中移除该任务并触发拒绝策略
 如果线程池线程数为0(没有指定核心线程数),会创建新的线程执行addWorker方法【参数二:是否核心数为上限】

3)如果任务无法添加到队列,会用核心线程之外的线程处理任务addWorker,如果执行失败,说明线程池shutdown或者饱和了,触发拒绝策略

public void execute(Runnable command) {
        if (command == null) throw new NullPointerException();
		//ctl是AtomicInteger,线程安全的。唠叨一句,AtomicInteger内部也是通过Unsafe.java来实现同步的。
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {//小于核心线程数
            if (addWorker(command, true))//第二个参数true,表示是使用corePoolSize作为上限
                return;//活动线程小于corePoolSize,不会走下面的逻辑了
            c = ctl.get();
        }
		//活动线程>=corePoolSize,先添加到workQueue中
		//offer()往队列中添加任务,成功返回true,满了返回false
        if (isRunning(c) && workQueue.offer(command)) {//线程池是RUNNING状态,且成功添加任务到队列
            int recheck = ctl.get();
            if (!isRunning(recheck) && remove(command))//又判断下线程池状态,非RUNNING,从队列移除该任务,然后调用reject(task)
                reject(command);
            else if (workerCountOf(recheck) == 0)//线程池处于RUNNING状态 || 线程池处于非RUNNING状态但是任务移除失败
                /* 
				 * 这行代码是为了SHUTDOWN状态下没有活动线程了,但是队列里还有任务没执行这种特殊情况。
				 * 添加一个null任务是因为SHUTDOWN状态下,线程池不再接受新任务,该方法会创建新的线程,从队列中获取任务并执行。
				 */
				addWorker(null, false);
        }
        else if (!addWorker(command, false))//非RUNNING状态拒绝新的任务 || 队列满了启动新的线程失败(workCount > maximumPoolSize)
            reject(command);
    }

7、Worker

内部类Worker是对任务的封装,所有submit的Runnable都被封装成了Worker,它本身也是一个Runnable, 

然后利用AQS(AbstractQueuedSynchronizer)实现了一个简单的非重入的互斥锁,实现互斥锁主要目的是为了中断的时候判断线程是在空闲还是运行。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
        final Thread thread;//执行任务的线程
        Runnable firstTask;//要执行的具体任务
        volatile long completedTasks;//该线程执行完成的任务数
        /**
	 * 通过传入的第一个task和线程工厂创建的线程,创建一个Worker对象,task可为null
         */
        Worker(Runnable firstTask) {
            setState(-1); //在调用runWorker()之前禁止中断 inhibit interrupts until runWorker
            this.firstTask = firstTask;
	//ThreadFactory默认是Executors.DefaultThreadFactory.java
            this.thread = getThreadFactory().newThread(this);//通过传入的/默认的线程工厂创建一个线程
        }
	//一被线程调用,就触发这个run()方法
        public void run() {
            runWorker(this);//调用ThreadPoolExecutor.runWorker(Worker)执行该任务
        }
	//是否独占状态,state != 0,表示锁定状态/独占状态
        protected boolean isHeldExclusively() {
            return getState() != 0;//state值为0:表示解锁状态,为1:表示锁定状态
        }
	/**
	 * 如果state为0(解锁状态),则state改为1(锁定状态),并设置当前线程为独占模式同步所有者
	 * 其实就是申请同步锁定,返回锁定结果,成功true,失败false
	 */
        protected boolean tryAcquire(int unused) {
	//这里又用到Unsafe.CAS做线程安全的比较与更新
            if (compareAndSetState(0, 1)) {//如果state为0,返回true并赋值为1;否则返回false
	//调用的AbstractQueuedSynchronizer的api,设置当前线程为独占模式同步的所有者
                setExclusiveOwnerThread(Thread.currentThread());
                return true;//成功获得锁
            }
            return false;
        }
	/**
	 * state只有0和1,互斥
	 * 解锁,设置独占模式同步所有者置为null,state改为0(解锁状态)
	 */
        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(); }
	//中断已经开始的任务,showDownNow()时终止所有线程,会调该方法
        void interruptIfStarted() {
            Thread t;
	//由于state初始化时为-1,所以runWorker()之前,thread是不会被interrupt()的
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

8、addWorker(Runnable task, boolean isCore)

/**
 * @param firstTask 第一个任务,可以为null。addWorker()时,会新增一个线程,去任务队列获取任务执行
 *	  所以execute()中有一个addWorker(null, false),用来新增一个线程去执行剩余的任务
 * @param core 是否以corePoolSize作为上限
 */
private boolean addWorker(Runnable firstTask, boolean core) {
        retry: for (;;) {//重试循环
            int c = ctl.get();
            int rs = runStateOf(c);//运行状态
	   /*
	    * 这条语句等价:rs >= SHUTDOWN && (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty())
             * 满足下列条件则直接返回false,线程创建失败:
             * 		rs > SHUTDOWN: STOP || TIDYING || TERMINATED 此时不再接受新的任务,且所有任务执行结束
             * 		rs = SHUTDOWN: firtTask != null 此时不再接受任务,但是仍然会执行队列中的任务
             * 		rs = SHUTDOWN: firtTask == null && workQueue.isEmpty() 见execute方法的addWorker(null, false)
			 *
             * 最后一种情况: SHUTDONW状态下,如果workQueue不为空继续往下执行
             * execute()只有workCount==0的时,addWorker(null, false),firstTask才会为null。
	     * 也就是说线程池SHUTDOWN了不再接受新任务,但是此时workQueue队列不为空,那么还得创建线程把任务给执行完才行。
	     */
            if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) {
                return false;
	    }
	/* 1)线程池状态为RUNNING
	 * 2)线程池状态为SHUTDOWN,但是任务队列还有任务未执行
	 */
            for (;;) {//又是死循环
                int wc = workerCountOf(c);//任务数量
		/* 1)wc >= CAPACITY: runState > SHUTDOWN || runState == SHUTDOWN && workQueue.isEmpty(),不再新增线程
                 * 也就是说,runState == SHUTDOWN && workQueue.isEmpty()是可以新增线程去执行队列中的任务的
		 * 2)后一段判断: wc(任务数量)达到线程池上限时,不在新增线程
		 */
                if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))//线程安全递增workCount
                    break retry;//跳出外层的重试循环
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)//线程池的状态发生变化,则重试
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
		//最后compareAndIncrementWorkerCount操作失败的话,重新跑内层循环
            }
        }
	// wokerCount递增成功
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);//创建worker对象
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
		//并发的访问线程池workers对象加锁
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
		//RUNNING状态 || SHUTDONW状态下清理队列中剩余的任务
                    if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) //线程还在使用中,那就没法执行worker了,非法线程状态
                            throw new IllegalThreadStateException();
                        workers.add(w);//任务添加到workers里,workers是个HashSet
                        int s = workers.size();
                        if (s > largestPoolSize)
		//largestPoolSize用来记录线程池达到过的最大大小
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {//任务添加到workers后释放锁
                    mainLock.unlock();
                }
                if (workerAdded) {//任务添加成功后,启动线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)//线程启动失败,移除线程池中对应任务,任务计数器-1
                addWorkerFailed(w);
        }
        return workerStarted;
    }
	//添加任务失败时,从线程池中移除worker,workerCount-1,同样有锁操作
	private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();//移除之前上锁
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();
            tryTerminate();
        } finally {//结束之后解锁
            mainLock.unlock();
        }
    }

9、runWorker(Worker ..)

    runWorker()是在Worker.run()中触发的,也就是在执行任务的子线程中触发任务添加成功后,启动线程,真正执行的是runWorker(Worker)。

runWorker是线程池的关键,一个线程一旦进入runWorker就很难停下来了

while (task != null || (task = getTask()) != null) {}
执行完一个任务后,会继续从workQueue里去拿,一直执行下去。无任务时通过workQueue.take()保活
final void runWorker(Worker w) {
	Thread wt = Thread.currentThread();//此时的当前线程,就是执行该任务的子线程
	Runnable task = w.firstTask;
	w.firstTask = null;//引用置空
	//Worker的构造函数中setState(-1)禁止线程中断,所以这里需要unlock允许中断
	w.unlock(); // allow interrupts
	
	/*用于标识是否异常终止,finally中processWorkerExit的方法会有不同逻辑
	 *为true的情况:1.执行任务抛出异常;2.被中断。*/
	boolean completedAbruptly = true;
	try {//如果getTask返回null那么getTask中会将workerCount递减,如果异常了这个递减操作会在processWorkerExit中处理
		while (task != null || (task = getTask()) != null) {
			w.lock();//禁止中断
			// If pool is stopping, ensure thread is interrupted;
			// if not, ensure thread is not interrupted.  This
			// requires a recheck in second case to deal with
			// shutdownNow race while clearing interrupt
			//如果线程池STOP/TIDYING/TERMINATED,而当前线程未中断,则中断线程
			if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) 
				 && !wt.isInterrupted())
				wt.interrupt();
			try {
				//任务执行前操作,子类重载该方法
				beforeExecute(wt, task);
				Throwable thrown = null;
				try {
				//真正执行任务 task其实就是FutureTask,上一篇说过。futureTask.run()->callable.call()
					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);//任务执行完毕后,清理线程池中数据
	}
}

10、getTask()

private Runnable getTask() {
	boolean timedOut = false; // Did the last poll() time out?
	for (;;) {
		int c = ctl.get();
		int rs = runStateOf(c);

		/* 1)rs >= STOP 此时不再处理任务队列中的任务
		 * 2)rs == SHUTDONW && workQueue.isEmpty 此时队列中已经没有任务,也就不用处理任务了
		 */
		if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
			/*getTask在runWorker中执行的,执行前worker添加到线程池中时计数器有递增
			 *此处返回null后该任务的执行线程就结束了所以计数器要workCount-1*/
			decrementWorkerCount();
			return null;
		}
		int wc = workerCountOf(c);
		//如果设置了允许核心线程闲置超时 || 任务数超过核心线程数,就是有时间限制了
		boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;//是否有超时限制
		/* 1) workCount > maximumPoolSize
		 * 2) timed && timedOut && (除了本任务外,还有其他任务在执行||任务队列已经没任务了)
		 *  	任务队列没任务,自然不需要再循环了。		
		 */
		if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
			if (compareAndDecrementWorkerCount(c))//workCount-1
			 /* 有超时限制时,循环走到这里线程就结束了
			  * 无超时限制,线程数大于线程池最大线程数,循环走到这里线程也结束
			  */
				return null;//结束本线程
			continue;//保留线程
		}
		/* 1)线程执行任务完成后触发processWorkerExit,该方法内部在RUNNING/SHUTDONW状态时,会addWorker(null,false)
		 * 	 也就是执行完之后线程池还活着,就再丢一个新线程进去,然后旧的线程就彻底结束了
		 * 2)这个新丢进来的线程进来时,发现workCount <= corePoolSize && 无超时限制 && RUNNING状态
		 *   那么会走到这一步,通过workQueue.take()保活,实现维持线程池核心线程数。
		 */
		try {
			/* 1)有超时限制:指定超时时间从队列中取任务
			 * 2)无超时限制:workQueue.take(),BlockingQueue,队列为空时阻塞,从而保证线程池核心线程一直存在
			 */
			Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
			if (r != null)
				return r;
			timedOut = true;
		} catch (InterruptedException retry) {
			timedOut = false;//线程被中断,重复循环
		}
	}
}
11、processWorkerExit //线程退出会执行这个方法做一些清理工作
private void processWorkerExit(Worker w, boolean completedAbruptly) {
	if (completedAbruptly) //默认为true,runWorker正常结束时会设置为false,否则就是异常
		decrementWorkerCount();//任务执行异常时workCount-1
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		completedTaskCount += w.completedTasks;
		workers.remove(w);//移除work,正常/异常,执行完都会remove
	} finally {
		mainLock.unlock();
	}
	tryTerminate();//尝试停止线程池
	int c = ctl.get();
	if (runStateLessThan(c, STOP)) {//还是RUNNING/SHUTDONW状态
		if (!completedAbruptly) {//正常执行完任务的处理
			//min是线程池最小闲置线程数,允许core超时,min为0,否则为corePoolSize
			int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
			if (min == 0 && ! workQueue.isEmpty())
				min = 1;//min为0,但是任务队列还有任务没执行,要留一个线程来执行队列中任务
			//workCount大于最小闲置线程数,该线程结束后不需要addWorker(null,false)补充线程
			if (workerCountOf(c) >= min) 
				return; // replacement not needed
		}
		addWorker(null, false);//workCount < min,线程池中线程低于最小闲置线程数,要补充线程。
	}
}

12、tryTerminate

final void tryTerminate() {
	for (;;) {
		int c = ctl.get();
		/*1)RUNNING状态,还要继续执行任务,不能停止线程池
		 *2)SHUTDOWN&&workQueue.notEmpty:虽然SHUTDOWN了,但是还有任务啊哟执行,不能停止线程池
		 *3)runState >= TIDYING:已经或者正在停止中,当然就不需要重复去停止了*/
		if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
			return;
		//到这里还剩下 SHUTDONW&&workQueue.isEmpty 、STOP这两种状态
		if (workerCountOf(c) != 0) { //还有任务没执行完
		//只中断一个闲置线程,发出中断信号,中断阻塞在获取任务的线程
			interruptIdleWorkers(ONLY_ONE);
			return;
		}

		final ReentrantLock mainLock = this.mainLock;
		mainLock.lock();
		try {//设置为TIDYING状态
			if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
				try {
					terminated();//给子类重载
				} finally {
					//设置为TERMINATED状态
					ctl.set(ctlOf(TERMINATED, 0));
		/* Condition termination = mainLock.newCondition()
		 * ThreadPoolExecutor中有个awaitTermination(),内部调用termination.awaitNanos(time)等待线程终止,
		 * 唤醒调用了 等待线程池终止的线程
		 * 唤醒所有等待线程池终止这个Condition的线程
		 */
					termination.signalAll();//继续awaitTermination ???
				}
				return;
			}
		} finally {
			mainLock.unlock();
		}
		// else retry on failed CAS
	}
}

13、shutdown、shutDownNow、reject

public void shutdown() {
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		checkShutdownAccess();
		advanceRunState(SHUTDOWN);//设置为SHUTDOWN,如果已经至少是这个状态那么则直接返回
		//中断闲置线程,SHOUTDOWN状态只是中断闲置线程,保证剩余任务能继续执行
		interruptIdleWorkers();
		onShutdown(); // hook for ScheduledThreadPoolExecutor
	} finally {
		mainLock.unlock();
	}
	tryTerminate();
}

public List<Runnable> shutdownNow() {
	List<Runnable> tasks;
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		checkShutdownAccess();
		advanceRunState(STOP);//设置为STOP,不接受新任务,不执行剩余任务
		interruptWorkers();//中断所有线程,这个调用的是work.interruptIfStarted()
		tasks = drainQueue();//返回队列中还没执行的任务
	} finally {
		mainLock.unlock();
	}
	tryTerminate();
	return tasks;
}

private void interruptIdleWorkers(boolean onlyOne) {
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		for (Worker w : workers) {//遍历线程池(work池)
			Thread t = w.thread;
			/* w.tryLock能获取到锁,说明该线程没有在运行,因为runWorker中执行任务会先lock,
			 * 保证了中断的肯定是空闲的线程。*/
			if (!t.isInterrupted() && w.tryLock()) {
				try {
					t.interrupt();
				} catch (SecurityException ignore) {
				} finally {
					w.unlock();
				}
			}
			if (onlyOne)//onlyOne只中断一个线程
				break;
		}
	} finally {
		mainLock.unlock();
	}
}

final void reject(Runnable command) {
	handler.rejectedExecution(command, this);//拒绝处理,默认只是抛出RejectedExecutionException异常
}
public static class AbortPolicy implements RejectedExecutionHandler {
	public AbortPolicy() { }
	//只是抛出RejectedExecutionException异常而已
	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
		throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
	}
}

总结

1)workQueue存放要执行的任务Runnable。workers存放的worker,worker包含任务Runnable和执行该任务的线程Thread

 2)线程池工作原理:

3)执行任务,未超过核心线程数时,任务添加到workers线程池中,用核心线程直接执行任务;
超过核心线程数时,任务添加到workQueue任务队列中,通过addWorker(null,false),
创建线程(new Worker()默认会创建一个线程)从workQueue中取任务来执行
4) 线程池如何维持核心线程数
a:一个任务的执行在runWorker中,执行完一个任务后会从workQueue去取任务继续执行,最终 runWorker的finally中调用processWorkerExit
b:线程执行任务完成后触发processWorkerExit,该方法内部 在RUNNING/SHUTDONW状态时,如果线程池workCount小于最小闲置线程数,会addWorker(null,false),也就是执行完之后线程池还活着,就再丢一个新线程进去,然后旧的线程就彻底结束了
c:这个新丢进来的线程进来时,发现 workCount <= corePoolSize && 无超时限制 && RUNNING状态
 那么会到任务队列中取,通过 workQueue.take()保活,实现维持线程池核心线程数。

d:线程池保活的关键在 runWorker(),该方法内部一个while循环,执行完当前任务后,会继续去workQueue里去取出来执行,直到取不到任务时,判断是否需要保活,需要保活的调用的是workQueue.take()。workQueue.take()取元素时有通过ReentrantLock.lock()/unlock();通过锁阻塞线程保活的原理看这里!

                e:没增加一个任务,如果线程数没达到最大线程数限制,会默认添加一个线程

参考资料:谢谢原作者

        https://segmentfault.com/a/1190000010353461

https://blog.csdn.net/wuyuxing24/article/details/50989530
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值