线程池

ThreadPool源码学习
zodiac ·2020-12-10 ·20 次阅读

ThreadPoolExecutor
jdk1.5在juc包里提供了方便快捷的线程池api,并提供了基于工厂模式的Executors工具类用于快捷创建线程池,在实际开发过程中,需要使用线程池时,应当优先考虑使用Executors

1、类继承关系

Executor为顶级接口,其主要目标是将任务、任务的提交与任务的执行解耦

ExecutorService接口则定义了正常的线程池应该有的功能与行为,诸如任务提交,异步执行等等

ScheduledExecutorService接口则定义了一些定时的特性

2、Executor
Executor执行提交的任务(Runnable),该接口提供了一种将任务提交与任务执行(包括执行细节:线程、定时等)解耦的途径。

通过Executor包装的线程(Thread)对象,避免直接使用Thread对象来执行任务,可以有效将线程信息屏蔽,避免直接对线程的操作。

Executor本身并不强制要求执行的任务必须是异步执行

用于执行任务的执行器,而Runnable则表示可以用于执行的任务。

/**

  • 在未来某个时刻执行给定的指令,命令可以在新的线程中、线程池或调用线程中执行
  • 实际情况取决于Executor的实现

*/
void execute(Runnable command);
3、ExecutorService
能够管理任务终止、能够产生追踪一个或多个异步任务处理进度的Future的执行器(Executor)

接口的核心定义:

提交任务

Future submit(Callable task);
Future submit(Runnable task, T result);
Future<?> submit(Runnable task);
第一种提交Callable task类型的任务较好理解,任务完成时会将task的结果放至Future中。

第二种方式,Runnable task和 T result作为参数,实际上内部通过包装将入参result作为返回值与Runnable task一同包装为一个Callable,最后任务完成时将Callable结果放至Future中。因此通过这种方式,入参result,返回结果则是可能在任务中被修改的result。

AbstractExecutorService

public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, null);//生成一个RunnableFuture作为任务
execute(ftask);
return ftask;
}

//异步任务的抽象,内部封装了实际的任务、任务状态、真正的执行线程以及等待任务完成的线程等细节
//实际上是调用FutureTask.run()的线程被阻塞,作为真正的执行线程
protected RunnableFuture newTaskFor(Runnable runnable, T value) {
return new FutureTask(runnable, value);//创建FutureTask
}
FutureTask

public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);//将Runnable适配为Callable
this.state = NEW; // ensure visibility of callable
}
Tips:

FutureTask里有很多特性(比如对等待任务完成的线程进行阻塞,任务完成后对等待线程的唤醒,防止任务被并发调用等等)都可以使用AbstractQueuedSynchronizer,但在JDK1.8中的源码却没有发现AQS的痕迹,想想这是为何?

早期FutureTask确实是使用AQS实现,后续修改为了目前的样子(很多通过内存直接修改对象的操作,Unsafe类),核心是为了性能。这一部分可以再单独深入看看

Executors.RunnableAdapter

