在我们的开发中“池”的概念并不罕见,有数据库连接池、线程池、对象池、常量池等等。下面我们主要针对线程池来一步一步揭开线程池的面纱。

使用线程池的好处

1、降低资源消耗:可以重复利用已创建的线程降低线程创建和销毁造成的消耗。

2、提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

3、提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

线程池的工作原理

首先我们看下当一个新的任务提交到线程池之后,线程池是如何处理的

1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。

2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步

3、线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务

线程池饱和策略

这里提到了线程池的饱和策略,那我们就简单介绍下有哪些饱和策略:

AbortPolicy

为Java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。

DiscardPolicy

直接抛弃,任务不执行,空方法

DiscardOldestPolicy

从队列里面抛弃head的一个任务,并再次execute 此task。

CallerRunsPolicy

在调用execute的线程里面执行此command,会阻塞入口

用户自定义拒绝策略(最常用)

实现RejectedExecutionHandler,并自己定义策略模式

下我们以ThreadPoolExecutor为例展示下线程池的工作流程图

1、如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2、如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3、如果无法将任务加入BlockingQueue(队列已满),则在非corePool中创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4、如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

线程池只是并发编程中的一小部分,下图是史上最全面的Java的并发编程学习技术总汇

关键方法源码分析

我们看看核心方法添加到线程池方法execute的源码如下:

////Executes the given task sometime in the future.  The task//may execute in a new thread or in an existing pooled thread.//// If the task cannot be submitted for execution, either because this// executor has been shutdown or because its capacity has been reached,// the task is handled by the current {@code RejectedExecutionHandler}.//// @param command the task to execute// @throws RejectedExecutionException at discretion of//         {@code RejectedExecutionHandler}, if the task//         cannot be accepted for execution// @throws NullPointerException if {@code command} is null//public void execute(Runnable command) {if (command == null)throw new NullPointerException();//// Proceed in 3 steps://// 1. If fewer than corePoolSize threads are running, try to// start a new thread with the given command as its first// task.  The call to addWorker atomically checks runState and// workerCount, and so prevents false alarms that would add// threads when it shouldn't, by returning false.// 翻译如下:// 判断当前的线程数是否小于corePoolSize如果是,使用入参任务通过addWord方法创建一个新的线程,// 如果能完成新线程创建exexute方法结束,成功提交任务// 2. If a task can be successfully queued, then we still need// to double-check whether we should have added a thread// (because existing ones died since last checking) or that// the pool shut down since entry into this method. So we// recheck state and if necessary roll back the enqueuing if// stopped, or start a new thread if there are none.// 翻译如下:// 在第一步没有完成任务提交;状态为运行并且能否成功加入任务到工作队列后,再进行一次check,如果状态// 在任务加入队列后变为了非运行(有可能是在执行到这里线程池shutdown了),非运行状态下当然是需要// reject;然后再判断当前线程数是否为0(有可能这个时候线程数变为了0),如是,新增一个线程;// 3. If we cannot queue task, then we try to add a new// thread.  If it fails, we know we are shut down or saturated// and so reject the task.// 翻译如下:// 如果不能加入任务到工作队列,将尝试使用任务新增一个线程,如果失败,则是线程池已经shutdown或者线程池// 已经达到饱和状态,所以reject这个他任务//int c = ctl.get();// 工作线程数小于核心线程数if (workerCountOf(c) < corePoolSize) {// 直接启动新线程,true表示会再次检查workerCount是否小于corePoolSizeif (addWorker(command, true))return;c = ctl.get();}// 如果工作线程数大于等于核心线程数// 线程的的状态未RUNNING并且队列notfullif (isRunning(c) && workQueue.offer(command)) {// 再次检查线程的运行状态,如果不是RUNNING直接从队列中移除int recheck = ctl.get();if (! isRunning(recheck) && remove(command))// 移除成功,拒绝该非运行的任务reject(command);else if (workerCountOf(recheck) == 0)// 防止了SHUTDOWN状态下没有活动线程了,但是队列里还有任务没执行这种特殊情况。// 添加一个null任务是因为SHUTDOWN状态下,线程池不再接受新任务addWorker(null, false);}// 如果队列满了或者是非运行的任务都拒绝执行else if (!addWorker(command, false))reject(command);}

