netty源码解析01-NioEventLoop详解01

  • netty NioEventLoop详解
    • 前言
    • NioEventLoop 创建
      • Executor
      • newChild()
      • newChooser()
    • NioEventLoop 启动
    • NioEventLoop IO 事件的检测和处理
    • runTask 执行队列任务
      • 任务的添加
      • 俩种任务的聚合与执行
    • 总结

提示:如果没有听过或者使用过java nio 和netty,不建议直接阅读此文章,建议先去了解java nio 和netty之后再阅读此文章。

netty NioEventLoop详解

上一篇文章,我们介绍了netty的启动流程,我们知道了netty在jdk nio的基础上多做了哪些事情,在上篇文章中,我们接触到了netty 的一个核心组件,就是我们的NioEventLoop。那么我们这篇文章就来对NioEventLoop做一个详细的探索吧~

前言

如果在面试中遇到过面试官问到netty相关的面试题,或许以下的几个题目你不会陌生:

  1. 默认情况下,netty服务端起多少个线程呢?什么时候启动呢?
  2. 知道jdk nio 什么时候会出现空轮询导致CPU飙升到100%吗?netty又是如何解决了这个BUG呢?
  3. netty是如何保证异步串行无锁化的?

NioEventLoop 创建

NioEventLoop的创建其实就比较简单了,它是从创建EventLoopGroup开始的,你还记得我们上一节的demo代码中,有这么一行代码:NioEventLoopGroup bossGroup = new NioEventLoopGroup(); 那么NioEventLoop就是从new NioEventLoopGroup();开始的。大致的流程我还是先告诉你:

  1. new NioEventLoopGroup(); 创建线程组,默认线程数量是cpu核数*2。
    1. new ThreadPreTaskExecutor(); 创建线程创建器。
    2. for(nThreads){newChild()} 创建对应数量的NioEventLoop,每个NioEventLoop对应一个线程。
    3. chooser = chooserFactory.newChooser(children);通过这个创建一个线程选择器,以后netty执行对应的事件,就通过这个选择器来选择哪一个NioEventLoop来进行执行。

我们一起去看看这个EventLoopGroup的构造函数做了哪些事情吧:

//EventLoopGroup.java
//第一步,从这里开始。
public NioEventLoopGroup() {this(0);
}
//第二步,依次调用对应的构造函数。
public NioEventLoopGroup(int nThreads) {//这里调用了一个新的构造函数,并且传入了一个Executor,为null,这里Executor的作用就是来创建NioEventLoop对应的底层的线程。this(nThreads, (Executor) null);
}
//第三步,依次调用对应的构造函数。
public NioEventLoopGroup(int nThreads, Executor executor) {//这里又多了一个参数provider,它的作用就是创建每个NioEventLoop对应的selector,对于这个,后面我们会讲到,不需要着急。this(nThreads, executor, SelectorProvider.provider());
}
//第四步,依次调用对应的构造函数。
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider) {this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
}
//第五步,依次调用对应的构造函数。
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
//MultithreadEventLoopGroup.java
//第六步,依次调用对应的构造函数。
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
//MultithreadEventExecutorGroup.java
//第七步,依次调用对应的构造函数。
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
//第八步,依次调用对应的构造函数。最终来到了我们重要的地方。
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,EventExecutorChooserFactory chooserFactory, Object... args) {if (nThreads <= 0) {throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));}//这里就是第一步,创建一个线程创建器。作用在上面的注释已经解释过了,是用于创建NioEventLoop底层的线程。if (executor == null) {//newDefaultThreadFactory()就是去创建线程。executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());}//创建对用线程数量的EventExecutor数组children = new EventExecutor[nThreads];//通过一个for循环,去创建一个相同数量的NioEventLoopfor (int i = 0; i < nThreads; i ++) {boolean success = false;try {//我们来看看这个newChild做了什么事情children[i] = newChild做了什么事情(executor, args);success = true;} catch (Exception e) {// TODO: Think about if this is a good exception typethrow new IllegalStateException("failed to create a child event loop", e);} finally {//这里当创建失败的时候,去关闭。if (!success) {for (int j = 0; j < i; j ++) {children[j].shutdownGracefully();}for (int j = 0; j < i; j ++) {EventExecutor e = children[j];try {while (!e.isTerminated()) {e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);}} catch (InterruptedException interrupted) {// Let the caller handle the interruption.Thread.currentThread().interrupt();break;}}}}}//第三步,通过这个工厂类创建一个线程选择器。chooser = chooserFactory.newChooser(children);final FutureListener<Object> terminationListener = new FutureListener<Object>() {@Overridepublic void operationComplete(Future<Object> future) throws Exception {if (terminatedChildren.incrementAndGet() == children.length) {terminationFuture.setSuccess(null);}}};for (EventExecutor e: children) {e.terminationFuture().addListener(terminationListener);}Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);Collections.addAll(childrenSet, children);readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

上面源代码中的注释,我们知道上述这个三个步骤是在源码中的哪个地方,那么我们现在来深入分析这个三个步骤吧。

Executor

我们通过上述代码知道,executor开始的时候是null,通过executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());进行赋值。首先我们看看这个newDefaultThreadFactory();做了什么事情,代码如下:

//MultithreadEventExecutorGroup.java
//第一步,创建了一个默认的线程创建工厂类。
protected ThreadFactory newDefaultThreadFactory() {return new DefaultThreadFactory(getClass());
}
//DefaultThreadFactory.java
//第二步,进入对应的构造函数。
public DefaultThreadFactory(Class<?> poolType) {this(poolType, false, Thread.NORM_PRIORITY);
}
//第三步,进入对应的构造函数。
public DefaultThreadFactory(Class<?> poolType, boolean daemon, int priority) {//这里的toPoolName方法就是将class的全类名通过StringUtil.simpleClassName(poolType);进行简化,去除了前面的包名称。//如果转化之后成className长度为0,那么就返回字符串"unknown"。//如果转化之后成className长度为1,那么就把lassName转成小写,然后返回。//否则,如果className第一个字母是大写,并且第二个字母是小写时。那么久将第一个字母转化成小写然后返回,如果不是,那么就直接返回className。this(toPoolName(poolType), daemon, priority);
}public DefaultThreadFactory(String poolName, boolean daemon, int priority, ThreadGroup threadGroup) {if (poolName == null) {throw new NullPointerException("poolName");}if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {throw new IllegalArgumentException("priority: " + priority + " (expected: Thread.MIN_PRIORITY <= priority <= Thread.MAX_PRIORITY)");}//这里就是保存线程名称的前缀。也就是 nioEventLoopGroup-id-//注意,有的文章和视频把nioEventLoopGroup说成了nioEventLoop。这是错的,因为整体都是从 new NioEventLoopGroup();开始的,所以getCalss()//自然而然就是NioEventLoopGroup 而不是NioEventLoop,这里注意一下就行,我把debug模式下的内容截图下来了,有图有真相~prefix = poolName + '-' + poolId.incrementAndGet() + '-';this.daemon = daemon;this.priority = priority;this.threadGroup = threadGroup;
}