//简单的适配,将Runnable包装为Callable
static final class RunnableAdapter implements Callable {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
第三种方式,与第二种类似,会创一个result 为 null的Runnable适配器。

异步执行任务

通过任务提交、invokeAny、invokeAll

等待任一/全部任务执行完成

invokeAny()
一次性提交批量任务,有任一任务完成时返回该任务的处理结果,调用线程阻塞。

private T doInvokeAny(Collection<? extends Callable> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
if (tasks == null)
throw new NullPointerException();
int ntasks = tasks.size();
if (ntasks == 0)
throw new IllegalArgumentException();
ArrayList<Future> futures = new ArrayList<Future>(ntasks);
ExecutorCompletionService ecs =
new ExecutorCompletionService(this);//Wrapper或Decorator模式(组合模式??),增强了对已完成任务的管理能力

try {
    ExecutionException ee = null;
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Iterator<? extends Callable<T>> it = tasks.iterator();
    //1、提交第一个任务
    futures.add(ecs.submit(it.next()));
    --ntasks;
    int active = 1;
    for (; ; ) {
        //2、判断任务是否完成
        Future<T> f = ecs.poll();
        if (f == null) {
            //3、没有完成,且还有任务可以提交时,继续提交
            if (ntasks > 0) {
                --ntasks;
                futures.add(ecs.submit(it.next()));
                ++active;
            } else if (active == 0)//4、没有完成、没有任务可以提交、无处理中任务跳出
                break;
            else if (timed) {//5、无任务提交,任务在处理中时,设置等待任务完成
                f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                if (f == null)
                    throw new TimeoutException();
                nanos = deadline - System.nanoTime();
            } else//6、无限期阻塞,等待完成任务的队列有完成任务可以获得
                f = ecs.take();
        }
        if (f != null) {//7、获取到完成任务
            --active;
            try {
                return f.get();//8、返回完成任务的结果
            } catch (ExecutionException eex) {
                ee = eex;
            } catch (RuntimeException rex) {
                ee = new ExecutionException(rex);
            }
        }
    }
 
    if (ee == null)
        ee = new ExecutionException();
    throw ee;
} finally {//9、最终取消所有任务
    for (int i = 0, size = futures.size(); i < size; i++)
        futures.get(i).cancel(true);//FutureTask.cancel()只有当未NEW状态才会取消
}

}
invokeAll
如果无超时时间(较为简单)

则遍历FutureTask,通过FutureTask.get()方法阻塞调用线程即可。

如果存在超时时间

遍历FutureTask,每提交一次任务检查一次是否超时。任务提交完成后,遍历未结束Future,调用Future.get(timeout),最终返回结果,任务清理。

public List<Future> invokeAll(Collection<? extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
long nanos = unit.toNanos(timeout);
ArrayList<Future> futures = new ArrayList<Future>(tasks.size());
boolean done = false;
try {
for (Callable t : tasks)
futures.add(newTaskFor(t));

    final long deadline = System.nanoTime() + nanos;
    final int size = futures.size();
 
    // Interleave time checks and calls to execute in case
    // executor doesn't have any/much parallelism.
    for (int i = 0; i < size; i++) {
        execute((Runnable)futures.get(i));//执行任务,executor不保证异步执行
        nanos = deadline - System.nanoTime();//检测超时时间
        if (nanos <= 0L)
            return futures;//超时间内未提交的任务,不会再被执行
    }
 
    for (int i = 0; i < size; i++) {
        Future<T> f = futures.get(i);
        if (!f.isDone()) {//未完成future
            if (nanos <= 0L)//阻塞前先判断一次是否超时
                return futures;
            try {
                f.get(nanos, TimeUnit.NANOSECONDS);//超时时间内获取
            } catch (CancellationException ignore) {
            } catch (ExecutionException ignore) {
            } catch (TimeoutException toe) {
                return futures;
            }
            nanos = deadline - System.nanoTime();//更新超时时间
        }
    }
    done = true;
    return futures;
} finally {
    if (!done)
        for (int i = 0, size = futures.size(); i < size; i++)
            futures.get(i).cancel(true);//最终未完成的任务,不会再被执行
}

}
终止任务提交

shutdown()

已提交的任务,仍将被执行,但新的任务不再被接收。如果已经被shutdown,再次调用无影响

终止任务执行

List shutdownNow();

立即尝试停止所有正在执行的任务,返回等待执行的任务,不会等待正在执行的任务终结。

该方法会尝试尽最大努力终结执行中的任务,但无法保证正在执行的任务被终结,因此,如果有任务终结失败,该任务也许永远无法被终止。

等待执行器进入终止状态

boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

当调用了showdown()后,在超时间内、调用线程被中断前阻塞调用线程等待已提交任务完成。

4、AbstractExecutorService
通过模板模式的设计模式,对ExecutorService接口中定义的某些方法,进行了通用实现。

public Future<?> submit(Runnable task){…}
public Future submit(Runnable task, T result) {…}
public Future submit(Callable task) {…}

public T invokeAny(Collection<? extends Callable> tasks) throws InterruptedException, ExecutionException {…}
public List<Future> invokeAll(Collection<? extends Callable> tasks) throws InterruptedException {…}
submit(…):

主要逻辑:

1、包装Callable、Runnable、result为RunnableFuture(实际默认实现是FutureTask,适配Runable接口)

2、调用实现类execute()方法执行RunnableFuture

invokeAny(…)

invokeAll(…)

上述方法的具体源码可以参见ExecutorService部分

5、ThreadPoolExecutor
虽然AbstractExecutorService进行了一些通用实现,但诸如execute()、shutdown()等依赖实际执行路径与执行器内部状态的方法并未被实现,因此这些方法的实现逻辑将是对Executor进行区分的重要因素。

ThreadPoolExecutor直译为线程池执行器,该类通过维护内线程池,最大程度复用线程,减少线程创建、销毁与维护的开销,提高任务的执行效率。通常会作为系统的一个异步处理模块出现,最大程度降低系统对硬件资源的占用。同时也提供了友善的api,通过对线程池核心参数的设计,可以设计出不同类型的线程池,扩展线程池的可应用场景。

基础概念
包含线程控制状态、作业队列、工人(worker)、工人集合(worker set)、工人数量(worker count)、终止条件、线程工厂、任务拒绝处理器、核心池大小、最大池大小等等

控制状态、工人数量

控制状态:

标记了执行器的生命周期,记录线程池的当前状态,分为RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED,主要是为了实现shutdown()等涉及到执行器状态的方法,如果不存在控制状态,则无法实现类似:拒绝新任务的添加、终结执行器等功能。

阶段 说明 状态转换
RUNING 任务执行中,此时可以接收新的任务 SHUTDOWN:调用shutdown()
STOP:调用shutdownNow()
SHUTDOWN 任务执行中,但不再接收新的任务 STOP:调用shutdownNow()
TIDYING:任务完成、线程池清空
STOP 执行中的任务将被终止,且不再接收新的任务,不再继续处理任务 TIDYING:线程池清空
TIDYING 整理阶段,主要是回收资源与收尾工作。所有的任务已经结束,工人数量为0(线程已经完成回收),在该阶段对应线程将调用钩子函数terminated()告知子类即将要终结 TERMINATED:terminated()执行后
TERMINATED terminated()完成
ThreadPoolExecutor实现中将线程池状态与工人数量整合到一个Integer内,为了保证并发安全,Integer使用AtomicInteger。控制状态一共5种,在设计中通过保证各个状态在状态空间中的有序,直接使用数值的方式判断当前状态。

工人数量(workerCount):

记录当前线程池中的有效线程数量,主要用于状态变更、判断是否需要新增线程、线程数量是否已达设定值等。

状态字段:

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;//0001111111111…

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;//RUNING状态下,ctl<0
private static final int SHUTDOWN = 0 << COUNT_BITS;//SHUTDOWN状态下,当workercount=0时,ctl=0
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; }//低位即wokerCount
private static int ctlOf(int rs, int wc) { return rs | wc; }
作业队列(workQueue)-BlockingQueue

