简介

ThreadPoolExecutor,线程池的基石。

概述

线程池,除了用HashSet承载一组线程做任务以外,还用BlockingQueue承载一组任务。corePoolSize和maximumPoolSize,分别表示线程弛里存活的最小和最大线程数目,keepAliveTime表示不干活的线程的存活时间。当过来一个任务时,如果线程池里的线程数目小于corePoolSize,那么直接创建一个线程去处理它;如果线程数目大于等corePoolSize并且小于maximumPoolSize,那么,将这个任务放进任务队列里;如果任务队列已满,则继续创建线程处理该任务;如果线程弛数目等于maximumPoolSize,此时任务队列肯定已经满了,那么采取饱和策略。如果线程池里的线程空闲时间达到keepAliveTime,并且数量大于corePoolSize,此刻任务队列肯定是空的,那么销毁该线程。

线程池的控制状态

 // 线程池的控制状态,高3位表示运行状态,低29位表示线程的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // 29位的偏移量
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 最大容量 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; // 整理中,所有的任务都已经停止,线程数为0,并调用terminate(钩子)方法
private static final int TERMINATED = 3 << COUNT_BITS; // 终止,terminate方法运行完毕 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;
}

状态转换关系

RUNNING -> SHUTDOWN                     // shutdown()方法被调用
(RUNNING or SHUTDOWN) -> STOP    // shutdownNow()方法被调用
SHUTDOWN -> TIDYING                       // 线程池和任务队列都为空
STOP -> TIDYING                                   // 线程池为空
TIDYING -> TERMINATED                     // terminated()方法运行完毕

属性

     private final BlockingQueue<Runnable> workQueue; // 任务队列
private final ReentrantLock mainLock = new ReentrantLock(); // 可重入锁
private final HashSet<Worker> workers = new HashSet<Worker>(); // 线程集合
private final Condition termination = mainLock.newCondition(); // 终止条件
private int largestPoolSize; // 最大线程池容量
private long completedTaskCount; // 已完成任务数量
private volatile ThreadFactory threadFactory; // 线程工厂
private volatile RejectedExecutionHandler handler; // 饱和策略
private volatile long keepAliveTime; // 线程等待时间
private volatile boolean allowCoreThreadTimeOut; // 是否允许核心线程超时
private volatile int corePoolSize; // 核心线程池大小
private volatile int maximumPoolSize; // 最大线程池大小
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); // 默认饱和策略

Worker

继承关系

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {}

属性

         final Thread thread; // 承载的线程
Runnable firstTask; // 首任务
volatile long completedTasks; // 已完成任务数量

构造方法

         Worker(Runnable firstTask) {
setState(-1); // 执行任务之前,禁止中断
this.firstTask = firstTask; // 初始化首任务
this.thread = getThreadFactory().newThread(this); // 初始化线程
}

主要方法

         public void run() { // 重写Runnable的run方法
runWorker(this);
} protected boolean isHeldExclusively() { // 是否被独占,0否,1是
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()) {// 1. 状态大于等于0 2.线程不为空 3该线程没被中断
try {
t.interrupt(); // 中断
} catch (SecurityException ignore) {
}
}
}
}

execute(Runnable)

     public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get(); // 获取线程池的控制状态
if (workerCountOf(c) < corePoolSize) { // 1. worker数量小于corePoolSize,
// 创建线程
if (addWorker(command, true))
return; // 成功返回
c = ctl.get(); // 失败,再次获取控制状态(调用addWorker方法,该状态更改过)
}
if (isRunning(c) && workQueue.offer(command)) {// 2.查看线程池是否处于运行状态,是则说明worker数量不满足条件1,因此任务入队;
// 否则,线程池处于非运行状态,进入3(最后会reject);要么入队失败,也进入3(也许会成功)
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);
}

状态再检查,进一步推广到变量可见性。1.两次使用之间,显式更新了变量,此时要重新获取,以便得到最新值。2.根据此变量的值,执行某项策略,需要回过头来再次检查,如果改变,则回滚。(乐观锁)