下面这张图就是为了说明这个poolName是nioEventLoopGroup 而并非是其他文章和视频所说的nioEventLoop。其实真正阅读和调试过源码的人不会出现这种低级错误,所以,懂得都懂~

image-20211028204054653

上面我们分析过了创建线程的DefaultThreadFactory的构造函数了,那么我们接下来看看这个ThreadPerTaskExecutor做了什么事情吧:

//ThreadPerTaskExecutor.java
//
public ThreadPerTaskExecutor(ThreadFactory threadFactory) {if (threadFactory == null) {throw new NullPointerException("threadFactory");}//就是把创建的线程创建工厂类保存起来。this.threadFactory = threadFactory;
}@Override
public void execute(Runnable command) {//每次执行task任务时,都会创建一个新的线程,去执行task。threadFactory.newThread(command).start();
}
//DefaultThreadFactory.java
//通过这里的newThread方法去创建一个新的线程。
@Override
public Thread newThread(Runnable r) {//这里netty使用的自己的一个DefaultRunnableDecorator,它implements了Runnable类。 Thread t = newThread(new DefaultRunnableDecorator(r), prefix + nextId.incrementAndGet());try {if (t.isDaemon()) {if (!daemon) {t.setDaemon(false);}} else {if (daemon) {t.setDaemon(true);}}if (t.getPriority() != priority) {t.setPriority(priority);}} catch (Exception ignored) {// Doesn't matter even if failed to set.}return t;
}
//这里它自己创建了一个FastThreadLocalThread,它继承自Thread,netty这么做的目的,就是对Thread中的ThreadLocal做进一步的优化。
//后续会讲到这个,感兴趣的可以自己先去研究研究。
protected Thread newThread(Runnable r, String name) {return new FastThreadLocalThread(threadGroup, r, name);
}

到这里,我们知道了这个ThreadPerTaskExecutor做了什么事情。他主要是在每次执行任务的时候,都会创建一个线程实例。并且线程名称的规则就是nioEventLoopGroup-poolId-threadId 这样的格式。

newChild()

上面讲到newChild() 是去创建NioEventLoop,其中NioEventLoop构造函数做了以下几件事情:

  1. 保存上面创建的线程执行器ThreadPerTaskExecutor。
  2. 创建一个队列MpscQueue。
  3. 创建一个selector。

我们来看看具体实现:

//NioEventLoopGroup.java
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {return new NioEventLoop(this, executor, (SelectorProvider) args[0],((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
//NioEventLoop.java
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);if (selectorProvider == null) {throw new NullPointerException("selectorProvider");}if (strategy == null) {throw new NullPointerException("selectStrategy");}//这里就进行一些保存变量操作。provider = selectorProvider;//第三步,通过openSelector();创建一个selector。那么也就是一个NioEventLoop对应一个selector。selector = openSelector();selectStrategy = strategy;
}
//SingleThreadEventLoop.java
//这里就是第二步
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,boolean addTaskWakesUp, int maxPendingTasks,RejectedExecutionHandler rejectedExecutionHandler) {super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);tailTasks = newTaskQueue(maxPendingTasks);
}//SingleThreadEventExecutor.java
//这里就是第一步,,创建了taskQueue。
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,boolean addTaskWakesUp, int maxPendingTasks,RejectedExecutionHandler rejectedHandler) {super(parent);this.addTaskWakesUp = addTaskWakesUp;this.maxPendingTasks = Math.max(16, maxPendingTasks);this.executor = ObjectUtil.checkNotNull(executor, "executor");taskQueue = newTaskQueue(this.maxPendingTasks);rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
//
@Override
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {// This event loop never calls takeTask()//multiple producers and a single consumer,这里multiple producers 对应的就是外部线程。consumer就是对应NioEventLoop.//也就是说外部线程(非NioEventLoop线程执行)可以把任务交给NioEventLoop去消费。return PlatformDependent.newMpscQueue(maxPendingTasks);
}

那么我们NioEventLoop的创建过程就搞明白了,但是创建了这么多个NioEventLoop,那么我们该怎么去选择哪一个NioEventLoop去执行任务呢?这时候线程选择器就至关重要了,netty就是通过chooser = chooserFactory.newChooser(children);来创建一个线程选择器。

newChooser()

而这个chooser就是为了给每一个新连接绑定对应的NioEventLoop,那么对应的方法就在NioEventLoopGroup中的next();方法中,我们一起来看下吧~

//MultithreadEventExecutorGroup.java  NioEventLoopGroup的父类。
@Override
public EventExecutor next() {//其实原理非常的简单。就是依次选择,第一个连接使用第一个NioEventLoop。。。第N个使用第N个NioEventLoop,第N+1个连接就从头开始,使用第一个连接。//N为当初创建线程数量。默认为CPU核数的俩倍。//这里netty也做了一些优化,就是判断这个N是否为2的n次幂。如果是2的n次幂,那么就直接使用位操作进行选择,否则使用我们的取余操作。//这里其实chooserFactory.newChooser(children);会判断,然后创建俩个不同的实体。具体实现方法如下:return chooser.next();
}
//DefaultEventExecutorChooserFactory.java
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {//判断executors.length是否为2的n次方if (isPowerOfTwo(executors.length)) {return new PowerOfTowEventExecutorChooser(executors);} else {return new GenericEventExecutorChooser(executors);}
}
private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser {private final AtomicInteger idx = new AtomicInteger();private final EventExecutor[] executors;PowerOfTowEventExecutorChooser(EventExecutor[] executors) {this.executors = executors;}@Overridepublic EventExecutor next() {//这里直接就进行位操作来选择,速度会比取余快上很多,但前提是executors.length必须是2的n次方//这里不明白的可以去多试几次。就明白了。return executors[idx.getAndIncrement() & executors.length - 1];}
}private static final class GenericEventExecutorChooser implements EventExecutorChooser {private final AtomicInteger idx = new AtomicInteger();private final EventExecutor[] executors;GenericEventExecutorChooser(EventExecutor[] executors) {this.executors = executors;}@Overridepublic EventExecutor next() {//这里直接就只能进行取余操作了,因为executors.length不是2的n次方return executors[Math.abs(idx.getAndIncrement() % executors.length)];}
}

到这里,我们就弄懂了NioEventLoop的创建,以及如果去选择哪一个NioEventLoop去实行任务。那么我们接下来去了解NioEventLoop是怎么被启动起来的。

NioEventLoop 启动

这里有俩个地方启动了,第一个就是我们之前说的绑定端口的时候,其实就是在NioEventLoop中的线程进行创建的。第二个就是新连接接入时,通过chooser来绑定NioEventLoop。这个后续会有文章详细讲解新连接接入流程。这里就以第一个为例,服务端绑定端口的时候,就是在doBind0();方法中调用了executor(); 我们一起来回忆一吧:

//AbstractBootstrap.java
private static void doBind0(final ChannelFuture regFuture, final Channel channel,final SocketAddress localAddress, final ChannelPromise promise) {// This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up// the pipeline in its channelRegistered() implementation.//这里就是使用了NioEventLoop的线程去执行绑定操作。channel.eventLoop().execute(new Runnable() {@Overridepublic void run() {if (regFuture.isSuccess()) {channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);} else {promise.setFailure(regFuture.cause());}}});
}
//SingleThreadEventExecutor.java
@Override
public void execute(Runnable task) {if (task == null) {throw new NullPointerException("task");}//判断当前执行的线程是否是NioEventLoop线程中,其实服务端启动的过程中,是通过主线程去启动的,也就是main线程。所以inEventLoop为false。boolean inEventLoop = inEventLoop();if (inEventLoop) {addTask(task);} else {startThread();addTask(task);if (isShutdown() && removeTask(task)) {reject();}}if (!addTaskWakesUp && wakesUpForTask(task)) {wakeup(inEventLoop);}
}
//AbstractEventExecutor.java
@Override
public boolean inEventLoop() {//将当前线程传入,这里当前线程为main。return inEventLoop(Thread.currentThread());
}//SingleThreadEventExecutor.java
@Override
public boolean inEventLoop(Thread thread) {return thread == this.thread;
}
private void startThread() {//判断当前线程是否是未启动。if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {//通过一个cas操作,来进行开启线程,这里就是防止多个线程同时调用同一个NioEventLoop实例执行这段代码,造成线程不安全。if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {//然后我们来看看这个方法doStartThread();}}
}
private void doStartThread() {assert thread == null;//这里其实就是通过executor的execute方法。我们前面分析过了,它其实就是通过那个ThreadFactory进行创建一个新的线程去执行这个任务。executor.execute(new Runnable() {@Overridepublic void run() {//拿到当前线程,并保存。这里就是进行NioEventLoop 和线程进行唯一的绑定。thread = Thread.currentThread();if (interrupted) {thread.interrupt();}boolean success = false;updateLastExecutionTime();try {//然后去调用NioEventLoop的run方法。这里就是实际的调用。SingleThreadEventExecutor.this.run();success = true;} catch (Throwable t) {logger.warn("Unexpected exception from an event executor: ", t);} finally {for (;;) {int oldState = STATE_UPDATER.get(SingleThreadEventExecutor.this);if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {break;}}// Check if confirmShutdown() was called at the end of the loop.if (success && gracefulShutdownStartTime == 0) {logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " +"before run() implementation terminates.");}try {// Run all remaining tasks and shutdown hooks.for (;;) {if (confirmShutdown()) {break;}}} finally {try {cleanup();} finally {STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);threadLock.release();if (!taskQueue.isEmpty()) {logger.warn("An event executor terminated with " +"non-empty task queue (" + taskQueue.size() + ')');}terminationFuture.setSuccess(null);}}}}});
}

到这里,我们知道了NioEventLoop的启动过程,我们以服务端启动时绑定端口进行举例,通过eventLoop.execute进行启动。最后的执行就是在NioEventLoop中的run();方法。

NioEventLoop IO 事件的检测和处理

@Override
protected void run() {for (;;) {try {//这里进行轮询IO事件。switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {case SelectStrategy.CONTINUE:continue;case SelectStrategy.SELECT:// 重点在这里。这里有一个wakenUp标识,标识当前select操作是否被唤醒。每次进行select操作时// 将标识置为false。表示需要进行select操作,并且表示是未唤醒状态。// 这里wakeUp是可以被外部线程唤醒的。 select(wakenUp.getAndSet(false));if (wakenUp.get()) {selector.wakeup();}default:// fallthrough}//暂时不重要的代码进行省略processSelectedKeys();}
}

从上面代码中,我们知道,select方法是最重要的,所以我们来看看select方法到底做了什么吧。

private void select(boolean oldWakenUp) throws IOException {Selector selector = this.selector;try {int selectCnt = 0;// 首先,获取当前时间。long currentTimeNanos = System.nanoTime();// 然后当前时间加上截止时间。这里我们需要知道,NioEventLoop底层有一个定时任务队列,如果感兴趣的可以先去了解一下,// 后面会说到,这个定时任务队列就是按照任务的截止时间排序的一个具有优先级别的队列。这里的delayNanos方法就是用来计算这个定时任务// 队列第一个任务的截止时间,这里后面会说到,感兴趣的自己可以先去了解一下。所以这个selectDeadLineNanos就是当前执行select操作最长不能超过selectDeadLineNanos时间。long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);for (;;) {// 先获取超时时间,如果操作超时了,并且一次也没有进行select,那么就进行一个非阻塞的selectNow。并且将selectCnt设置为1。long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;if (timeoutMillis <= 0) {if (selectCnt == 0) {selector.selectNow();selectCnt = 1;}break;}// If a task was submitted when wakenUp value was true, the task didn't get a chance to call// Selector#wakeup. So we need to check task queue again before executing select operation.// If we don't, the task might be pended until select operation was timed out.// It might be pended until idle timeout if IdleStateHandler existed in pipeline.//如果没有到截止时间,如果当前有任务,也就是taskQueue不为空,并且通过cas操作将wakeUp在select执行前改为false改为true,注意,这个wakeUp是可以被外部线程修改的,也就是select操作是可以被外部线程唤醒的,所以这里到这里前没有被外部线程唤醒过。// 如果俩个条件都满足要求,那么就执行一个非阻塞的select操作,并将selectCnt设置为1,也就是select次数。然后跳出循环,结束此次select操作。if (hasTasks() && wakenUp.compareAndSet(false, true)) {selector.selectNow();selectCnt = 1;break;}// 如果当前任务队列为空,并且阻塞时间没到,那么就进行一个阻塞式的select。// 这个selectedKeys表示轮询到的事件,如果等于0,表示当前没有事件。int selectedKeys = selector.select(timeoutMillis);// 轮询次数加一。selectCnt ++;// 也就是当满足如下任意一个条件时,就会跳出循环,结束select操作:// 1. 当前轮询到了事件,也就是selectedKeys不等于0.// 2. 这个select操作需要被唤醒。// 3. 当前wakeUp被外部线程修改,表示这个select操作需要被唤醒// 4. 任务队列中有任务。// 5. 定时任务队列中存在任务。if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {// - Selected something,// - waken up by user, or// - the task queue has a pending task.// - a scheduled task is ready for processingbreak;}// 如果当前线程被中断。if (Thread.interrupted()) {// Thread was interrupted so reset selected keys and break so we not run into a busy loop.// As this is most likely a bug in the handler of the user or it's client library we will// also log it.//// See https://github.com/netty/netty/issues/2426if (logger.isDebugEnabled()) {logger.debug("Selector.select() returned prematurely because " +"Thread.currentThread().interrupt() was called. Use " +"NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");}selectCnt = 1;break;}//这里就对jdk空轮询的bug用一种巧妙的方式给解决了。敲黑板,这里面试经常问到。long time = System.nanoTime();// 这里就是表示如果阻塞完后的当前之间与开始时间之间的时间间隔超过了超时时间,那么此次select结束。if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {// timeoutMillis elapsed without anything selected.//到这里,那么就意味着已经执行过一次阻塞式的select操作了。selectCnt = 1;// 那么反过来说明了本来需要阻塞timeoutMillis那么久,但是实际上并没有阻塞这么久,select方法就被唤醒了。// 这里极限情况下就造成了空轮询,因为在一些极端情况下,可能select并没有进行阻塞,立马返回了,这就导致了一直在死循环// 这就导致了空轮询,照成了cpu飙升100%。这种极端情况之一就是连接出现了RST,// 因为poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为POLLHUP或者POLLERR,// eventSet事件集合发生了变化,这就导致Selector会被唤醒,进而导致CPU 100%问题。// 根本原因就是JDK没有处理好这种情况,比如SelectionKey中就没定义有异常事件的类型。// 上述描述来自于:https://zhuanlan.zhihu.com/p/92133508 可以去自己查看。} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {// 那么netty在这里使用了一个非常巧妙的方式,就是这种情况连续空轮询了一定次数后(512次),说明这个select已经出现了问题,// 那么就将selector上原来的事件和属性重新注册到一个新的selector上,从而解决了空轮询的bug。// The selector returned prematurely many times in a row.// Rebuild the selector to work around the problem.logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",selectCnt, selector);// 这个方法就不点进去查看了,自己可以去查看一下,原理非常简单,就是重新进行注册到新的selector上,并且重新绑定selector。rebuildSelector();// 这时候就将局部变量重新赋值。selector = this.selector;// Select again to populate selectedKeys.selector.selectNow();selectCnt = 1;break;}currentTimeNanos = time;}if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {if (logger.isDebugEnabled()) {logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",selectCnt - 1, selector);}}} catch (CancelledKeyException e) {if (logger.isDebugEnabled()) {logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",selector, e);}// Harmless exception - log anyway}
}

上面我们重点讲到了select方法,就是当有事件或者有任务或者定时任务时,那么就结束select操作,也就是说,它只是去查找,直到有事件或者任务,才会结束,那么下面我们来说说找到事件之后,怎么去进行处理这些时间,netty就是通过processSelectedKeys();进行处理IO事件的。处理流程大致如下:

  1. select keySet 集合的优化,将底层的hashSet通过反射将hashSet替换成数组。那么将select操作的时间复杂度变成了O(1);这个优化放在了NioEventLoop的构造函数中,也就是openSelector();方法。
  2. 调用processSelectedKeysOptimized();真正的处理IO事件。

我们来看看他的优化过程吧:

private Selector openSelector() {final Selector selector;try {selector = provider.openSelector();} catch (IOException e) {throw new ChannelException("failed to open a new selector", e);}// 这里表示是否需要去进行优化。if (DISABLE_KEYSET_OPTIMIZATION) {return selector;}// 在这里,使用了一个自定义的Set进行保存selectKeys,在4.1.6.Final版本时使用了俩个数组进行交替实现,使用flip进行切换,他只实现了final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();// 省略暂时不需要的代码,我们直接看看SelectedSelectionKeySet的代码:// 他其实还是实现了Set接口,但是其底层实现使用了俩个数组,当前源码版本是4.1.6.Final。// 但是在最新的版本中只用到了一个数组,这是因为他想通过俩个数组进行交替实现,但是对于这个SelectedSelectionKeySet只是单线程的处理。// 所以有人提了一个issues,作者也认可了,在最新版本修改了,使用了一个数组。final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {private SelectionKey[] keysA;private int keysASize;private SelectionKey[] keysB;private int keysBSize;private boolean isA = true;//创建了一个大小为1024的数组。SelectedSelectionKeySet() {keysA = new SelectionKey[1024];keysB = keysA.clone();}// 就是在这里进行了一个优化,因为jdk Nio 使用的是一个HashSet,我们知道,HashSet他使用的又是一个Hashmap。// HashMap的add方法时间复杂度并不稳定,最坏的可以达到O(n)。所以这里netty作者自己使用数组实现了一个KeySet。@Overridepublic boolean add(SelectionKey o) {if (o == null) {return false;}// 这是因为是单线程的,所以可以这样直接操作。if (isA) {int size = keysASize;keysA[size ++] = o;keysASize = size;//如果容量不够,就进行扩容。if (size == keysA.length) {doubleCapacityA();}} else {int size = keysBSize;keysB[size ++] = o;keysBSize = size;if (size == keysB.length) {doubleCapacityB();}}return true;}// 这里没啥好说的,就是对数组进行双倍扩容。private void doubleCapacityA() {SelectionKey[] newKeysA = new SelectionKey[keysA.length << 1];System.arraycopy(keysA, 0, newKeysA, 0, keysASize);keysA = newKeysA;}// 这里没啥好说的,就是对数组进行双倍扩容。private void doubleCapacityB() {SelectionKey[] newKeysB = new SelectionKey[keysB.length << 1];System.arraycopy(keysB, 0, newKeysB, 0, keysBSize);keysB = newKeysB;}//通过这个获取SelectionKey数组,并且获取SelectionKey[] flip() {if (isA) {isA = false;keysA[keysASize] = null;keysBSize = 0;return keysA;} else {isA = true;keysB[keysBSize] = null;keysASize = 0;return keysB;}}@Overridepublic int size() {if (isA) {return keysASize;} else {return keysBSize;}}// 这三个方法是不支持的,因为使用SelectedSelectionKeySet时,// 并不需要使用这三个方法,这也就是为啥能使用数组去实现KeySet。@Overridepublic boolean remove(Object o) {return false;}@Overridepublic boolean contains(Object o) {return false;}@Overridepublic Iterator<SelectionKey> iterator() {throw new UnsupportedOperationException();}}
}

看完SelectedSelectionKeySet 我们继续顺着openSelector();后面的方法查看:

// 这里其实就是使用了反射的方式得到SelectorImpl类对象。
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {@Overridepublic Object run() {try {return Class.forName("sun.nio.ch.SelectorImpl",false,PlatformDependent.getSystemClassLoader());} catch (ClassNotFoundException e) {return e;} catch (SecurityException e) {return e;}}
});
// 这里判断是否是一个Calss对象,并且判断这个selector是否这个maybeSelectorImplClass的一个实现。
if (!(maybeSelectorImplClass instanceof Class) ||// ensure the current selector implementation is what we can instrument.!((Class<?>) maybeSelectorImplClass).isAssignableFrom(selector.getClass())) {if (maybeSelectorImplClass instanceof Exception) {Exception e = (Exception) maybeSelectorImplClass;logger.trace("failed to instrument a special java.util.Set into: {}", selector, e);}// 如果不是他的实现,并且通过反射拿到的selectorImpl类对象并没有报错,那么就直接返回该selector。return selector;
}
// 如果是他的实现。
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {@Overridepublic Object run() {try {// 这里就通过反射的方式,拿到俩个属性,分别是selectedKeys 和 publicSelectedKeys。Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");// 下面就是标准的通过反射进行赋值。selectedKeysField.setAccessible(true);publicSelectedKeysField.setAccessible(true);// 将jdk Nio 里面的HashSet替换成netty自己的selectedKeySet。selectedKeysField.set(selector, selectedKeySet);publicSelectedKeysField.set(selector, selectedKeySet);return null;} catch (NoSuchFieldException e) {return e;} catch (IllegalAccessException e) {return e;} catch (RuntimeException e) {// JDK 9 can throw an inaccessible object exception here; since Netty compiles// against JDK 7 and this exception was only added in JDK 9, we have to weakly// check the typeif ("java.lang.reflect.InaccessibleObjectException".equals(e.getClass().getName())) {return e;} else {throw e;}}}
});if (maybeException instanceof Exception) {selectedKeys = null;Exception e = (Exception) maybeException;logger.trace("failed to instrument a special java.util.Set into: {}", selector, e);
} else {// 这里就将selectedKeySet 保存到NioEventLoop 的成员变量selectedKeys中。selectedKeys = selectedKeySet;logger.trace("instrumented a special java.util.Set into: {}", selector);
}return selector;

那么到这里,我们就知道了netty对selectedKeys的优化,通过netty自己实现的一个set,将add方法进行优化,使得add方法的时间复杂度降到了O(1)

然后我们继续往下,最终执行完select();方法后,会执行processSelectedKeys();那么我们查看一下processSelectedKeys();有关的的源码:

private void processSelectedKeys() {if (selectedKeys != null) {processSelectedKeysOptimized(selectedKeys.flip());} else {processSelectedKeysPlain(selector.selectedKeys());}
}
// 最终会调用到这个方法里面。
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {// 遍历selectedKeysfor (int i = 0;; i ++) {final SelectionKey k = selectedKeys[i];if (k == null) {break;}// null out entry in the array to allow to have it GC'ed once the Channel close// See https://github.com/netty/netty/issues/2363selectedKeys[i] = null;// 这里你们是否还记得这个a变量是啥吗?在我们之前注册的时候,将NioServerSocketChannel 当作一个attachment,绑定到了这个Jdk Channel 中,这里可以通过SelectionKey实例拿到。final Object a = k.attachment();if (a instanceof AbstractNioChannel) {// 我们看看这个方法的具体实现。processSelectedKey(k, (AbstractNioChannel) a);} else {@SuppressWarnings("unchecked")NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;processSelectedKey(k, task);}if (needsToSelectAgain) {// null out entries in the array to allow to have it GC'ed once the Channel close// See https://github.com/netty/netty/issues/2363for (;;) {i++;if (selectedKeys[i] == null) {break;}selectedKeys[i] = null;}selectAgain();// Need to flip the optimized selectedKeys to get the right reference to the array// and reset the index to -1 which will then set to 0 on the for loop// to start over again.//// See https://github.com/netty/netty/issues/1523selectedKeys = this.selectedKeys.flip();i = -1;}}
}private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {// 拿到这个channel的unsafe。final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();// 判断这个SelectionKey是否合法,因为这个连接可能有点问题if (!k.isValid()) {final EventLoop eventLoop;try {eventLoop = ch.eventLoop();} catch (Throwable ignored) {// If the channel implementation throws an exception because there is no event loop, we ignore this// because we are only trying to determine if ch is registered to this event loop and thus has authority// to close ch.return;}// Only close ch if ch is still registerd to this EventLoop. ch could have deregistered from the event loop// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is// still healthy and should not be closed.// See https://github.com/netty/netty/issues/5125if (eventLoop != this || eventLoop == null) {return;}// close the channel if the key is not valid anymore// 那么就调用unsafe 的close方法。其实就是使用pipeline进行操作。关于pipeline,后面我们会讲到。unsafe.close(unsafe.voidPromise());return;}try {// 如果是合法的,就拿到这个SelectionKey上的IO事件。int readyOps = k.readyOps();// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise// the NIO JDK channel implementation may throw a NotYetConnectedException.//判断这个事件的具体是哪个。// 判断是否是OP_CONNECT事件。if ((readyOps & SelectionKey.OP_CONNECT) != 0) {// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking// See https://github.com/netty/netty/issues/924int ops = k.interestOps();ops &= ~SelectionKey.OP_CONNECT;k.interestOps(ops);unsafe.finishConnect();}// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.// 判断是否是OP_WRITE事件。if ((readyOps & SelectionKey.OP_WRITE) != 0) {// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to writech.unsafe().forceFlush();}// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead// to a spin loop// 判断是否是OP_READ或者OP_ACCEPT事件。if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {unsafe.read();if (!ch.isOpen()) {// Connection already closed - no need to handle write.return;}}} catch (CancelledKeyException ignored) {unsafe.close(unsafe.voidPromise());}
}

到这里我们就看到了netty是如何进行IO检测的,我们梳理一下流程。首先我们通过select进行获取IO事件,当然这里并不只有IO事件,还有外部线程的打断、任务队列和定时任务队列中存在任务,都会打断select的轮询,然后我们遍历这个selector上的SelectionKey,判断当前的IO事件,这样我就检测到了IO事件。并且通过pipeline进行处理这些IO事件。

runTask 执行队列任务

这里就到了我们NioEventLoop的最后一个流程:执行任务队列和定时任务队列里面的任务。我们先梳理一下大概的流程。在NioEventLoop中提供了接口,用于添加和删除普通任务和定时任务,然后将定时任务与普通任务进行合并,然后进行执行俩种任务。

任务的添加

我们上面说了,任务分为俩种,一种是普通任务,一种是定时任务,其实我们分析过了,在NioEventLoop构造函数中,就创建了taskQueue。我们一起来看看吧:

// NioEventLoop.java
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {// 进入父类构造方法中。super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);if (selectorProvider == null) {throw new NullPointerException("selectorProvider");}if (strategy == null) {throw new NullPointerException("selectStrategy");}provider = selectorProvider;selector = openSelector();selectStrategy = strategy;
}
// SingleThreadEventLoop.java
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,boolean addTaskWakesUp, int maxPendingTasks,RejectedExecutionHandler rejectedExecutionHandler) {// 继续跟进去super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);tailTasks = newTaskQueue(maxPendingTasks);
}//SingleThreadEventExecutor.java
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,boolean addTaskWakesUp, int maxPendingTasks,RejectedExecutionHandler rejectedHandler) {super(parent);this.addTaskWakesUp = addTaskWakesUp;this.maxPendingTasks = Math.max(16, maxPendingTasks);this.executor = ObjectUtil.checkNotNull(executor, "executor");// 这里调用PlatformDependent.newMpscQueue(maxPendingTasks);进行创建一个MpscQueue,这里我们之前讲解过了。taskQueue = newTaskQueue(this.maxPendingTasks);rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
// 然后在外部线程调用NioEventLoop的execute方法时:
//SingleThreadEventExecutor.java
@Override
public void execute(Runnable task) {if (task == null) {throw new NullPointerException("task");}boolean inEventLoop = inEventLoop();if (inEventLoop) {addTask(task);} else {//这里startThread我们之前就分析过了。startThread();// 我们仔细来查看这个addTask(task)addTask(task);if (isShutdown() && removeTask(task)) {reject();}}if (!addTaskWakesUp && wakesUpForTask(task)) {wakeup(inEventLoop);}
}
//SingleThreadEventExecutor.java
protected void addTask(Runnable task) {if (task == null) {throw new NullPointerException("task");}//这里调用了offerTask,那么其实就是往taskQueue中添加一个任务。这里就是对普通任务的添加。if (!offerTask(task)) {reject(task);}
}
final boolean offerTask(Runnable task) {if (isShutdown()) {reject();}return taskQueue.offer(task);
}

分析了普通任务的添加,我们来查看定时任务的添加,如果你是用过netty的定时任务,那么你应该知道,这个入口就在NioEventLoop的schedule();方法,这个方法在AbstractScheduledEventExecutor类中,它是NioEventLoop的一个父类。 我们来看看这个方法的实现:

// AbstractScheduledEventExecutor.java
@Override
public  ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {ObjectUtil.checkNotNull(command, "command");ObjectUtil.checkNotNull(unit, "unit");if (delay < 0) {throw new IllegalArgumentException(String.format("delay: %d (expected: >= 0)", delay));}// 这里将callable 封装成netty自己的一个FutureTask。return schedule(new ScheduledFutureTask<Void>(this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
}
// 这里就是讲定时任务添加到定时任务队列中。
<V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {// 首先判断调用这个schedule方法的线程是不是一个NioEventLoop线程。// 如果是的话,那么就直接讲task添加到定时任务队列中,否则就把添加定时任务也当做一个普通任务来进行,// 因为这个定时任务队列它并不是线程安全的。因为NioEventLoop与线程是一对一的关系,// 所以放在EventLoop的线程中执行是一个单线程操作,这样就不会有线程安全问题。不仅仅是添加,所有关于// 定时任务队列相关的操作,都会放到NioEventLoop中去执行,用来保证线程安全。if (inEventLoop()) {scheduledTaskQueue().add(task);} else {execute(new Runnable() {@Overridepublic void run() {scheduledTaskQueue().add(task);}});}return task;
}

我们上面分析了,定时任务的添加这一步骤,需要保证在NioEventLoop的线程中进行,为了保证其线程安全。如果不是在,那么把这个操作当成一个普通任务的形式,通过execute方法定时任务添加到定时任务队列中。那么到这里,我们分析了俩种定时任务的添加,下面我们来分析netty怎么去把这俩种任务聚合在一起。

俩种任务的聚合与执行

还记得netty在执行处理IO事件前,记录了当前时间戳,我们一起来看看之前的代码吧:

// NioEventLoop.java 中的run方法:
cancelledKeys = 0;
needsToSelectAgain = false;
//这个默认是50
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {try {processSelectedKeys();} finally {// Ensure we always run tasks.runAllTasks();}
} else {// 这里记录了一个开始时间final long ioStartTime = System.nanoTime();try {//执行处理IO事件processSelectedKeys();} finally {// Ensure we always run tasks.// 获取处理IO事件花费的时间。final long ioTime = System.nanoTime() - ioStartTime;// 这里表示,执行任务的时间不能超过 ioTime * (100 - ioRatio) / ioRatio runAllTasks(ioTime * (100 - ioRatio) / ioRatio);}
}// SingleThreadEventExecutor.java
// 任务的聚合。
private boolean fetchFromScheduledTaskQueue() {// 这里从定时任务队列中获取第一个任务。这个定时任务列队是根据优先级来的,// 因为这个定时任务队列,要么就是可以传入元素对应的比较器,要么就是这个元素本身实现了比较器。// 你可以去查看一下这个队列源码,其实很简单。比较规则在ScheduledFutureTask中的compareTo方法,// 具体的比较规则就是先比较截止时间,截止时间小的的在前面,// 截止时间小的在后面,如果截止时间相同,那么就比较id,id小的在前面,id大的在后面,如果相同,就抛异常。long nanoTime = AbstractScheduledEventExecutor.nanoTime();Runnable scheduledTask  = pollScheduledTask(nanoTime);// 这里就比较好理解了,循环取出定时任务,往taskQueue中添加。while (scheduledTask != null) {if (!taskQueue.offer(scheduledTask)) {// No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);return false;}scheduledTask  = pollScheduledTask(nanoTime);}return true;
}// AbstractEventExecutor.java
protected static void safeExecute(Runnable task) {try {// 这里直接去执行run方法。出现异常也并没有做过多处理,只是将异常信息打印出来。// 异常内部消化了,所以即使某个任务出现了异常,也会保证后续任务不会受到干扰,// 能正常的执行下去。task.run();} catch (Throwable t) {logger.warn("A task raised an exception. Task: {}", task, t);}
}
//SingleThreadEventExecutor.javaprotected boolean runAllTasks(long timeoutNanos) {// 这里就是任务的聚合fetchFromScheduledTaskQueue();Runnable task = pollTask();if (task == null) {afterRunningAllTasks();return false;}// 计算出超时时间final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;long runTasks = 0;long lastExecutionTime;// 然后循环去执行任务。for (;;) {safeExecute(task);runTasks ++;// Check timeout every 64 tasks because nanoTime() is relatively expensive.// XXX: Hard-coded value - will make it configurable if it is really a problem.// 当任务执行到64次的时候,就去判断是否超时,这里为啥要等64次呢,上面也解释了,// 因为这个nanoTime他也是一个耗时操作,如果每次执行完都检测,那么将影响效率。// 其实这里的超时时间并不保证执行时间必须要小于这个超时时间,只是尽可能的保证不超过超市时间。if ((runTasks & 0x3F) == 0) {lastExecutionTime = ScheduledFutureTask.nanoTime();if (lastExecutionTime >= deadline) {break;}}// 重新从任务队列中拿到这个任务。task = pollTask();// 如果队列里面没有任务了, 那么就记录当前时间,赋值给最后执行时间变量。if (task == null) {lastExecutionTime = ScheduledFutureTask.nanoTime();break;}}// 然后任务执行后续操作。afterRunningAllTasks();this.lastExecutionTime = lastExecutionTime;return true;
}

那么到这里,我们就分析了俩种任务的合并与执行。我们来总结一下这个流程netty做了什么事情吧~

首先,在netty调用自己的select()方法后,会去执行runTask();方法,执行任务队列中的任务,这一步又细分了俩个步骤,第一步,是将俩种任务进行合并,遍历定时任务队列,将定时任务添加到普通任务队列中。添加完成后,根据处理IO时间的时间,然后做一个计算,得出他的一个任务队列执行的一个超时时间,也就是在执行这些任务期间,尽可能不能超过这个超时时间,每过64次去检测一次,至于为啥不是每次去检测一次,因为ScheduledFutureTask.nanoTime();这个方法是一个比较耗时操作,所以不应该每次执行完一个任务就去检测。执行完后,会去记录最后的一个截止时间。

总结

到这里我们就分析完了NioEventLoop一些流程,比如从我们的NioEventLoop的创建,到启动,然后从NioEventLoop怎么去检测与处理IO事件、执行任务队列。这一篇文章的知识点很多,也需要我们自己多动手,仔细跟踪一下源码,进行巩固。

那么我们前面的三个问题通过这篇文章就可以进行解答了。

第一个,默认创建的是CPU核数*2的线程个数。在我们调用execute方法时,先去判断线程是否开启,如果没有开启,那么就去创建一个新的线程,如果开启了,那么就将当前的任务添加到任务队列中等待去执行。

第二个问题,netty是如何解决java nio 空轮训的BUG的。首先出现空轮训的bug的原因,就是当前这个selector出现了问题,可能是网络连接发生了故障,比如出现了RST,然后底层就会触发网络中断,从而唤醒阻塞的java线程,也就是java nio 的阻塞select,但是返回的标识位还是0,跟正常模式一样,这就导致了用户程序无法判断到底是没有IO事件还是因为发生了RST,所以一般我们是select返回值不为0,我们才结束轮训select,否则就继续轮训select,如果发生了RST,那么此时的select并不是一个阻塞的了,他会立马返回,因为它一调用底层的poll 或者epoll方法,就会立马触发网络中断,从而返回,这样就代码就等价于while(true){} 从而造成了空轮训,导致CPU飙升100%。然后netty的解决办法也很巧妙,因为它对select有时间限制,所以它最多阻塞timeoutMillis,本来要阻塞这么久,但是如果我发现它并没有阻塞这么久,并且这种情况达到了一定次数,那么netty就认为这个连接可能出现了问题,也就是这个selector出现了问题,那么就把这个selector上的东西重新注册到一个新的selector上,这样就可以避免空轮训的出现。

第三个问题,netty就是在执行任务的时候,首先判断是否是在NioEventLoop的内部线程中,如果不是,那么就把任务封装成一个netty自己的task,然后丢到任务队列中,等待select执行结束,就会去执行这个任务队列。

微信公众号:码农小谭,一个热爱coding、生活、分享、探讨的打工人,如果我的文章对你有帮助,麻烦给个关注吧~

扫描下方二维码即可关注公众号~

公众号分享技术博文、生活百事、欢迎关注~

资料获取方式,无任何套路,也不需要解压验证码,如下:

需要java相关资料请回复【java】

需要数据库相关资料请回复【数据库】

需要计算机网络相关资料请回复【计算机网络】

需要操作系统相关资料请回复【操作系统】

需要算法相关资料请回复【算法】

如果资料链接失效,请点击联系作者添加微信,第一时间会进行更新。

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 21-11-07周总结

    11.07周总结前言一、未来五组总结二、周总结前言 又来到十一月份了&#xff0c;去年的这个时候我可能刚刚拿到新的电脑&#xff0c;刚刚开始新的考核&#xff0c;我无论如何也不会想到&#xff0c;我已经在未来的一年中已经开始走上程序员的道路了。更为幸运的是&#xff0c;我…...

    2024/4/14 20:12:21
  2. Linux网络安全配置

    文章目录网络参数设置禁用IP转发禁止数据包发送重定向不接收源路由信息包记录可疑数据包启用TCO SYN Cookies禁用ipv6网络访问控制TCP-Wrappers禁用不常用的网络协议禁用DCCP/SCTP/RDS协议防火墙配置-[iptables](https://www.cnblogs.com/vathe/p/6973656.html)默认防火墙拒绝策…...

    2024/4/17 23:48:58
  3. 量子信息基础:数学基础

    向量空间和希尔伯特空间 希尔伯特空间是欧几里得空间的推广&#xff0c;不再局限于有限维的情形&#xff0c;即希尔伯特空间是无穷维的欧几里得空间。 狄拉克符号 量子力学中的一个量子态可以用希尔伯特空间中的一个矢量来标记&#xff0c;而力学量对应线性厄米算符。也就是…...

    2024/4/18 22:13:07
  4. 查看MySql版本号命令

    在命令行登录mysql即可查看...

    2024/4/18 23:37:46
  5. 学习-Java循环do…while之前n个自然数平均值

    任务描述 本关任务&#xff1a;给定一个大于 1 的正整数 n&#xff0c;求所有小于 n 的自然数的平均值&#xff08;保留两位小数&#xff09;。 自然数的定义&#xff1a;大于等于 0 的整数。 相关知识 当我们想要对同样的操作执行多次&#xff0c;就需要使用到循环结构&…...

    2024/5/4 21:31:16
  6. L1-052 2018我们要赢 (5 分)

    L1-052 2018我们要赢 (5 分) 2018年天梯赛的注册邀请码是“2018wmyy”&#xff0c;意思就是“2018我们要赢”。本题就请你用汉语拼音输出这句话。 输入格式&#xff1a; 本题没有输入。 输出格式&#xff1a; 在第一行中输出&#xff1a;“2018”&#xff1b;第二行中输出&am…...

    2024/4/14 20:12:11
  7. 团体程序设计天梯赛-练习集 L1-012 计算指数 (5 分)

    真的没骗你&#xff0c;这道才是简单题 —— 对任意给定的不超过 10 的正整数 n&#xff0c;要求你输出 2n。不难吧&#xff1f; 输入格式&#xff1a; 输入在一行中给出一个不超过 10 的正整数 n。 输出格式&#xff1a; 在一行中按照格式 2^n 计算结果 输出 2n 的值。 …...

    2024/4/19 0:43:58
  8. zsh和bash环境变量问题

    这里写自定义目录标题zsh和bash环境变量问题zsh和bash环境变量问题 https://www.jianshu.com/p/4a8f04155e90...

    2024/4/14 20:12:16
  9. 用scrapy-redis分布式框架采集站点的漂亮图片,代理服务器,随机UA技术

    分享点以前做的项目&#xff0c;攒点项目经验 前面讲了怎么做scrapy-redis的配置&#xff0c;这次做个采集的实例 网站是彼岸图网&#xff1a;https://pic.netbian.com/4kmeinv/ 分析 采集上面这个链接分类下的所有图片&#xff0c;分析网站是148页&#xff0c;读取下一页的链…...

    2024/4/7 3:24:13
  10. L1-046 整除光棍 (20 分)

    L1-046 整除光棍 (20 分) 这里所谓的“光棍”&#xff0c;并不是指单身汪啦~ 说的是全部由1组成的数字&#xff0c;比如1、11、111、1111等。传说任何一个光棍都能被一个不以5结尾的奇数整除。比如&#xff0c;111111就可以被13整除。 现在&#xff0c;你的程序要读入一个整数x…...

    2024/4/14 20:12:06
  11. Echarts中Option属性设置,Java详解

    // width: 50, // 文字块的宽度。 默认 // height: 40, // 文字块的高度 默认 textBorderColor: “transparent”, // 文字本身的描边颜色。 textBorderWidth: 0, // 文字本身的描边宽度。 textShadowColor: “transparent”, // 文字本身的阴影颜色。 textShadowBlur: 0,…...

    2024/4/5 5:32:17
  12. C# StringBuilder

    StringBuilder适用于需要经常变化的字符串&#xff0c;普通的String类型在对它进行修改的时候&#xff0c;看似是对同一个字符串进行修改&#xff0c;其实它的本质是在内存另外开辟一块空间用作修改后的内容的存储&#xff0c;所以在用到需要经常修改的字符串的时候&#xff0c…...

    2024/4/20 12:54:02
  13. Java实现XSS防御

    XSS概述 跨站脚本攻击(Cross Site Scripting)&#xff0c;缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码&#xff0c;当用户浏览该页之时&#xff0c;嵌入其中Web里面的Script代码会被执行&#xff0c;从而达到恶意攻击用户的目的。 Servlet的方式 1、继承HttpServle…...

    2024/4/14 20:13:02
  14. 各式轮播效果

    轮播效果是一个幻灯片效果&#xff0c;使用CSS 3D变形转换和一些JAvaScript构建一内容循环播放&#xff0c;它适用于一系列图像、文本或自定义标记&#xff0c;还包括对上一个/下一个图的浏览控制和指令支持。通过 .carousel 命名样式引入轮播组件&#xff0c;同时为此控件设置…...

    2024/4/14 20:12:57
  15. shardingjdbc (三) 标准分片算法

    标准分片算法包括 行表达式分片算法 和 时间范围分片算法; 行表达式分片算法 type:INLINE props: algorithm-expression:表达式 allow-range-query-with-inline-sharding :true 表示是否允许范围查询 提供了简单的单分片键的&#xff0c;基于goovy 表达式的inline 配置语句; …...

    2024/4/14 20:13:17
  16. 如何在Mysql的Docker容器启动时初始化数据库

    1. 前言 Docker在开发中使用的越来越多了&#xff0c;最近搞了一个Spring Boot应用&#xff0c;为了方便部署将Mysql也放在Docker中运行。那么怎么初始化 SQL脚本以及数据呢&#xff1f; 我这里有两个传统方案。第一种方案是在容器启动后手动导入&#xff0c;太 low 了不行。…...

    2024/4/14 20:13:17
  17. 数据统计与分析基础——实验四 机器学习算法建模与求解

    实验目的&#xff1a;掌握使用实用软件通过各类基础的机器学习算法解决实际数据统计分析任务的能力&#xff0c;熟悉线性回归、SVM、kmeans、PCA等算法的调用。 实验工具&#xff1a;python 实验内容&#xff1a; 1、对于下表中的数据&#xff0c;对1990年-2005年内的数据建立…...

    2024/4/18 19:43:21
  18. application context not configured for this file?

    意思是新建的Spring配置文件没有被加入到spring里面 就可以了...

    2024/4/16 3:39:36
  19. 程序命名规则——没有让所有程序员都满意的命名规则

    1、头文件命名大小写问题。2、变量命名规范 1、 头文件的命名一般全都是小写。 虽然Unix区分大小写&#xff0c;但windows是不区分大小写&#xff0c;所以为了方便&#xff0c;头文件全是小写命名。 2、​​​​​​​ C语言规定变量由字母、数字、下划线组成&#xff0c;且…...

    2024/4/14 20:12:57
  20. keil报*** WARNING L5: CODE SPACE MEMORY OVERLAP警告信息原因

    keil报*** WARNING L5: CODE SPACE MEMORY OVERLAP警告信息 仔细检查所使用的中断服务程序后面的中断号&#xff0c;是否重叠。 修改后 各中断标志号 各中断标志位...

    2024/4/15 10:51:23

最新文章

  1. Windows API函数之文件类函数(一)

    三. API之文件处理函数 3.1 函数名&#xff1a;CloseHandle &#xff08;1&#xff09;函数的概述 CloseHandle是一个Windows API函数&#xff0c;用于关闭一个打开的句柄。句柄是Windows中用于标识各种资源&#xff08;如文件、设备、内存映射等&#xff09;的抽象标识符。Cl…...

    2024/5/5 14:43:35
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. UE5、CesiumForUnreal实现加载建筑轮廓GeoJson数据生成白模功能

    1.实现目标 在UE5.3中,通过加载本地建筑边界轮廓面GeoJson数据,获取底面轮廓和楼高数据,拉伸生成白模,并支持点选高亮。为防止阻塞Game线程,使用了异步任务进行优化,GIF动图如下所示: 其中建筑数量:128871,顶点索引数量:6695748,三角面数量:2231916,顶点数量:165…...

    2024/5/4 6:55:48
  4. jQuery(一)

    文章目录 1. 基本介绍2.原理示意图3.快速入门1.下载jQuery2.创建文件夹&#xff0c;放入jQuery3.引入jQuery4.代码实例 4.jQuery对象与DOM对象转换1.基本介绍2.dom对象转换JQuery对象3.JQuery对象转换dom对象4.jQuery对象获取数据获取value使用val&#xff08;&#xff09;获取…...

    2024/5/4 22:17:11
  5. 【蓝桥杯】省模拟赛

    题目 1.奇数次数2.最小步数3.最大极小值和最小极大值 1.奇数次数 问题描述 给定一个仅包含数字字符的字符串&#xff0c;统计一下这个字符串中出现了多少个值为奇数的数位。 输入格式 输入一行包含一个字符串&#xff0c;仅由数字字符组成。 输出格式 输出一行包含一个整数&am…...

    2024/5/1 19:23:28
  6. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/4 23:54:56
  7. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/4 23:54:56
  8. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  9. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/4 23:55:17
  10. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  11. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  12. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  13. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/4 23:55:16
  14. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  15. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/4 18:20:48
  16. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  17. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/4 23:55:17
  18. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/4 23:55:06
  19. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  20. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/4 23:55:06
  21. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/5 8:13:33
  22. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/4 23:55:16
  23. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  24. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/4 23:55:01
  25. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57