用于保存待处理任务并将任务移交给工作线程。声明类型为阻塞队列(BlockingQueue)

不要求poll()返回null时代表队列为空(isEmpty),在决定是否进行线程池状态转移时(比如由SHUTDOWN转移为TIDYING需要判断任务队列是否为空)使用isEmpty判断队列是否为空。这种设计就可以让workQueue使用一些特殊设计的队列,比如延迟队列(DelayQueue,即便poll()为null,但延迟一段时间后可以返回non-null,即无法通过poll()是否为null判断队列是否为空),ThreadPoolExecutor通过isEmpty而非poll() == null 的方式判断队列是否为空,能够对工作队列的实现类型更加包容。

ThreadPoolExecutor并没有为BlokingQueue提供默认实现(声明时没有指定实例,且所有的构造方法没有为其提供默认实现),但使用Executors创建ThreadPoolExecutors时默认会使用LinkedBlockedQueue作为默认实现。

关于阻塞队列(BlockingQueue)

合适的阻塞队列,当队列空时会阻塞消费者,队列满时阻塞生产者。

如果不适用阻塞队列,可以使用线程安全队列+标志锁实现~(但自己实现的方式可能会存在很多细节问题,有空可以把这个细节深入研究一下)

工人(worker)

保存了所有在线程池中的工作线程的集合

private final HashSet workers = new HashSet();
该声明并未使用线程安全的Set类,而是使用了最为简单的HashSet,因此其内部要求,任何对该集合的访问都需要获取private final ReentrantLock mainLock = new ReentrantLock();的锁权限。

Worker类属于ThreadPoolExecutor的私有内部类,因此只有ThreadPoolExecutor能够创建该类的实例,作为内部类,该类的实例能够直接访问ThreadPoolExecutor的属性与方法。

Woker源码:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
//通过ThreadPoolExecutor内的ThreadFactory生成的执行线程
//woker持有该thread,主要是为了保证可以获取到是哪个线程是该woker的运行线程
final Thread thread;
//该woker的第一个任务,可能为null
Runnable firstTask;
//该woker完成的任务总量
volatile long completedTasks;

//AQS中state标志位,0:无锁状态,1:被持有锁状态,>=0:可被中断状态
Worker(Runnable firstTask) {
    setState(-1); // 禁止中断该worker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);//worker也是runnable,将worker作为其执行线程的target,当其执行线程start()时,JVM会调用worker.run()
}
 
//该方法表明woker是个Runnable,当其执行线程start时,会调用该方法
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();
}
 
//仅当woker线程运行时,允许中断
void interruptIfStarted() {
    Thread t;
    //getState() >= 0 通过判断state是否大于0,初始情况下state=-1
    //当state < 0,即state = -1时,其执行线程尚未start,因此无需中断其执行线程
    //并不要求获取了woker的锁
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
        try {
            t.interrupt();//中断执行线程
        } catch (SecurityException ignore) {
        }
    }
}

}
ThreadPoolExecutor.runWoker()

final void runWorker(ThreadPoolExecutor.Worker w) {
//runWoker()方法只会被Woker.run()调用,而Woker.run()只会在其执行线程start后由jvm调用,因此runWoker()必定只会被对应Worker的执行线程调用
Thread wt = Thread.currentThread();//所以这里当前线程就是w.thread,也就是woker的执行线程
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();//此时woker的执行线程已经启动,允许外部进行中断,因此将woker的state置为0
boolean completedAbruptly = true;
try {
//getTask()重点,内部其实是从任务队列里获取任务,包含了线程阻塞、挂起、淘汰等一系列逻辑
//正常情况下就是该线程不断从任务队列里取任务
while (task != null || (task = getTask()) != null) {
//这里的这个lock很有意思
//如果仅仅是执行线程执行任务的话,其实执行线程获取woker锁并无太大意义,一方面是因为该方法(runWoker(…))只会运行在执行线程中,不可能有并发情况,另一方面woker本身没有可以共享的资源(这个地方可以再考虑一下:获取的任务、本身执行的线程是否算可共享资源呢),没必要获取这个锁,但这个地方有个隐藏逻辑,就是一旦woker在执行任务,则woker必定是被其执行线程锁住的,因此通过woker锁的状态可以判断woker是否在执行任务。当外部线程想要让worker正常执行完任务,然后再停止woker时,就必须获取该woker的锁,如此就能够保证woker当前正在执行的任务被正常完成
w.lock();
//简短的代码,逻辑复杂,具体分析参见“Worker检测ThreadPoolExecutor是否Stoping及线程状态机制”
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();//释放锁,表明当前worker已经完成某个任务的执行
}
}
completedAbruptly = false;//表明线程正常执行完任务,如果为true,则说明执行形成可能中途被中断,或是用户任务发生了异常
} finally {
/* *处理将要结束的worker的收尾工作。
* 1、将当前worker从workerSet中移除
* 2、尝试将线程池过度为Terminated状态
* 3、在线程池仍然需要执行任务的状态下(RUNING、SHUTDOWN),判断是否需要添加新的worker至线程池,添加条件为:1)当前worker为突然终止;2)当前线程池的线程数量小于最小需求线程数量
* 4、走到这行代码一般有两种场景:1)无任务可做,且不会有新任务来了;2)用户任务执行期间发生了异常
*/
processWorkerExit(w, completedAbruptly);
}
}
Worker检测ThreadPoolExecutor是否Stoping及线程状态机制