addWorker(Runnable, boolean)

     private boolean addWorker(Runnable firstTask, boolean core) {
retry: for (;;) { // A. runState变化,重试
int c = ctl.get(); // ctl
int rs = runStateOf(c); // runState
// 等价于
// if (!(rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())))
// return false;
// 也即是
// if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
// 接着往下走:a. rs == RUNNING; 或者,b. rs == SHUTDOWN, 并且,firstTask == null, 再并且工作队列不为空
// a. 不必解释
// b. rs == SHUTDOWN时,此时,不再接受新任务,但是,工作队列里的任务还是要处理的,若是线程池里没有线程了,还是需要新增线程处理这些任务的
// 如何与由新任务发起的创建线程做区分呢?答案就是firstTask是否为null,不为null就是新任务发起的
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false; for (;;) { // B.workerCount变化,重试
int wc = workerCountOf(c);
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) // 校验workerCount
return false;
if (compareAndIncrementWorkerCount(c)) // 竞态点,可能失败:1.runState变化 -> A 2.workerCount变化 -> B
break retry;
c = ctl.get(); // 重新读取ctl
if (runStateOf(c) != rs) // 如果runState发生变化,转向A;否则,转向B
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(); // 因为要对workers操作,加锁
try {
int rs = runStateOf(ctl.get()); // 再次读取runState, 因为在此过程中,可能会有别的线程操作此变量
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { // C 第二次校验runState,与上面不同的是,这次没有校验工作队列是否为空
if (t.isAlive()) // 检验该线程是否已经启动,正常没有启动
throw new IllegalThreadStateException();
workers.add(w); // 添加到线程池里
int s = workers.size(); // 工作者的实际数量
if (s > largestPoolSize)
largestPoolSize = s; // 记录工作者最大数量
workerAdded = true; // 添加成功
}
} finally {
mainLock.unlock(); // 解锁,别的线程可以操作workers了
}
if (workerAdded) { // 如果添加成功
t.start(); // 启动线程
workerStarted = true; // 线程启动成功
}
}
} finally {
if (!workerStarted) // 如果线程启动失败
addWorkerFailed(w); // 回滚
}
return workerStarted; // 返回结果
}

代码注释C处,第二次校验runState时,为什么没有校验工作队列workQueue了呢?

考虑这样一种场景

  1. 线程池里的线程没有空闲的,都在工作,执行任务。
  2. 其中一个线程由于某种原因,跳出了while 循环。也许是某一时刻,workQueue里的任务被其他线程取空了,到此线程时,阻塞在workQueue.take()方法上了,而后又刚好遇到中断(各种原因),于是返回null并跳出了循环。
  3. 后来,workQueue又持续添加了新的任务,其他线程接着工作,中间没收到打扰。
  4. 接着,runState变为SHUTDOWN或者在第8步之后变为SHUTDOWN,此时还是RUNNING ,那个跳出循环的线程,我们暂且称为JUMP线程吧,走到addWork(null,false)方法。
  5. 在双重for循环检测时,恰巧满足条件
    if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))

    于是,会继续往下执行,此时,其他的线程仍在紧锣密鼓地工作。

  6. 注意,JUMP线程已经执行过tryTerminate()方法了,并且此线程马上就消亡了,调用addWork(null,false)方法也是为了创建新的线程替代它的。
  7. JUMP线程走到创建工作者那里时,也就是再次(第二次)检验runState的地方。
  8. 恰巧这时,workQueue被其他线程取空了,更巧的是,所有其他的线程都阻塞在了workQueue.take()方法上,从getTask() 方法逻辑可知,这是发生在runState变为SHUTDOWN之前,不然后面的线程会检查runState状态,直接return null了。
  9. 好了,时间停在了这一刻:JUMP线程准备第二次校验runState,其他线程阻塞在workQueue.take()方法上;不过,在runState变为SHUTDOWN时,也就是调用shutdown()方法,会调用tryTerminate()方法。
  10. 在tryTerminate()方法里,检查runState为SHUTDOWN,workQueue为空,但threadPool不为空,于是会走interruptIdleWorkers(ONLY_ONE)方法,随意中断一个空闲线程,注意,这里没有走到terminated()方法。
  11. 一个阻塞在workQueue.take()方法上的线程被唤醒,并跳出while循环,runWorker(Worker)里的逻辑,然后调用tryTerminate()方法,同步骤10,接着调用addWorker(null, false),当然在方法开始处,双重for循环那里就返回了,因为不满足条件,此刻workQueue已经为空。
  12. 就这样,10->11->10->11,一个接一个地调用,中断后面的阻塞着的线程并传播下去,直到最后一个,调用tryTerminate()方法,由于workCount不为0.,因为JUMP线程在双重for循环那里通过了检查,使得workCount加1了,于是,这最后一个被中断叫醒的线程也是走的第10步,只是没有空闲线程可以中断了。
  13. 最后,JUMP在第二次检查runState时,不应该再检查workQueue是否为空了,如果检查,由于workQueue为空,那么将会回滚,JUMP线程没有加入到threadPool里面去,那么便没有线程调用最终的terminated()方法了。
  14. 由于都是异步的,以上步骤并不具有严格的时间顺序。
  15. 第13步有误,即便由于检查了workQueue为空而回滚,也会调用terminated()方法的,因为回滚的时候会调用addWorkerFailed(Worker)方法,这个方法会调用tryTerminate()方法,因为此时满足了条件继而terminated()方法。
  16. 但是不检查workQueue是否为空也没错,因为两方面费的力气差不多,因此,只在必要的时候检查workQueue,就像第一次那样。

