Java threadpool机制深入分析
简介
在前面的一篇文章里我对java threadpool的几种基本应用方法做了个总结。Java的线程池针对不同应用的场景,主要有固定长度类型、可变长度类型以及定时执行等几种。针对这几种类型的创建,java中有一个专门的Executors类提供了一系列的方法封装了具体的实现。这些功能和用途不一样的线程池主要依赖于ThreadPoolExecutor,ScheduledThreadPoolExecutor等几个类。如前面文章讨论所说,这些类和相关类的主要结构如下:
这里不是对所有类的详细实现做一个分析,而是从现有线程池ThreadPoolExecutor的源代码出发,分析一个线程池应该考虑的要点。从本文整体的方向来说,主要结合前面文章中提交线程给线程池之后分为返回结果和不返回结果的方式,按照他们执行的脉络来分析当我们提交一个线程到线程池之后他们内部是如何运行的。顺便也详细理解线程池这种参考实现的内部结构。
起始点
我们从最初使用多线程的代码开始,在一些示例代码里,我们通过Executors.newFixedThreadPool()等方法创建了一个ExecutorService类型的线程池。实际上具体实现对应的是ThreadPoolExecutor等。然后我们再使用这个对象的execute或者submit方法。前面我们了解到,通常我们用execute方法执行一个线程不通过这个方法本身返回执行结果或者我们不需要利用这个方法来获取结果。而submit方法是需要得到结果的。那么他们两者一个要结果,一个不要结果的是怎么统一起来的呢?如果我们看类AbstractExecutorService的如下代码则就可以理解了:
- /**
- * @throws RejectedExecutionException {@inheritDoc}
- * @throws NullPointerException {@inheritDoc}
- */
- public Future<?> submit(Runnable task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<Void> ftask = newTaskFor(task, null);
- execute(ftask);
- return ftask;
- }
- /**
- * @throws RejectedExecutionException {@inheritDoc}
- * @throws NullPointerException {@inheritDoc}
- */
- public <T> Future<T> submit(Runnable task, T result) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<T> ftask = newTaskFor(task, result);
- execute(ftask);
- return ftask;
- }
- /**
- * @throws RejectedExecutionException {@inheritDoc}
- * @throws NullPointerException {@inheritDoc}
- */
- public <T> Future<T> submit(Callable<T> task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<T> ftask = newTaskFor(task);
- execute(ftask);
- return ftask;
- }
前面这30多行代码没什么特别的,主要针对不同类型的方法签名。他们可以接收Runnable, Callable类型的参数。对于需要返回结果的类型,通过专门一个变量来保存结果。总体来说相当于一个简单的包装。而具体执行的代码还是要看execute方法。这里需要注意的一点就是newTaskFor(task)方法通过一个包装类将Callable变量包装成一个线程,让它可以运行。
既然前面的两种方法都归结于同一个方法execute,那么我们就来看看它的具体实现:
- 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.
- *
- * 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.
- *
- * 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.
- */
- 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);
- }
这是ThreadPoolExecutor里面的代码。前面这部分的代码看起来比较困难,而且也比较难懂。别急,我们先把代码放这里。在讨论这些详细实现的思路前,我们先看看几个要实现线程池需要考虑的点。
考虑的关键点
假定我们要实现一个线程池,那么有哪些地方是我们需要认真考虑的呢?从线程池本身的定义来看,它是将一组事先创建好的线程放在一个资源池里,当需要的时候就将该线程分配给具体的任务来执行。那么,这个池子该有多大呢?我们线程池肯定要面临多个线程资源的访问,是不是本身的结构要保证线程安全呢?还有,如果线程池创建好之后我们后续有若干任务使用了线程资源,当池里面的资源使用完之后我们该如何安排呢?是给线程池扩容,创建更多的线程资源,还是增加一个队列,让一些任务先在里面排队呢?在一些极端的情况下,比如说来的任务实在是太多了线程池处理不过来,对于这些任务该怎么处理呢?是丢弃还是通知给请求方?线程执行的时候会有碰到异常或者错误的情况,这些异常我们该怎么处理?怎么样保证这些异常的处理不会导致线程池其他任务的正常运行不出错呢?
总的来说,前面的这几个问题可以归结为一下几个方面:
1. 线程池的结构。
2. 线程池的任务分配策略。
3. 线程池的异常和错误处理。
下面,我们针对这几个问题结合源代码详细的分析一下。
源代码分析
线程数量和线程池状态
在ThreadPoolExecutor里面有一个AtomicInteger的数值,它用来表示两个信息,一个是当前线程池的状态,还有一个就是当前线程的数目。因为这两部分都是糅合到一个整型数字里头,所以他们的信息访问就比较紧凑和特殊一点:
- 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; }
这部分的代码看起来有点怪异,其实很好理解。我们设定的线程池里面最多可以容纳的线程数为(2^29) -1。这也就是为什么前面用一个Integer.SIZE - 3作为位数。这样这个整数的0-28位表示的就是线程的数目。而高位的部分,29-31位的地方则表示线程池的状态。这里定义的主要有5种状态,分别对应的值是从-1到3.
他们对应着线程的running, shutdown, stop, tidying, terminated这几个状态。
结构
除了前面的几个部分以外,线程池里还有如下几个成员:
- private final BlockingQueue<Runnable> workQueue;
- private final ReentrantLock mainLock = new ReentrantLock();
- /**
- * Set containing all worker threads in pool. Accessed only when
- * holding mainLock.
- */
- private final HashSet<Worker> workers = new HashSet<Worker>();
- /**
- * Wait condition to support awaitTermination
- */
- private final Condition termination = mainLock.newCondition();
- /**
- * Tracks largest attained pool size. Accessed only under
- * mainLock.
- */
- private int largestPoolSize;
- /**
- * Counter for completed tasks. Updated only on termination of
- * worker threads. Accessed only under mainLock.
- */
- private long completedTaskCount;
- private volatile ThreadFactory threadFactory;
- /**
- * Handler called when saturated or shutdown in execute.
- */
- private volatile RejectedExecutionHandler handler;
- /**
- * Timeout in nanoseconds for idle threads waiting for work.
- * Threads use this timeout when there are more than corePoolSize
- * present or if allowCoreThreadTimeOut. Otherwise they wait
- * forever for new work.
- */
- private volatile long keepAliveTime;
- /**
- * If false (default), core threads stay alive even when idle.
- * If true, core threads use keepAliveTime to time out waiting
- * for work.
- */
- private volatile boolean allowCoreThreadTimeOut;
- /**
- * Core pool size is the minimum number of workers to keep alive
- * (and not allow to time out etc) unless allowCoreThreadTimeOut
- * is set, in which case the minimum is zero.
- */
- private volatile int corePoolSize;
- /**
- * Maximum pool size. Note that the actual maximum is internally
- * bounded by CAPACITY.
- */
- private volatile int maximumPoolSize;
这些部分的内容看起来比较多,实际上他们几个都是在一些方法里经常用到的。
workQueue: 一个BlockingQueue<Runnable>队列,本身的结构可以保证访问的线程安全。相当于一个排队等待队列。当我们线程池里线程达到corePoolSize的时候,一些需要等待执行的线程就放在这个队列里等待。
workers: 一个HashSet<Worker>的集合。线程池里所有可以立即执行的线程都放在这个集合里。
mainLock: 一个访问workers所需要使用的锁。从前面的workQueue, workers这两个结构我们可以看到,如果我们要往线程池里面增加执行任务或者执行完毕一个任务,都要访问到这两个结构。所以大多数情况下为了保证线程安全,就需要使用mainLock这个锁。
corePoolSize: 处于活跃状态的最少worker数目。我们一个线程池里肯定事先创建好了若干个,等来执行任务的时候直接拿去就可以跑了。那么到底要保证最初有多少个呢?就由corePoolSize这个来指定了。
maximumPoolSize:线程池最大的长度。可以设置的一个参数。在我们当前池里面的线程数到达这个数字的时候就不能再往里面加了。需要注意的是这里是我们设定的一个池最大范围。在这里可以设定的最大数字是(2^29) -1。
其他还有几个牵涉到的成员比如说RejectedExecutionHandler等,相对都比较简单一点,代码里的注释就已经能够说清楚了。
ok,有了前面这几个基本成员的说明,我们再看看他们使用的work的结构。既然执行的都是一个Worker的集合。那么在这里Worker的定义是什么样的呢?下面是Worker的定义代码:
- private final class Worker
- extends AbstractQueuedSynchronizer
- implements Runnable
- {
- /**
- * This class will never be serialized, but we provide a
- * serialVersionUID to suppress a javac warning.
- */
- private static final long serialVersionUID = 6138294804551838833L;
- /** Thread this worker is running in. Null if factory fails. */
- final Thread thread;
- /** Initial task to run. Possibly null. */
- Runnable firstTask;
- /** Per-thread task counter */
- volatile long completedTasks;
- /**
- * Creates with given first task and thread from ThreadFactory.
- * @param firstTask the first task (null if none)
- */
- Worker(Runnable firstTask) {
- this.firstTask = firstTask;
- this.thread = getThreadFactory().newThread(this);
- }
- /** Delegates main run loop to outer runWorker */
- public void run() {
- runWorker(this);
- }
- // Lock methods
- //
- // The value 0 represents the unlocked state.
- // The value 1 represents the locked state.
- protected boolean isHeldExclusively() {
- return getState() == 1;
- }
- 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(); }
- }
这里我们可以看到Worker本身实现了Runnable接口,所以它可以当成一个线程来执行。然后也继承了AbstractQueuedSynchronizer,也可以实现一些对本身的线程同步访问。这里最重要的几个部分在于它里面定义了一个Thread thread和Runnable firstTask。看到这里,我们可能会比较奇怪,我们只是要一个可以执行的线程,这里放一个Thread和一个Runnable的变量做什么呢?在Worker的run方法里,调用的runWorker方法到底是怎么执行的呢?我们再来看看runWorker方法:
- final void runWorker(Worker w) {
- Runnable task = w.firstTask;
- w.firstTask = null;
- boolean completedAbruptly = true;
- try {
- while (task != null || (task = getTask()) != null) {
- w.lock();
- clearInterruptsForTaskRun();
- try {
- beforeExecute(w.thread, 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);
- }
- }
这部分代码看起来挺多的,里面的beforeExecute,afterExecute的方法在默认的实现里是空的。在一些有特定要求的地方可以通过继承ThreadPoolExecutor提供自定义的实现。和前面的定义结合起来看,看来Worker这里也没干什么别的,就是绕了个圈执行了里面设定的firstTask。
如果我们仔细看其中的代码,还有一个需要注意的地方就是这里用了一个while循环来执行task,而跳出循环的条件则是要task为null。那么这个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.
- if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
- decrementWorkerCount();
- return null;
- }
- boolean timed; // Are workers subject to culling?
- for (;;) {
- int wc = workerCountOf(c);
- timed = allowCoreThreadTimeOut || wc > corePoolSize;
- if (wc <= maximumPoolSize && ! (timedOut && timed))
- break;
- if (compareAndDecrementWorkerCount(c))
- return null;
- c = ctl.get(); // Re-read ctl
- if (runStateOf(c) != rs)
- continue retry;
- // else CAS failed due to workerCount change; retry inner loop
- }
- try {
- Runnable r = timed ?
- workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
- workQueue.take();
- if (r != null)
- return r;
- timedOut = true;
- } catch (InterruptedException retry) {
- timedOut = false;
- }
- }
- }
从代码的注释里可以看到,这个方法要从workQueue里面获取task然后提交执行。也就是说我们线程池执行完了当前的任务后会主动到这个队列里来取后续等待的任务执行。如果当前线程因为超时、线程池要关闭等状态影响则可能会退出,而如果一切都正常的话,则会从workQueue里面调用poll或take方法取到当前任务。前面一大堆的判断和循环就是判断当前线程池长度是否超过maximumPoolSize以及当前状态是否要关闭了。如果长度超了或者状态不对则没必要继续去取任务执行了,需要尽快返回。
有了前面这部分的分析,我们知道Worker只不过包含了一个指向我们需要创建的Runnable对象,然后在Worker作为线程执行的时候再来运行这个Runnable里面的线程执行部分。
线程池执行流程
有了前面那部分的铺垫,我们再来回过头看线程池拿到一个任务后execute方法的执行。我们将这些代码拆开来看,这是第一部分:
- int c = ctl.get();
- if (workerCountOf(c) < corePoolSize) {
- if (addWorker(command, true))
- return;
- c = ctl.get();
- }
这里获取到当前正在执行的线程数目,如果这些线程的数目少于corePoolSize,则将该线程加入到线程池中。然后返回。
另外一部分的代码如下:
- 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);
- }
这里是假定如果前面不能直接加入到线程池Worker集合里,则加入到workQueue队列等待执行。里面的if else判断语句则是检查当前线程池的状态。如果线程池本身的状态是要关闭并清理了,我们则不能提交线程进去了。这里我们就要reject他们。所以前面我们看到的一些线程池拒绝线程执行的机制在这里也得到了验证。
最后面这部分的代码如下:
- else if (!addWorker(command, false))
- reject(command);
这里对应代码注释里的第3种情况,我们前面做了两种尝试,一个是将线程加入到workers集合或者workerQueue队列排队。在这两种情况都失败的情况下,我们尝试加入一个新的线程。如果这种情况下我们也失败了,则拒绝线程提交执行。
这里几个地方都用到了addWorker方法,而我们既然是execute方法,肯定要让线程执行起来。可是这里没有见到那个地方调用线程的start方法。那么很可能这个线程方法的具体调用就在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;
- // else CAS failed due to workerCount change; retry inner loop
- }
- }
- Worker w = new Worker(firstTask);
- Thread t = w.thread;
- 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 c = ctl.get();
- int rs = runStateOf(c);
- if (t == null ||
- (rs >= SHUTDOWN &&
- ! (rs == SHUTDOWN &&
- firstTask == null))) {
- decrementWorkerCount();
- tryTerminate();
- return false;
- }
- workers.add(w);
- int s = workers.size();
- if (s > largestPoolSize)
- largestPoolSize = s;
- } finally {
- mainLock.unlock();
- }
- t.start();
- // It is possible (but unlikely) for a thread to have been
- // added to workers, but not yet started, during transition to
- // STOP, which could result in a rare missed interrupt,
- // because Thread.interrupt is not guaranteed to have any effect
- // on a non-yet-started Thread (see Thread#interrupt).
- if (runStateOf(ctl.get()) == STOP && ! t.isInterrupted())
- t.interrupt();
- return true;
- }
前面的嵌套for循环主要是用来判断当前线程池的状态是否可以允许继续加线程,同时也判断线程池的长度是否已经超标。当然,既然我们有一个线程加入了执行,当前运行的数量也要更新。如果没问题,则通过break retry;跳出这两个循环开始后面的正式执行。
在正式执行的时候我们创建一个Worker对象,并将mainLock加锁。保证后续执行部分是单线程执行的。在进入加锁的部分之后还需要再一次检查一下线程池的状态。这里我们将当前的线程加入到workers集合。然后我们通过t.start()方法正式执行线程。在这里一个线程才算是真正的执行起来了。
总结
前面我们看了一下线程池的执行机制。在默认的线程池实现里,它是通过一个workers集合来保持最核心活跃状态的线程组。当我们新加入线程执行任务时,则先利用这里的线程。如果这里的被占用满了之后则加入到workQueue这个队列里排队。这里面有一个重要的地方就是要经常检查当前线程池的状态,只有在运行状态的时候才可以往里面加线程,否则提交线程任务则会被拒绝。我们也要检查线程池的长度,防止提交的执行任务达到了我们设定的上限。为了保证线程的提交和执行安全,我们用一个lock来管理对线程集合workers和workerQueue的加锁控制。
在线程池中也有一些扩展点。比如在线程执行的过程中我们可以覆写beforeExecute, afterExecute方法来提供自己特定的功能。另外,当线程执行不符合条件要被丢弃或者拒绝的时候,我们也可以提供一些RejectExecutionHandler的具体实现。在系统的默认实现里已经提供了5种。
总的来说,对于一个线程池,它最核心的部分是对应一个线程运行集合和一个队列。如果我们能够保证好他们的状态、大小以及线程安全执行,那么基本上一个线程的雏形就差不多完成了。
Java threadpool机制深入分析的更多相关文章
- Linux select 机制深入分析
Linux select 机制深入分析 作为IO复用的实现方式.select是提高了抽象和batch处理的级别,不是传统方式那样堵塞在真正IO读写的系统调用上.而是堵塞在sele ...
- 深入理解Java流机制(一)
一.前言 C语言本身没有输入输出语句,而是调用"stdio.h"库中的输入输出函数来实现.同样,C++语言本身也没有输入输出,不过有别于C语言,C++有一个面向对象的I/O流类库& ...
- Java并发机制的底层实现原理之volatile应用,初学者误看!
volatile的介绍: Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现 ...
- 第28章 java反射机制
java反射机制 1.类加载机制 1.1.jvm和类 运行Java程序:java 带有main方法的类名 之后java会启动jvm,并加载字节码(字节码就是一个类在内存空间的状态) 当调用java命令 ...
- Java反射机制
Java反射机制 一:什么事反射机制 简单地说,就是程序运行时能够通过反射的到类的所有信息,只需要获得类名,方法名,属性名. 二:为什么要用反射: 静态编译:在编译时确定类型,绑定对象,即通过 ...
- java基础知识(十一)java反射机制(上)
java.lang.Class类详解 java Class类详解 一.class类 Class类是java语言定义的特定类的实现,在java中每个类都有一个相应的Class对象,以便java程序运行时 ...
- java基础知识(十一)java反射机制(下)
1.什么是反射机制? java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象都能够调用他的属性和方法,这种动态获取属性和方法的功能称为java的反射机制. ...
- JAVA 异常处理机制
主要讲述几点: 一.异常的简介 二.异常处理流程 三.运行时异常和非运行时异常 四.throws和throw关键字 一.异常简介 异常处理是在程序运行之中出现的情况,例如除数为零.异常类(Except ...
- java基础知识(四)java内存机制
Java内存管理:深入Java内存区域 上面的文章对于java的内存管理机制讲的非常细致,在这里我们只是为了便于后面内容的理解,对java内存机制做一个简单的梳理. 程序计数器:当前线程所执行的字节码 ...
随机推荐
- 获取一个请求的URL内容
using System.Net; 1. // 创建一个请求的URL. WebRequest request = WebRequest.Create("http://www ...
- <memory> is not a BOMStorage file
解决 Autoresizing 和AutoLayout 冲突 设置 self.autoresizingMask = UIViewAutoresizingNone;
- HBASE学习笔记--配置信息
hbase的配置信息,在hbase-site.xml里面有详细说明. 可以按照需要查询相关的配置. <?xml version="1.0"?> <?xml-sty ...
- jquery ajax 参数可以序列化
<form> <input type="text" name="FirstName" value="Bill" /> ...
- OC中另外的一个常用技术:通知(Notification)
OC中另外的一个常用技术:通知(Nofitication)其实这里的通知和之前说到的KVO功能很想,也是用于监听操作的,但是和KVO不同的是,KVO只用来监听属性值的变化,这个发送监听的操作是系统控制 ...
- 浅谈print2flash的在线预览转换应用(原创)
print2flash是一种在线预览转换工具,可以将doc.docx.xls.pdf.ppt等格式的文档转换成flash文件进行预览,因为之前使用的flash2paper只支持32为操作系统,不支持6 ...
- Git 系列(三):建立你的第一个 Git 仓库
现在是时候学习怎样创建你自己的 Git 仓库了,还有怎样增加文件和完成提交. 在本系列前面的文章中,你已经学习了怎样作为一个最终用户与 Git 进行交互:你就像一个漫无目的的流浪者一样偶然发现了一个开 ...
- placeholder颜色
::-moz-placeholder{color:#b9bfc1;} // Firefox::-webkit-input-placeholder{color:#b9bfc1;} // Chrome, ...
- avalon.js 多级下拉框实现
学习avalon.js的时候,有一个多级下拉框的例子,地址 戳这里 代码实现了联动, 但是逻辑上面理解有点难度,获取选择的值 和 页面初始化 功能存在问题. 在写地图编辑的时候,也用到了多级下拉框,特 ...
- hdu 5654 xiaoxin and his watermelon candy 莫队
题目链接 求给出的区间中有多少个三元组满足i+1=j=k-1 && a[i]<=a[j]<=a[k] 如果两个三元组的a[i], a[j], a[k]都相等, 那么这两个三 ...