1、如果ThreadPool处于Stop、Tidying或Terminated,且当前线程未被中断,则中断当前线程;2、如果处理Runing、Shutdown状态,则清除当前中断标志位,返回之前的中断标志,如果之前是中断的,且ThreadPool状态变为了Stop、Tidying或Terminated,则再次中断执行线程;3、这段逻辑保证了:如果当前ThreadPoolExecutor处于RUNING、SHUTDOWN,则中断标志位被清除;否则,直接中断执行线程。4、进一步思考:如果不这样实现会有什么问题?

下图为实际的判断逻辑,如果没有红框部分逻辑,则之前的清除中断标志位可能会导致在箭头处插入的ShutdownNow事件被忽略。

处理woker结束的工作

private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn’t adjusted
//正常结束不需要再去减少workerCount,原因是在getTask()的地方已经预先减过了
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
    completedTaskCount += w.completedTasks;
    workers.remove(w);
} finally {
    mainLock.unlock();
}
 
tryTerminate();
 
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
    if (!completedAbruptly) {
        int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
        if (min == 0 && ! workQueue.isEmpty())
            min = 1;
        if (workerCountOf(c) >= min)
            return; // replacement not needed
    }
    addWorker(null, false);
}

}
线程池里的线程创建与销毁

失效场景:1、空闲worker(当前worker数量超过corePoolSize,且线程keepAliveTime时间内未获取到任务,这也是线程池控制worker超时失效的机制);2、用户程序异常;3、任务完全处理完成

线程池在处理失效线程时,如果仍然需要增加线程,那么会再次通过ThreadFactory创建一个新的线程。

假设如下场景:线程池持续接收新任务,如果新的任务正常执行,那么执行该任务的线程还会继续服役,处理后续的新任务,但是如果新的任务无法正常执行,抛出了异常,那么该线程将由于该异常而而终结,线程池会创建新的线程继续处理后续任务。所以当所有的新任务都抛出异常时,可能会导致线程池单个线程的寿命极短,频繁创建线程,事实上导致线程池失效。

实际上呢?

通过submit()提交给线程池的RunnableTask都被包装为FutureTask了,而FutureTask在run()的时候,对异常进行包装,将outCome输出为Exception,也就是说正常的用户异常根本不会抛出到Worker的runWoker loop里…

但是如果直接调用ThreadPoolExecutor.execute()方法,则不会使用FutureTask包装该Runnable了,包装的过程都是在AbstractExecutorService里完成的,直接通过execute执行任务的话,则会产生之前所述的场景,线程将频繁创建

ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(5, new NamedThreadFactory());
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.execute(() -> {//使用Executor.execute()执行,则会出现频繁创建线程池的情况,使用submit()则不会,但其实execute与submit都是异步执行
throw new RuntimeException();
});
}
6、总结
线程池的核心其实是一个基于BlockingQueue的任务生产消费模型,任务的生产方为ThreadPool的使用方,通过submit与execute生产Runnable任务,任务的消费方则为ThreadPool内部的Worker。TheadPool做了很多关于线程池特性的控制:比如核心线程池大小、最大线程池大小、空闲线程的最大存活时间等等,主要就是通过Worker从BlockingQueue中获取任务的状态来控制Worker的销毁。

源码十分精巧,部分代码写的相当简练,但包含了很深的关于状态定义、任务生产、任务消费、超时控制、同步控制、空闲线程销毁、新增线程等一系列特性的实现逻辑,每一步的动作都值得进一步思考。

如果让自己去实现一遍这样的线程池,该如何去实现呢?

最后附上向ThreadPoolExecutor submit任务的执行流程图~

l

问答系统总结2

1.开发工具GIt,idea

  • 我是把代码放到github上的,以便于代码版本管理,不能拷贝来拷贝去
  • java语言/规范最基础,要研究jvm,垃圾回收算法思路,根据什么样的规则,怎么回收申请的一些对象;参数可以配置,怎么配置,对应的后台运行又是怎么样的

    2.Spring Boot,Velocity

  • spring框架:spring是怎么做的,view、controller、service是怎么连在一起的:spring是控制反转、依赖注入,解决了数据数据初始化一些问题,能够吧代码写得很简洁,然后脱颖而出,核心是怎么做的,数据初始化是怎么做的。面向切面的编程可以用在什么地方,spring的一般框架是mvc
    :层是controller、中间层是service、底层是dao。为什么这么分?他比其他的区别是什么?对一些request包装了
    找spring一两点深入研究:我不仅仅是用了spring,我还研究了他某一个组件,他是怎么实现的?讲讲(闪光点)
  • velocity(模板语言)
    我用了velocity,我发现了velocity里的很多东西和我学的java、c++都是一样的,他这个框架他把一些公用的都提取出来,前面的spring boot是个java框架,为什么要用velocity,因为我要前后端分离,view和后面的数据要分离,data是怎么传过来的,怎么解析的,他支持什么东西?(不仅仅用了这个东西,还有思想,解耦)

    3.mybatis

  • 怎么把数据库的一些前后的读取给做了,怎么把xml里的多重条件,怎么做文法的解析,然后把这些条件给处理掉。

    4.登录/注册

  • 网站安全(salt):密码为什么加了一个salt就变得安全?
  • 通过拦截器来实现的:拦截器的思想、框架,留好接口;拦截器实现登录注册:我在cookie里放了一个token,token怎么处理用户登录注册的:在用户登录注册的时候,会下发一个token,把token与用户信息关联起来,关联起来之后我为了优化token信息,把token放到数据库里(redis),设计一个分布式的统一登录系统,现在的互联网产品都是统一登录的,比如,登录qq之后,登录网页就不需要登录了,qq登录过的token直接注入到网页上去。这是个ssension共享问题:、
  • 保证数据安全:验证(邮件激活)

    5.前缀树

  • 构造一个前缀树,通过一个有限状态机来实现一传文本是不是包含敏感词,繁杂度是多少 很重要:优点有哪些?为什么不用kmp,文本查找算法:
    可以很快的加一些词汇过来;有扩展性,以及性能更提高

    6.redis

  • 数据结构:跳列表,哈希,优先队列,list:我了解redis底层是怎么实现的,为什么他的效率很高,他的字符串是怎么保存的,做这个工程的时候我用在的异步队列上、排序上、异步框架

    7.异步框架