addWorkerFailed(Worker)

     private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w);
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}

runWorker(Worker w)

     final void runWorker(Worker w) {
Thread wt = Thread.currentThread(); // 当前线程
Runnable task = w.firstTask; // 首任务
w.firstTask = null; // 置位
w.unlock(); // 允许中断(interruptIdleWorkers()方法),因为即便不unlock(),也阻止不了interruptWorkers()方法中断此线程
boolean completedAbruptly = true; // 是否是突然完成,即异常情况
try {
while (task != null || (task = getTask()) != null) { // 如果首任务不为空,执行首任务;否则,从任务队列里取任务
w.lock(); // 加锁,防止中断(interruptIdleWorkers()方法)
// 如果线程池停止了,中断此线程,否则,复位可能的中断,若此线程中断过,需要再次检查线程池是否停止
// 有可能这边刚把中断复位,那边就把线程池停止了
// !第一次检查线程池是否停止,是因为,线程池停止了,应该直接中断此线程
// !第二次检查线程池是否停止,是因为,如果线程被中断了,刚好把它复位,很有可能是前一瞬间线程池停止导致的中断,所以要再次确认线程池的状态
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(); // 释放锁,允许中断(interruptIdleWorkers()方法)
}
}
completedAbruptly = false; // 平滑结束
} finally {
processWorkerExit(w, completedAbruptly); // 处理后续工作
}
}

getTask()

     private Runnable getTask() {
boolean timedOut = false; // 记录上次workQueue.poll是否超时 for (;;) { // 循环
int c = ctl.get(); // 得到ctl
int rs = runStateOf(c); // 得到runState // 等价于,if !(rs < SHUTDOWN || (rs < STOP && !workQueue.isEmpty()))
// 即是,if !(rs == RUNNING || (rs == SHUTDOWN && !workQueue.isEmpty()))
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount(); // workerCount - 1
return null; // 返回null
} int wc = workerCountOf(c); // 得到workerCount // 是否允许超时
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 1. workerCount > maximumPoolSize
// 2. timed为真,并且timeOut也为真,即上次已超时,而且,此刻workerCount大于1,或者workQueue为空
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // workerCount减1
return null;
continue; // 否则,重试
} try {
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); // 允许超时,调用poll()方法,否则take()方法
if (r != null)
return r; // 不为空,返回
timedOut = true; // 否则,继续
} catch (InterruptedException retry) {
timedOut = false; // 中断,清除超时标记
}
}

processWorkerExit(Worker, boolean)

     private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // 如果是异常结束,需要调整workCount(-1),因为正常结束的,会在getTask()方法里调用decrementWorkerCount()方法
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock; // 可重入锁,因为要对works操作
mainLock.lock(); // 加锁
try {
completedTaskCount += w.completedTasks; // 添加当前线程完成的任务数量加到总的完成任务数量
workers.remove(w); // 从线程池中移除该线程
} finally {
mainLock.unlock(); // 释放锁
} tryTerminate(); // 调用tryTerminate()方法,看是否满足结束条件 int c = ctl.get(); // 获得ctl
if (runStateLessThan(c, STOP)) { // 线程池没有停止
if (!completedAbruptly) { // 该线程平滑结束
int min = allowCoreThreadTimeOut ? 0 : corePoolSize; // 得出最小线程数
if (min == 0 && !workQueue.isEmpty()) // 最少保留1个
min = 1;
if (workerCountOf(c) >= min) // 如果不小于最小线程数,不必再添加
return;
}
addWorker(null, false); // 如果是异常结束,则说明任务队列里应该还有任务,那么直接添加新的线程替换它
}
}