下面我们继续看看addWorker是如何实现的:

private boolean addWorker(Runnable firstTask, boolean core) {// java标签retry:// 死循环for (;;) {int c = ctl.get();// 获取当前线程状态int rs = runStateOf(c);// Check if queue empty only if necessary.// 这个逻辑判断有点绕可以改成 // rs >= shutdown && (rs != shutdown || firstTask != null || workQueue.isEmpty())// 逻辑判断成立可以分为以下几种情况均不接受新任务// 1、rs > shutdown:--不接受新任务// 2、rs >= shutdown && firstTask != null:--不接受新任务// 3、rs >= shutdown && workQueue.isEmppty:--不接受新任务// 逻辑判断不成立// 1、rs==shutdown&&firstTask != null:此时不接受新任务,但是仍会执行队列中的任务// 2、rs==shotdown&&firstTask == null:会执行addWork(null,false)//  防止了SHUTDOWN状态下没有活动线程了,但是队列里还有任务没执行这种特殊情况。//  添加一个null任务是因为SHUTDOWN状态下,线程池不再接受新任务if (rs >= SHUTDOWN &&! (rs == SHUTDOWN && firstTask == null &&! workQueue.isEmpty()))return false;// 死循环// 如果线程池状态为RUNNING并且队列中还有需要执行的任务for (;;) {// 获取线程池中线程数量int wc = workerCountOf(c);// 如果超出容量或者最大线程池容量不在接受新任务if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))return false;// 线程安全增加工作线程数if (compareAndIncrementWorkerCount(c))// 跳出retrybreak retry;c = ctl.get();  // Re-read ctl// 如果线程池状态发生变化,重新循环if (runStateOf(c) != rs)continue retry;// else CAS failed due to workerCount change; retry inner loop}}// 走到这里说明工作线程数增加成功boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {final ReentrantLock mainLock = this.mainLock;w = new Worker(firstTask);final Thread t = w.thread;if (t != null) {// 加锁mainLock.lock();try {// Recheck while holding lock.// Back out on ThreadFactory failure or if// shut down before lock acquired.int c = ctl.get();int rs = runStateOf(c);// RUNNING状态 || SHUTDONW状态下清理队列中剩余的任务if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {// 检查线程状态if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();// 将新启动的线程添加到线程池中workers.add(w);// 更新线程池线程数且不超过最大值int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock();}// 启动新添加的线程,这个线程首先执行firstTask,然后不停的从队列中取任务执行if (workerAdded) {//执行ThreadPoolExecutor的runWoker方法t.start();workerStarted = true;}}} finally {// 线程启动失败,则从wokers中移除w并递减wokerCountif (! workerStarted)// 递减wokerCount会触发tryTerminate方法addWorkerFailed(w);}return workerStarted;}

addWorker之后是runWorker,第一次启动会执行初始化传进来的任务firstTask;然后会从workQueue中取任务执行,如果队列为空则等待keepAliveTime这么长时间

final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;// 允许中断w.unlock(); // allow interruptsboolean 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 interruptif ((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);}}

我们看下getTask是如何执行的

private Runnable getTask() {boolean timedOut = false; // Did the last poll() time out?// 死循环retry: for (;;) {// 获取线程池状态int c = ctl.get();int rs = runStateOf(c);// Check if queue empty only if necessary.// 1.rs > SHUTDOWN 所以rs至少等于STOP,这时不再处理队列中的任务// 2.rs = SHUTDOWN 所以rs>=STOP肯定不成立,这时还需要处理队列中的任务除非队列为空// 这两种情况都会返回null让runWoker退出while循环也就是当前线程结束了,所以必须要decrementif (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {// 递减workerCount值decrementWorkerCount();return null;}// 标记从队列中取任务时是否设置超时时间boolean timed; // Are workers subject to culling?// 1.RUNING状态// 2.SHUTDOWN状态,但队列中还有任务需要执行for (;;) {int wc = workerCountOf(c);// 1.core thread允许被超时,那么超过corePoolSize的的线程必定有超时// 2.allowCoreThreadTimeOut == false && wc >// corePoolSize时,一般都是这种情况,core thread即使空闲也不会被回收,只要超过的线程才会timed = allowCoreThreadTimeOut || wc > corePoolSize;// 从addWorker可以看到一般wc不会大于maximumPoolSize,所以更关心后面半句的情形:// 1. timedOut == false 第一次执行循环, 从队列中取出任务不为null方法返回 或者// poll出异常了重试// 2.timeOut == true && timed ==// false:看后面的代码workerQueue.poll超时时timeOut才为true,// 并且timed要为false,这两个条件相悖不可能同时成立(既然有超时那么timed肯定为true)// 所以超时不会继续执行而是return null结束线程。if (wc <= maximumPoolSize && !(timedOut && timed))break;// workerCount递减,结束当前threadif (compareAndDecrementWorkerCount(c))return null;c = ctl.get(); // Re-read ctl// 需要重新检查线程池状态,因为上述操作过程中线程池可能被SHUTDOWNif (runStateOf(c) != rs)continue retry;// else CAS failed due to workerCount change; retry inner loop}try {// 1.以指定的超时时间从队列中取任务// 2.core thread没有超时Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();if (r != null)return r;timedOut = true;// 超时} catch (InterruptedException retry) {timedOut = false;// 线程被中断重试}}}

下面我们看下processWorkerExit是如何工作的

private void processWorkerExit(Worker w, boolean completedAbruptly) {// 正常的话再runWorker的getTask方法workerCount已经被减一了if (completedAbruptly)decrementWorkerCount();final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// 累加线程的completedTaskscompletedTaskCount += w.completedTasks;// 从线程池中移除超时或者出现异常的线程workers.remove(w);} finally {mainLock.unlock();}// 尝试停止线程池tryTerminate();int c = ctl.get();// runState为RUNNING或SHUTDOWNif (runStateLessThan(c, STOP)) {// 线程不是异常结束if (!completedAbruptly) {// 线程池最小空闲数,允许core thread超时就是0,否则就是corePoolSizeint min = allowCoreThreadTimeOut ? 0 : corePoolSize;// 如果min == 0但是队列不为空要保证有1个线程来执行队列中的任务if (min == 0 && !workQueue.isEmpty())min = 1;// 线程池还不为空那就不用担心了if (workerCountOf(c) >= min)return; // replacement not needed}// 1.线程异常退出// 2.线程池为空,但是队列中还有任务没执行,看addWoker方法对这种情况的处理addWorker(null, false);}}

tryTerminate

processWorkerExit方法中会尝试调用tryTerminate来终止线程池。这个方法在任何可能导致线程池终止的动作后执行:比如减少wokerCount或SHUTDOWN状态下从队列中移除任务。

final void tryTerminate() {for (;;) {int c = ctl.get();// 以下状态直接返回:// 1.线程池还处于RUNNING状态// 2.SHUTDOWN状态但是任务队列非空// 3.runState >= TIDYING 线程池已经停止了或在停止了if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && !workQueue.isEmpty()))return;// 只能是以下情形会继续下面的逻辑:结束线程池。// 1.SHUTDOWN状态,这时不再接受新任务而且任务队列也空了// 2.STOP状态,当调用了shutdownNow方法// workerCount不为0则还不能停止线程池,而且这时线程都处于空闲等待的状态// 需要中断让线程“醒”过来,醒过来的线程才能继续处理shutdown的信号。if (workerCountOf(c) != 0) { // Eligible to terminate// runWoker方法中w.unlock就是为了可以被中断,getTask方法也处理了中断。// ONLY_ONE:这里只需要中断1个线程去处理shutdown信号就可以了。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));// 继续awaitTerminationtermination.signalAll();}return;}} finally {mainLock.unlock();}// else retry on failed CAS}}

shutdown这个方法会将runState置为SHUTDOWN,会终止所有空闲的线程。shutdownNow方法将runState置为STOP。和shutdown方法的区别,这个方法会终止所有的线程。主要区别在于shutdown调用的是interruptIdleWorkers这个方法,而shutdownNow实际调用的是Worker类的interruptIfStarted方法:

他们的实现如下:

public void shutdown() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();// 线程池状态设为SHUTDOWN,如果已经至少是这个状态那么则直接返回advanceRunState(SHUTDOWN);// 注意这里是中断所有空闲的线程:runWorker中等待的线程被中断 → 进入processWorkerExit →// tryTerminate方法中会保证队列中剩余的任务得到执行。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();// STOP状态:不再接受新任务且不再执行队列中的任务。advanceRunState(STOP);// 中断所有线程interruptWorkers();// 返回队列中还没有被执行的任务。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) {Thread t = w.thread;// w.tryLock能获取到锁,说明该线程没有在运行,因为runWorker中执行任务会先lock,// 因此保证了中断的肯定是空闲的线程。if (!t.isInterrupted() && w.tryLock()) {try {t.interrupt();} catch (SecurityException ignore) {} finally {w.unlock();}}if (onlyOne)break;}}finally {mainLock.unlock();}
}
void interruptIfStarted() {Thread t;// 初始化时state == -1if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {try {t.interrupt();} catch (SecurityException ignore) {}}
}

线程池的使用

线程池的创建

我们可以通过ThreadPoolExecutor来创建一个线程池

/*** @param corePoolSize 线程池基本大小,核心线程池大小,活动线程小于corePoolSize则直接创建,大于等于则先加到workQueue中,* 队列满了才创建新的线程。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,* 等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,* 线程池会提前创建并启动所有基本线程。* @param maximumPoolSize 最大线程数,超过就reject;线程池允许创建的最大线程数。如果队列满了,* 并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务* @param keepAliveTime* 线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率* @param unit  线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、* 毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)* @param workQueue 工作队列,线程池中的工作线程都是从这个工作队列源源不断的获取任务进行执行*/public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {// threadFactory用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);}

向线程池提交任务

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。

threadsPool.execute(new Runnable() {@Overridepublic void run() {}});

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask);try{Object s = future.get();}catch(InterruptedException e){// 处理中断异常}catch(ExecutionException e){// 处理无法执行任务异常}finally{// 关闭线程池executor.shutdown();}

关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

合理的配置线程池

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。

1、任务的性质:CPU密集型任务、IO密集型任务和混合型任务。

2、任务的优先级:高、中和低。

3、任务的执行时间:长、中和短。

4、任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行

如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有时候我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。

线程池的监控

如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性

  • taskCount:线程池需要执行的任务数量。

  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。

  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。

  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。

  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。

作者:fuyuwei2015
原文链接:

https://blog.csdn.net/fuyuwei2015/article/details/72758179

面试官常问的线程池,你真的了解吗?相关推荐

  1. 面试官常问的 web前端 问题(二)

    面试官常问的 web前端 问题 11-20 11.什么是响应式设计? 12.为什么我们要弃用 table 标签 13.iframe 有哪些缺点 14.meta viewport 是做什么用的,怎么写? ...

  2. 平面设计面试官常问的问题有哪些?

      平面设计面试官常问的问题有哪些?一般情况下都会有一个开篇的自我介绍,这里建议就用平常聊天说话的语气语速来介绍就可以,介绍自己扬长避短,多讲自己的前工作项目经验,在校获奖也可以讲.与工作岗位不强相关 ...

  3. 面试官常问的 web前端 问题(四)

    面试官常问的 web前端 问题 31-40 31.使用 Vue 的好处 32.MVVM 定义 33.Vue 的生命周期(重点) 34.Vue 的响应式原理 35.第一次页面加载会触发哪几个钩子? 36 ...

  4. 面试官:说一下线程池内部工作原理?

    以下文章来源方志朋的博客,回复"666"获面试宝典 作者:清泉 cnblogs.com/qingquanzi/p/8146638.html 随着cpu核数越来越多,不可避免的利用多 ...

  5. Android岗大厂面试官常问的那些问题,知乎上转疯了!

    前言 对于android开发,我们大部分工作都是在应用层,但为了体现"技术含量",以及"知其所以然",以便在遇到具体问题时不至于束手无策,因此有必要了解底层的工 ...

  6. android自学视频!Android岗大厂面试官常问的那些问题,终局之战

    前言 说起程序员人们的第一印象就是工资高.加班凶.话少钱多头发少.再加上现在科技互联网公司太吃香,bat.华为小米等公司程序员加班情况被广泛传播,程序员用生命在敲代码的印象刻在了很多人的心里. 与其它 ...

  7. 【拥抱大厂系列】几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!

    点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了3个月总结的一线大厂Java面试总结,本人已拿腾 ...

  8. 几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:牛人 20000 字的 Spring Cloud 总结,太硬核了~ 先瞎比比一下,上一篇文章已经过去2个多月了, ...

  9. 爆赞!Android岗大厂面试官常问的那些问题,论程序员成长的正确姿势

    开头 昨天去面了一家公司,价值观有受到冲击. 面试官技术方面没的说,他可能是个完美主义的人,无论什么事情到了他那里好像都有解决的方案,我被说的无所适从,感觉他很厉害. 但我不能认可的是,面试官觉得加班 ...

  10. 想拿高工资?那些年Java面试官常问的知识点项目实战

    Kubernetes简介 Kubernetes(简称K8S,K和S之间有8个字母)是用于自动部署,扩展和管理容器化应用程序的开源系统.它将组成应用程序的容器组合成逻辑单元,以便于管理和服务发现.Kub ...

最新文章

  1. ftl不存在为真_LTL和FTL货运之间有什么区别?
  2. web标准常见问题整理
  3. 多对多关联查询sql语句
  4. c语言讲输入退回缓冲区_开始之前的结束-如何不退回输入错误的用户电子邮件...
  5. Spark Streaming 遇到 kafka
  6. 力扣450. 删除二叉搜索树中的节点(JavaScript)
  7. WPF TextBox提示文字设定
  8. 感谢那些打赏赞助过我的人
  9. 蓝牙数字密钥建立在过去实践的基础上, 以创造更安全的未来
  10. cocos creator fgui 按钮事件
  11. stc15f2k60f2单片机定时器_STC15F2K60S2系列
  12. 河北大学计算机系院长,徐建民(河北大学网络空间安全与计算机学院教授)_百度百科...
  13. Web爬虫|入门教程之爬虫简介
  14. 阅读这篇文章,假设你不知道的傅里叶变换,然后来掐死我
  15. AnySDK+GooglePlay对接1
  16. 软件工程第2次作业 | 结对项目-最长单词链
  17. matlab 2014a 安装教程(内附破解文件)
  18. 【附源码】Java计算机毕业设计基于微信小程序停车系统(程序+LW+部署)
  19. 计算机视觉——SIFT特征提取与检索+匹配地理标记图像+RANSAC算法
  20. 《“边缘计算+”技术白皮书》发布!

热门文章

  1. 面向对象(Python):学习笔记之异常
  2. dedecms关于访问量的修改
  3. vector容器中是否应该放指针?解决方法
  4. 小目标Trick | Detectron2、MMDetection、YOLOv5都通用的小目标检测解决方案
  5. Unity Android 使用UnityWebRequest Post 数据后,没有获得服务器返回的数据
  6. 关于deployment descripter(web.xml)的认识
  7. PHP中“简单工厂模式”实例讲解(转)
  8. vivado与modelsim的联合仿真
  9. linux下获取主机信息
  10. SQL2008 行锁使用RowLock