*思路:我这个网站附带的每一步操作可能附带的操作都非常多,为了更快的吧结果返回给用户,所以采用异步框架,自己写的,数据结构:使用redis的队列,因为redis能够保证线程同步;除了用队列,我还想过用有优先队列,这样我的异步框架能够把紧急的任务线处理掉。我这个异步框架:有消息的发射,消息的处理,事件的模型定义以及具体执行的eventhandle,我定义了一些公共接口把这些实现了。

8.邮件(smtp协议)

  • 做了一个简单邮件,怎么连接上服务器,我当时做这个的时候,ssl问题
    ,ssl理解,服务器需要ssl链接,为了安全服务器是怎么做的;java sdk 1.7 1.8的问题,1.8是需要换一个jar包的
  • 豆瓣电影排序:好的问题能挑选出来,互动越多,时间越新,评分越高。

    9.timeline(时间轴)

  • 肯定会问:为什么用推拉模式,用推实时性高能让好友快速得到消息,用拉能节省僵尸号、不是活跃用户的存储空间。怎么区分?最后把timeline组合起来‘timeline模板系统,每个新鲜事展向不一样,和velocity结合起来,后台存储的都是核心数据,每个数据对应的是一个模板,我把模板结合起来,我就能快速的把时间轴展示出来。

    10.爬虫

    11.solr搜索

  • 搜索去重:对比相似度,敏感哈希算法,哈希算法:两个字符串稍微有一点点不一样,结构就是不一样的。可能头尾是不一样的,内容一样:采用敏感哈希算法把相似度求出来,区别:敏感哈希算法两个文档相似度很高,他生成的哈希值的比例是很相似的。

    12.单元测试/部署

  • 部署:运维,llinux nigix反向代理,与正向对比。负载均衡:为什么要负载均衡。
l

问答系统总结

1.开发工具GIt,idea

  • 我是把代码放到github上的,以便于代码版本管理,不能拷贝来拷贝去
  • java语言/规范最基础,要研究jvm,垃圾回收算法思路,根据什么样的规则,怎么回收申请的一些对象;参数可以配置,怎么配置,对应的后台运行又是怎么样的

    2.Spring Boot,Velocity

  • spring框架:spring是怎么做的,view、controller、service是怎么连在一起的:spring是控制反转、依赖注入,解决了数据数据初始化一些问题,能够吧代码写得很简洁,然后脱颖而出,核心是怎么做的,数据初始化是怎么做的。面向切面的编程可以用在什么地方,spring的一般框架是mvc
    :层是controller、中间层是service、底层是dao。为什么这么分?他比其他的区别是什么?对一些request包装了
    找spring一两点深入研究:我不仅仅是用了spring,我还研究了他某一个组件,他是怎么实现的?讲讲(闪光点)

  • velocity(模板语言)
    我用了velocity,我发现了velocity里的很多东西和我学的java、c++都是一样的,他这个框架他把一些公用的都提取出来,前面的spring boot是个java框架,为什么要用velocity,因为我要前后端分离,view和后面的数据要分离,data是怎么传过来的,怎么解析的,他支持什么东西?(不仅仅用了这个东西,还有思想,解耦)

    3.mybatis

  • 怎么把数据库的一些前后的读取给做了,怎么把xml里的多重条件,怎么做文法的解析,然后把这些条件给处理掉。

    4.登录/注册

  • 网站安全(salt):密码为什么加了一个salt就变得安全?

  • 通过拦截器来实现的:拦截器的思想、框架,留好接口;拦截器实现登录注册:我在cookie里放了一个token,token怎么处理用户登录注册的:在用户登录注册的时候,会下发一个token,把token与用户信息关联起来,关联起来之后我为了优化token信息,把token放到数据库里(redis),设计一个分布式的统一登录系统,现在的互联网产品都是统一登录的,比如,登录qq之后,登录网页就不需要登录了,qq登录过的token直接注入到网页上去。这是个ssension共享问题:、

  • 保证数据安全:验证(邮件激活)

    5.前缀树

  • 构造一个前缀树,通过一个有限状态机来实现一传文本是不是包含敏感词,繁杂度是多少 很重要:优点有哪些?为什么不用kmp,文本查找算法:
    可以很快的加一些词汇过来;有扩展性,以及性能更提高

    6.redis

  • 数据结构:跳列表,哈希,优先队列,list:我了解redis底层是怎么实现的,为什么他的效率很高,他的字符串是怎么保存的,做这个工程的时候我用在的异步队列上、排序上、异步框架

    7.异步框架

  • 思路:我这个网站附带的每一步操作可能附带的操作都非常多,为了更快的吧结果返回给用户,所以采用异步框架,自己写的,数据结构:使用redis的队列,因为redis能够保证线程同步;除了用队列,我还想过用有优先队列,这样我的异步框架能够把紧急的任务线处理掉。我这个异步框架:有消息的发射,消息的处理,事件的模型定义以及具体执行的eventhandle,我定义了一些公共接口把这些实现了。

    8.邮件(smtp协议)

  • 做了一个简单邮件,怎么连接上服务器,我当时做这个的时候,ssl问题
    ,ssl理解,服务器需要ssl链接,为了安全服务器是怎么做的;java sdk 1.7 1.8的问题,1.8是需要换一个jar包的

  • 豆瓣电影排序:好的问题能挑选出来,互动越多,时间越新,评分越高。

    9.timeline(时间轴)

  • 肯定会问:为什么用推拉模式,用推实时性高能让好友快速得到消息,用拉能节省僵尸号、不是活跃用户的存储空间。怎么区分?最后把timeline组合起来‘timeline模板系统,每个新鲜事展向不一样,和velocity结合起来,后台存储的都是核心数据,每个数据对应的是一个模板,我把模板结合起来,我就能快速的把时间轴展示出来。

    10.爬虫

    11.solr搜索

  • 搜索去重:对比相似度,敏感哈希算法,哈希算法:两个字符串稍微有一点点不一样,结构就是不一样的。可能头尾是不一样的,内容一样:采用敏感哈希算法把相似度求出来,区别:敏感哈希算法两个文档相似度很高,他生成的哈希值的比例是很相似的。

    12.单元测试/部署

  • 部署:运维,llinux nigix反向代理,与正向对比。负载均衡:为什么要负载均衡。