shutdown()

     public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 获得锁
try {
checkShutdownAccess(); // 检查权限
advanceRunState(SHUTDOWN); // 设置状态
interruptIdleWorkers(); // 中断空闲线程
onShutdown(); // 钩子 ScheduledThreadPoolExecutor
} finally {
mainLock.unlock(); // 释放锁
}
tryTerminate(); // 尝试终止线程池
}

shutdownNow()

     public List<Runnable> shutdownNow() {
List<Runnable> tasks; // 存放未执行的任务
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 获得锁
try {
checkShutdownAccess(); // 检查权限
advanceRunState(STOP); // 设置状态
interruptWorkers(); // 中断线程
tasks = drainQueue(); // 拉取任务队列里的任务
} finally {
mainLock.unlock(); // 释放锁
}
tryTerminate(); // 尝试终止线程池
return tasks; // 返回任务列表
}

interruptIdleWorkers()

     private void interruptIdleWorkers() {
interruptIdleWorkers(false);
} private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 获得可重入锁
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) { // 线程没被中断,并且获得锁
try {
t.interrupt(); // 中断线程
} catch (SecurityException ignore) {
} finally {
w.unlock(); // 释放锁
}
}
if (onlyOne) // 如果仅仅中断一个,跳出循环
break;
}
} finally {
mainLock.unlock(); // 释放可重入锁
}
}

interruptWorkers()

     private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock; // 获得可重入锁
mainLock.lock(); // 加锁
try {
for (Worker w : workers)
w.interruptIfStarted(); // 只要线程已经启动,就中断它
} finally {
mainLock.unlock(); // 释放锁
}
}

tryTerminate()

     final void tryTerminate() {
for (;;) {
int c = ctl.get(); // 获得ctl
// 1. 线程池正在运行
// 2. 线程池已经结束或正在整理
// 3. 线程池已经SHUTDOWN,但是workQueue不为空
if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && !workQueue.isEmpty()))
return;
// workerCount不等于0,只中断一个空闲线程,保证中断传播下去
if (workerCountOf(c) != 0) {
interruptIdleWorkers(ONLY_ONE);
return;
} final ReentrantLock mainLock = this.mainLock; // 可重入锁
mainLock.lock(); // 加锁
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { // 设置状态为TIDYING
try {
terminated(); // 调用terminated()方法,钩子,用户实现
} finally {
ctl.set(ctlOf(TERMINATED, 0)); // 设置状态为TERMINATED
termination.signalAll(); // 通知信号
}
return;
}
} finally {
mainLock.unlock(); // 解锁
}
}
}

行文至此结束。

尊重他人的劳动,转载请注明出处:http://www.cnblogs.com/aniao/p/aniao_tpe.html