l

[toc]

无人机系统组成

航电系统

任务载荷系统

地面系统

综合保障系统

试验实施

照相侦察试验

试验目的

试验条件和要求

试验内容与方法步骤

试验记录

电视及红外侦察系统试验

通信对抗侦查试验

通信对抗干扰试验

通信中继试验

合成孔径雷达侦察试验

航空辐射侦察系统试验

隐身性能试验

操作使用性能试验

环境适应试验

安全性试验

试验测试(2)

飞行新年试验

动力装置飞行试验

无人机发射回收(起飞/着陆)性能试验

飞行控制系统飞行试验

导航系统飞行试验

机载电子电器设备性能飞行试验

无线电测控与信息传输系统性能飞行试验

地面控制站飞行操作使用性能试验

机载侦察任务设备性能飞行试验

机载通信对抗系统飞行试验

机载通信中继系统性能飞行试验

机载合成孔径雷达侦察飞行试验

航空辐射侦察系统飞行试验

隐身性能飞行试验

l

git reset放弃修改&放弃增加文件

1. 本地修改了一堆文件(并没有使用git add到暂存区),想放弃修改。
单个文件/文件夹:

1
$ git checkout -- filename

所有文件/文件夹:

1
$ git checkout .

2. 本地新增了一堆文件(并没有git add到暂存区),想放弃修改。
单个文件/文件夹:

1
$ rm filename / rm dir -rf

所有文件/文件夹:

1
$ git clean -xdf

// 删除新增的文件,如果文件已经已经git add到暂存区,并不会删除!

3. 本地修改/新增了一堆文件,已经git add到暂存区,想放弃修改。
单个文件/文件夹:

1
$ git reset HEAD filename

所有文件/文件夹:

1
$ git reset HEAD .

4. 本地通过git add & git commit 之后,想要撤销此次commit

1
$ git reset commit_id

这个id是你想要回到的那个节点,可以通过git log查看,可以只选前6位
// 撤销之后,你所做的已经commit的修改还在工作区!

1
$ git reset --hard commit_id

这个id是你想要回到的那个节点,可以通过git log查看,可以只选前6位
// 撤销之后,你所做的已经commit的修改将会清除,仍在工作区/暂存区的代码不会清除!

l

git

git指令

一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

workspace:工作区

Index/Stage:暂存区

Repository:仓库区(或本地仓库)

Remote:远程仓库

  • 述任务及修改内容,填写要发布的前后端应用和提测、上线时间,以及研发负责人、研发人员、测试负责人。
  1. 将测试报告(已解密)通过ftp上传

    • 将上传报告名加到app-sz-artemis中的ReportRecognitionServiceTest类
    • (注)报告提前解密,保证准时完成上线。上传报告名必须以模板名开头:例,xikang1.田淑梅.pdf
  2. 提交项目到dev与release分支

    • 首先commit&push sz-artemis-common 项目(因为sz-artemis-report-parser、sz-artemis-report-api、app-sz-artemis均依赖于它),然后c&p sz-artimis-report-parser,最后c&p app-sz-artemis。
    • (注)先push到dev分支,再push到release分支,release分支严格遵守格式:例,release-2019-09-05
  3. 通过Rocket.chat编译

    • 使用Rocket.chat编译,严格按照步骤3里的顺序,并注意指令正确。每编译完一个项目到http://118.178.137.55:8089/job下查看console output,是否编译成功。
    • (注)指令格式:@Gerty b sz-artemis-common;@Gerty b sz-artemis-report-parser;@Gerty b sz-artemis-api;@Gerty b test_sz-artemis
  4. 自测

    • 通过识别回归测试工具,进行自测,查看测试通过的状况以及与以往测试用例集相异的excel结果
    • 若excel结果无差异,或差异在可接受范围以内,则测试通过,并将本次测试产生的测试用例覆盖历史测试用例。
    • 测试通过则可上线。
  5. 发jira链接

    • 链接发顾璟琛审核,之后发方雷部署。

 

l

github 上传大文件100MB姿势

具体就是安装git-lfs,先下载,然后就是一顿操作:

  1. 先在web建立一个空仓库

  2. 然后建立跟仓库名一样的文件夹,并执行初始化命令:git init

  3. 然后执行git lfs install

  4. 然后添加你要上传的文件名或后缀名:git lfs track '*.zip'

  5. 然后就把生成的 .gitattributes文件,先传到远程仓库

    • git add .gitattributes
  • git commit -m 'large - init file'
    • git push -u origin master:master # 第一次要这样执行,后面再传就git push就行。
  1. 然后就可以正常添加上传大文件了!

    • git add bigfile.zip
    • git commit -m 'upload Big file.'
    • git push # 第一次要这样执行,后面再传就git push就行。

  • 删除远程仓库文件,但本地文件不删除,如

    1
    bigfile.zip
    • git rm bigfile.zip
    • git commit -m 'rm bigfile.zip'
    • git push
l

git指令

个人开发

一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

workspace:工作区

Index/Stage:暂存区

Repository:仓库区(或本地仓库)

Remote:远程仓库

  1. 新建代码库

    1
    2
    # 下载一个项目和它的整个代码历史
    $ git clone [url]
  2. 配置

    Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 显示当前的Git配置
    $ git config --list

    # 编辑Git配置文件
    $ git config -e [--global]

    # 设置提交代码时的用户信息
    $ git config [--global] user.name "[name]"
    $ git config [--global] user.email "[email address]"
  3. 增加/删除文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 添加指定文件到暂存区
    $ git add [file1] [file2] ...

    # 添加指定目录到暂存区,包括子目录
    $ git add [dir]

    # 添加当前目录的所有文件到暂存区
    $ git add .

    # 添加每个变化前,都会要求确认
    # 对于同一个文件的多处变化,可以实现分次提交
    $ git add -p

    # 删除工作区文件,并且将这次删除放入暂存区
    $ git rm [file1] [file2] ...
  4. 代码提交

    1
    2
    3
    4
    5
    6
    7
    8
    # 提交暂存区到仓库区
    $ git commit -m [message]

    # 提交暂存区的指定文件到仓库区
    $ git commit [file1] [file2] ... -m [message]

    # 提交时显示所有diff信息
    $ git commit -v
  5. 分支

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    # 列出所有本地分支
    $ git branch

    # 列出所有远程分支
    $ git branch -r

    # 列出所有本地分支和远程分支
    $ git branch -a

    # 新建一个分支,但依然停留在当前分支
    $ git branch [branch-name]

    # 新建一个分支,并切换到该分支
    $ git checkout -b [branch]

    # 新建一个分支,指向指定commit
    $ git branch [branch] [commit]

    # 新建一个分支,与指定的远程分支建立追踪关系
    $ git branch --track [branch] [remote-branch]

    # 切换到指定分支,并更新工作区
    $ git checkout [branch-name]

    # 切换到上一个分支
    $ git checkout -

    # 建立追踪关系,在现有分支与指定的远程分支之间
    $ git branch --set-upstream [branch] [remote-branch]

    # 合并指定分支到当前分支
    $ git merge [branch]

    # 删除分支
    $ git branch -d [branch-name]

    # 删除远程分支
    $ git push origin --delete [branch-name]
    $ git branch -dr [remote/branch]
  6. 查看信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    # 显示有变更的文件
    $ git status

    # 显示当前分支的版本历史
    $ git log

    # 显示commit历史,以及每次commit发生变更的文件
    $ git log --stat

    # 搜索提交历史,根据关键词
    $ git log -S [keyword]

    # 显示某个文件的版本历史,包括文件改名
    $ git log --follow [file]
    $ git whatchanged [file]

    # 显示指定文件相关的每一次diff
    $ git log -p [file]

    # 显示过去5次提交
    $ git log -5 --pretty --oneline

    # 显示所有提交过的用户,按提交次数排序
    $ git shortlog -sn

    # 显示指定文件是什么人在什么时间修改过
    $ git blame [file]

    # 显示暂存区和工作区的差异
    $ git diff

    # 显示暂存区和上一个commit的差异
    $ git diff --cached [file]

    # 显示工作区与当前分支最新commit之间的差异
    $ git diff HEAD

    # 显示两次提交之间的差异
    $ git diff [first-branch]...[second-branch]

    # 显示某次提交的元数据和内容变化
    $ git show [commit]

    # 显示某次提交发生变化的文件
    $ git show --name-only [commit]

    # 显示某次提交时,某个文件的内容
    $ git show [commit]:[filename]

    # 显示当前分支的最近几次提交
    $ git reflog
  7. 远程同步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 下载远程仓库的所有变动
    $ git fetch [remote]

    # 显示所有远程仓库
    $ git remote -v

    # 显示某个远程仓库的信息
    $ git remote show [remote]

    # 增加一个新的远程仓库,并命名
    $ git remote add [shortname] [url]

    # 取回远程仓库的变化,并与本地分支合并
    $ git pull [remote] [branch]

    # 上传本地指定分支到远程仓库
    $ git push [remote] [branch]

    # 推送所有分支到远程仓库
    $ git push [remote] --all
  8. 撤销

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    # 恢复暂存区的指定文件到工作区
    $ git checkout [file]

    # 恢复某个commit的指定文件到暂存区和工作区
    $ git checkout [commit] [file]

    # 恢复暂存区的所有文件到工作区
    $ git checkout .

    # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
    $ git reset [file]

    # 重置暂存区与工作区,与上一次commit保持一致
    $ git reset --hard

    # 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
    $ git reset [commit]

    # 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
    $ git reset --hard [commit]

    # 重置当前HEAD为指定commit,但保持暂存区和工作区不变
    $ git reset --keep [commit]

    # 新建一个commit,用来撤销指定commit
    # 后者的所有变化都将被前者抵消,并且应用到当前分支
    $ git revert [commit]

    # 暂时将未提交的变化移除,稍后再移入
    $ git stash
    $ git stash pop

    多人合作开发

    1. 源仓库

      项目开始的时候,项目发起者构建起一个项目的最原始的仓库,称之为源仓库(origin)。

      • 源仓库有两个作用
        1. 汇总参与该项目的各个开发者的代码
        2. 存放趋于稳定和可发布的代码
      • 源仓库受到保护,开发者不直接对其进行开发工作。只有项目管理者能对其进行较高权限的操作。
    2. 开发者仓库

      如上所说,任何开发者都不会对源仓库进行直接的操作,源仓库建立以后,每个开发者需要fork一份源仓库,作为自己日常开发的仓库。

      每个开发者所fork的仓库是完全独立的,互不干扰。每个开发者仓库相当于一个源仓库实体的镜像,开发者在这个镜像中进行编码,提交到自己的仓库中,这样就可以轻易地实现团队成员之间的并行开发工作。而开发工作完成以后,开发者可以向源仓库发送pull request,请求管理员把自己的代码合并到源仓库中,这样就实现了分布式开发工作,和最后的集中式的管理。

    3. 分支 (Branch)

      git分支有两类,5种:

      1
      2
      3
      4
      5
      6
      7
      永久性分支
      master branch:主分支
      develop branch:开发分支
      临时性分支
      feature branch:功能分支
      release branch:预发布分支
      hotfix branch:bug修复分支
    4. 多人合作具体步骤

      1)clone远程代码

      1
      git clone [url]

      2)切换到develop分支,将本地新项目提交到本地develop分支,再将本地develp分支上的新建项目将上传到远程develop分支。

      1
      2
      3
      4
      git checkout develop
      git add .
      git commit -m "new branch commit"
      git push origin new branch commit

    3)开发

    • 切换到develop分支

      1
      git checkout develop
    • 分出一个功能性分支

      1
      git checkout -b function-branch
    • 在功能性分支上进行开发工作,多次commit,测试以后

    • 把做好的功能合并到develop中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      git checkout develop
      # 回到develop分支

      git merge --no-ff function-branch
      # 把做好的功能合并到develop中

      git branch -d functio-branch
      # 删除功能性分支

      git push origin develop
      # 把develop提交到自己的远程仓库中

    4)合并远程master和develop分支

    切换到master分支,从远程pull代码,将develop分支合并到本地master分支(此时本地master分支是与远程同步的),有冲突解决,没有则罢。最后push到远程master仓库。

    (注)保证多人协作的时候尽量少出现merge conflict和污染主分支,做到以下几点:

    • 做好分工,尽量避免出现多人修改同一个文件。

    • 每个人的所有开发工作都只在自己的分支开发。

    • 每个人只允许在自己的分支直接push远程分支。

    • 合并的时候必须遵循以下条件.

      1)首先本地切换到develop分支

      1
      git checkout develop

      2)git pull

      3)那么在pull到远程的develop最新的内容之后,

      1
      git merge  [branch]

      4)如果出现confict那么清除conflict之后,commit。然后把本地develop push到远程develop。

      5)没完成一个功能就提交一次。不要累计代码。

git checkout dev

git push origin –delete release-1.00.00

l

正常上线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
git clone

git checkout dev

git checkout -b feature-function

git add .
git commit -m '结束功能开发'

git checkout dev
git merge feature-function
git push origin dev

git checkout -b release-2019-09-29
git push origin release-2019-09-29

线上发现bug 紧急上线

git checkout -b hotfix-2019-09-29 origin/master
git checkout -b hotfix-2019-09-29
git add .
git commit -m 'message'
git push origin hotfix-2019-09-29

git checkout dev
git merge hotfix-2019-09-29
git push origin dev


git branch -d feature-function
l

git 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
正常上线
git clone

git checkout dev

git checkout -b feature-function

git add .
git commit -m '结束功能开发'

git checkout dev
git merge feature-function
git push origin dev

git checkout -b release-2019-09-29
git push origin release-2019-09-29

线上发现bug 紧急上线

git checkout -b hotfix-2019-09-29 origin/master
git checkout -b hotfix-2019-09-29
git add .
git commit -m 'message'
git push origin hotfix-2019-09-29

git checkout dev
git merge hotfix-2019-09-29
git push origin dev


git branch -d feature-function
l