【JUC源码解析】ThreadPoolExecutor的更多相关文章

  1. 【JUC源码解析】ScheduledThreadPoolExecutor

    简介 它是一个线程池执行器(ThreadPoolExecutor),在给定的延迟(delay)后执行.在多线程或者对灵活性有要求的环境下,要优于java.util.Timer. 提交的任务在执行之前支 ...

  2. 【JUC源码解析】ForkJoinPool

    简介 ForkJoin 框架,另一种风格的线程池(相比于ThreadPoolExecutor),采用分治算法,工作密取策略,极大地提高了并行性.对于那种大任务分割小任务的场景(分治)尤其有用. 框架图 ...

  3. 【JUC源码解析】SynchronousQueue

    简介 SynchronousQueue是一种特殊的阻塞队列,该队列没有容量. [存数据线程]到达队列后,若发现没有[取数据线程]在此等待,则[存数据线程]便入队等待,直到有[取数据线程]来取数据,并释 ...

  4. 【JUC源码解析】DelayQueue

    简介 基于优先级队列,以过期时间作为排序的基准,剩余时间最少的元素排在队首.只有过期的元素才能出队,在此之前,线程等待. 源码解析 属性 private final transient Reentra ...

  5. 【JUC源码解析】CyclicBarrier

    简介 CyclicBarrier,一个同步器,允许多个线程相互等待,直到达到一个公共屏障点. 概述 CyclicBarrier支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后 ...

  6. 【JUC源码解析】ConcurrentLinkedQueue

    简介 ConcurrentLinkedQueue是一个基于链表结点的无界线程安全队列. 概述 队列顺序,为FIFO(first-in-first-out):队首元素,是当前排队时间最长的:队尾元素,当 ...

  7. 【JUC源码解析】Exchanger

    简介 Exchanger,并发工具类,用于线程间的数据交换. 使用 两个线程,两个缓冲区,一个线程往一个缓冲区里面填数据,另一个线程从另一个缓冲区里面取数据.当填数据的线程将缓冲区填满时,或者取数据的 ...

  8. Jdk1.6 JUC源码解析(12)-ArrayBlockingQueue

    功能简介: ArrayBlockingQueue是一种基于数组实现的有界的阻塞队列.队列中的元素遵循先入先出(FIFO)的规则.新元素插入到队列的尾部,从队列头部取出元素. 和普通队列有所不同,该队列 ...

  9. Jdk1.6 JUC源码解析(13)-LinkedBlockingQueue

    功能简介: LinkedBlockingQueue是一种基于单向链表实现的有界的(可选的,不指定默认int最大值)阻塞队列.队列中的元素遵循先入先出 (FIFO)的规则.新元素插入到队列的尾部,从队列 ...

  10. Jdk1.6 JUC源码解析(6)-locks-AbstractQueuedSynchronizer

    功能简介: AbstractQueuedSynchronizer(以下简称AQS)是Java并发包提供的一个同步基础机制,是并发包中实现Lock和其他同步机制(如:Semaphore.CountDow ...

随机推荐

  1. Odoo权限控制

    转载请注明原文地址:https://www.cnblogs.com/cnodoo/p/9278734.html 一:Odoo中的权限设置主要有以下5种 1)菜单.报表的访问权限 Odoo可以设置菜单项 ...

  2. 4、Android-数据存储方案(使用LitePal操作数据库)

    4.5.使用LitePal操作数据库 4.5.1.LitePal简介 LitePal是一款开源的Android数据库框架 采用了关系映射(ORM)的模式 将经常使用的一些数据库做了封装 是得不用编写S ...

  3. webpack4配置

    一.安装webpack 需要先在项目中npm init初始化一下,生成package.json 建议node版本安装到8.2以上 // webpack4中除了正常安装webpack之外,需要再单独安一 ...

  4. SpringBoot实战(九)之Validator

    表单验证,是最为常见的,今天演示的是利用hibernate-validtor进行校验,有的时候,虽然前端方面通过jQuery或者require.js校验框架进行校验,可以减轻服务器的压力和改善用户体验 ...

  5. [LuoguP1034][Noip2002] 矩形覆盖

    [LuoguP1034][Noip2002] 矩形覆盖(Link) 在平面上有\(N\)个点,\(N\)不超过五十, 要求将这\(N\)个点用\(K\)个矩形覆盖,\(k\)不超过\(4\),要求最小 ...

  6. 《移动App测试实战》读书笔记

    第一章 概述 什么是移动产品? 移动产品是一个可以在移动设备上安装的App,或者一个可以在移动设备上访问的定制页面. 1.1 研发流程 互联网产品的研发过程主要涉及以下职位分工. 产品经理:负责产品方 ...

  7. H.264的码率控制:CBR和VBR

    CBR: Constants Bits Rate, 静态比特率. 比特率在流的进行过程中基本保持恒定并且接近目标比特率,当对复杂内容编码时质量会下降. 在流式播放方案中使用CBR编码最为有效;优点是带 ...

  8. LeetCode31.下一个排列 JavaScript

    实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许使用额外常数空间. ...

  9. Java中CSS&JS篇基础笔记

    HTML就是由一组标签所组成的.HTML的字体标签: <font>标签: 属性:color,size,face HTML的排版标签: h标签:标题标签. p标签:段落标签. b标签:加粗标 ...

  10. Microsoft Visio / Project professional 2013 官方版本(下载)

    Microsoft Visio微软开发的一款软件, 它有助于 IT 和商务专业人员轻松地可视化.分析和交流复杂信息. 它能够将难以理解的复杂文本和表格转换为一目了然的 Visio 图表. 该软件通过创 ...