考虑这样一个场景,现在有5000个任务,要让这5000个任务每隔5分中触发某个操作,怎么去实现这个需求。大部分人首先想到的是使用定时器,但是5000个任务,你就要用5000个定时器,一个定时器就是一个线程,你懂了吧,这种方法肯定是不行的。
针对这个场景,催生了时间轮算法,时间轮到底是什么?我一贯的风格,自行谷歌去。大发慈悲,发个时间轮介绍你们看看,看文字和图就好了,代码不要看了,那个文章里的代码运行不起来,时间轮介绍。
看好了介绍,我们就开始动手吧。
开发环境:idea + jdk1.8 + maven
新建一个maven工程

创建如下的目录结构

不要忘了pom.xml中添加netty库

<dependencies><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.5.Final</version></dependency></dependencies>

代码如下
Timeout.java

package com.tanghuachun.timer;
public interface Timeout {Timer timer();TimerTask task();boolean isExpired();boolean isCancelled();boolean cancel();
}

Timer.java

package com.tanghuachun.timer;
import java.util.Set;
import java.util.concurrent.TimeUnit;public interface Timer {Timeout newTimeout(TimerTask task, long delay, TimeUnit unit, String argv);Set<Timeout> stop();
}

TimerTask.java

package com.tanghuachun.timer;
public interface TimerTask {void run(Timeout timeout, String argv) throws Exception;
}

TimerWheel.java

/** Copyright 2012 The Netty Project** The Netty Project licenses this file to you under the Apache License,* version 2.0 (the "License"); you may not use this file except in compliance* with the License. You may obtain a copy of the License at:**   http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the* License for the specific language governing permissions and limitations* under the License.*/
package com.tanghuachun.timer;
import io.netty.util.*;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;public class TimerWheel implements Timer {static final InternalLogger logger =InternalLoggerFactory.getInstance(TimerWheel.class);private static final ResourceLeakDetector<TimerWheel> leakDetector = ResourceLeakDetectorFactory.instance().newResourceLeakDetector(TimerWheel.class, 1, Runtime.getRuntime().availableProcessors() * 4L);private static final AtomicIntegerFieldUpdater<TimerWheel> WORKER_STATE_UPDATER;static {AtomicIntegerFieldUpdater<TimerWheel> workerStateUpdater =PlatformDependent.newAtomicIntegerFieldUpdater(TimerWheel.class, "workerState");if (workerStateUpdater == null) {workerStateUpdater = AtomicIntegerFieldUpdater.newUpdater(TimerWheel.class, "workerState");}WORKER_STATE_UPDATER = workerStateUpdater;}private final ResourceLeak leak;private final Worker worker = new Worker();private final Thread workerThread;public static final int WORKER_STATE_INIT = 0;public static final int WORKER_STATE_STARTED = 1;public static final int WORKER_STATE_SHUTDOWN = 2;@SuppressWarnings({ "unused", "FieldMayBeFinal", "RedundantFieldInitialization" })private volatile int workerState = WORKER_STATE_INIT; // 0 - init, 1 - started, 2 - shut downprivate final long tickDuration;private final HashedWheelBucket[] wheel;private final int mask;private final CountDownLatch startTimeInitialized = new CountDownLatch(1);private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();private final Queue<HashedWheelTimeout> cancelledTimeouts = PlatformDependent.newMpscQueue();private volatile long startTime;/*** Creates a new timer with the default thread factory* ({@link Executors#defaultThreadFactory()}), default tick duration, and* default number of ticks per wheel.*/public TimerWheel() {this(Executors.defaultThreadFactory());}/*** Creates a new timer with the default thread factory* ({@link Executors#defaultThreadFactory()}) and default number of ticks* per wheel.** @param tickDuration   the duration between tick* @param unit           the time unit of the {@code tickDuration}* @throws NullPointerException     if {@code unit} is {@code null}* @throws IllegalArgumentException if {@code tickDuration} is &lt;= 0*/public TimerWheel(long tickDuration, TimeUnit unit) {this(Executors.defaultThreadFactory(), tickDuration, unit);}/*** Creates a new timer with the default thread factory* ({@link Executors#defaultThreadFactory()}).** @param tickDuration   the duration between tick* @param unit           the time unit of the {@code tickDuration}* @param ticksPerWheel  the size of the wheel* @throws NullPointerException     if {@code unit} is {@code null}* @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is &lt;= 0*/public TimerWheel(long tickDuration, TimeUnit unit, int ticksPerWheel) {this(Executors.defaultThreadFactory(), tickDuration, unit, ticksPerWheel);}/*** Creates a new timer with the default tick duration and default number of* ticks per wheel.** @param threadFactory  a {@link ThreadFactory} that creates a*                       background {@link Thread} which is dedicated to*                       {@link TimerTask} execution.* @throws NullPointerException if {@code threadFactory} is {@code null}*/public TimerWheel(ThreadFactory threadFactory) {this(threadFactory, 100, TimeUnit.MILLISECONDS);}/*** Creates a new timer with the default number of ticks per wheel.** @param threadFactory  a {@link ThreadFactory} that creates a*                       background {@link Thread} which is dedicated to*                       {@link TimerTask} execution.* @param tickDuration   the duration between tick* @param unit           the time unit of the {@code tickDuration}* @throws NullPointerException     if either of {@code threadFactory} and {@code unit} is {@code null}* @throws IllegalArgumentException if {@code tickDuration} is &lt;= 0*/public TimerWheel(ThreadFactory threadFactory, long tickDuration, TimeUnit unit) {this(threadFactory, tickDuration, unit, 512);}/*** Creates a new timer.** @param threadFactory  a {@link ThreadFactory} that creates a*                       background {@link Thread} which is dedicated to*                       {@link TimerTask} execution.* @param tickDuration   the duration between tick* @param unit           the time unit of the {@code tickDuration}* @param ticksPerWheel  the size of the wheel* @throws NullPointerException     if either of {@code threadFactory} and {@code unit} is {@code null}* @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is &lt;= 0*/public TimerWheel(ThreadFactory threadFactory,long tickDuration, TimeUnit unit, int ticksPerWheel) {this(threadFactory, tickDuration, unit, ticksPerWheel, true);}/*** Creates a new timer.** @param threadFactory  a {@link ThreadFactory} that creates a*                       background {@link Thread} which is dedicated to*                       {@link TimerTask} execution.* @param tickDuration   the duration between tick* @param unit           the time unit of the {@code tickDuration}* @param ticksPerWheel  the size of the wheel* @param leakDetection  {@code true} if leak detection should be enabled always, if false it will only be enabled*                       if the worker thread is not a daemon thread.* @throws NullPointerException     if either of {@code threadFactory} and {@code unit} is {@code null}* @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is &lt;= 0*/public TimerWheel(ThreadFactory threadFactory,long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection) {if (threadFactory == null) {throw new NullPointerException("threadFactory");}if (unit == null) {throw new NullPointerException("unit");}if (tickDuration <= 0) {throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);}if (ticksPerWheel <= 0) {throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);}// Normalize ticksPerWheel to power of two and initialize the wheel.wheel = createWheel(ticksPerWheel);mask = wheel.length - 1;// Convert tickDuration to nanos.this.tickDuration = unit.toNanos(tickDuration);// Prevent overflow.if (this.tickDuration >= Long.MAX_VALUE / wheel.length) {throw new IllegalArgumentException(String.format("tickDuration: %d (expected: 0 < tickDuration in nanos < %d",tickDuration, Long.MAX_VALUE / wheel.length));}workerThread = threadFactory.newThread(worker);leak = leakDetection || !workerThread.isDaemon() ? leakDetector.open(this) : null;}private static HashedWheelBucket[] createWheel(int ticksPerWheel) {if (ticksPerWheel <= 0) {throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);}if (ticksPerWheel > 1073741824) {throw new IllegalArgumentException("ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);}ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];for (int i = 0; i < wheel.length; i ++) {wheel[i] = new HashedWheelBucket();}return wheel;}private static int normalizeTicksPerWheel(int ticksPerWheel) {int normalizedTicksPerWheel = 1;while (normalizedTicksPerWheel < ticksPerWheel) {normalizedTicksPerWheel <<= 1;}return normalizedTicksPerWheel;}/*** Starts the background thread explicitly.  The background thread will* start automatically on demand even if you did not call this method.** @throws IllegalStateException if this timer has been*                               {@linkplain #stop() stopped} already*/public void start() {switch (WORKER_STATE_UPDATER.get(this)) {case WORKER_STATE_INIT:if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {workerThread.start();}break;case WORKER_STATE_STARTED:break;case WORKER_STATE_SHUTDOWN:throw new IllegalStateException("cannot be started once stopped");default:throw new Error("Invalid WorkerState");}// Wait until the startTime is initialized by the worker.while (startTime == 0) {try {startTimeInitialized.await();} catch (InterruptedException ignore) {// Ignore - it will be ready very soon.}}}@Overridepublic Set<Timeout> stop() {if (Thread.currentThread() == workerThread) {throw new IllegalStateException(TimerWheel.class.getSimpleName() +".stop() cannot be called from " +TimerTask.class.getSimpleName());}if (!WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_STARTED, WORKER_STATE_SHUTDOWN)) {// workerState can be 0 or 2 at this moment - let it always be 2.WORKER_STATE_UPDATER.set(this, WORKER_STATE_SHUTDOWN);if (leak != null) {leak.close();}return Collections.emptySet();}boolean interrupted = false;while (workerThread.isAlive()) {workerThread.interrupt();try {workerThread.join(100);} catch (InterruptedException ignored) {interrupted = true;}}if (interrupted) {Thread.currentThread().interrupt();}if (leak != null) {leak.close();}return worker.unprocessedTimeouts();}@Overridepublic Timeout newTimeout(TimerTask task, long delay, TimeUnit unit, String argv) {if (task == null) {throw new NullPointerException("task");}if (unit == null) {throw new NullPointerException("unit");}start();// Add the timeout to the timeout queue which will be processed on the next tick.// During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline, argv);timeouts.add(timeout);return timeout;}private final class Worker implements Runnable {private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();private long tick;@Overridepublic void run() {// Initialize the startTime.startTime = System.nanoTime();if (startTime == 0) {// We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.startTime = 1;}// Notify the other threads waiting for the initialization at start().startTimeInitialized.countDown();do {final long deadline = waitForNextTick();if (deadline > 0) {int idx = (int) (tick & mask);processCancelledTasks();HashedWheelBucket bucket =wheel[idx];transferTimeoutsToBuckets();bucket.expireTimeouts(deadline);tick++;}} while (WORKER_STATE_UPDATER.get(TimerWheel.this) == WORKER_STATE_STARTED);// Fill the unprocessedTimeouts so we can return them from stop() method.for (HashedWheelBucket bucket: wheel) {bucket.clearTimeouts(unprocessedTimeouts);}for (;;) {HashedWheelTimeout timeout = timeouts.poll();if (timeout == null) {break;}if (!timeout.isCancelled()) {unprocessedTimeouts.add(timeout);}}processCancelledTasks();}private void transferTimeoutsToBuckets() {// transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just// adds new timeouts in a loop.for (int i = 0; i < 100000; i++) {HashedWheelTimeout timeout = timeouts.poll();if (timeout == null) {// all processedbreak;}if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {// Was cancelled in the meantime.continue;}long calculated = timeout.deadline / tickDuration;timeout.remainingRounds = (calculated - tick) / wheel.length;final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.int stopIndex = (int) (ticks & mask);HashedWheelBucket bucket = wheel[stopIndex];bucket.addTimeout(timeout);}}private void processCancelledTasks() {for (;;) {HashedWheelTimeout timeout = cancelledTimeouts.poll();if (timeout == null) {// all processedbreak;}try {timeout.remove();} catch (Throwable t) {if (logger.isWarnEnabled()) {logger.warn("An exception was thrown while process a cancellation task", t);}}}}/*** calculate goal nanoTime from startTime and current tick number,* then wait until that goal has been reached.* @return Long.MIN_VALUE if received a shutdown request,* current time otherwise (with Long.MIN_VALUE changed by +1)*/private long waitForNextTick() {long deadline = tickDuration * (tick + 1);for (;;) {final long currentTime = System.nanoTime() - startTime;long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;if (sleepTimeMs <= 0) {if (currentTime == Long.MIN_VALUE) {return -Long.MAX_VALUE;} else {return currentTime;}}// Check if we run on windows, as if thats the case we will need// to round the sleepTime as workaround for a bug that only affect// the JVM if it runs on windows.//// See https://github.com/netty/netty/issues/356if (PlatformDependent.isWindows()) {sleepTimeMs = sleepTimeMs / 10 * 10;}try {Thread.sleep(sleepTimeMs);} catch (InterruptedException ignored) {if (WORKER_STATE_UPDATER.get(TimerWheel.this) == WORKER_STATE_SHUTDOWN) {return Long.MIN_VALUE;}}}}public Set<Timeout> unprocessedTimeouts() {return Collections.unmodifiableSet(unprocessedTimeouts);}}private static final class HashedWheelTimeout implements Timeout {private static final int ST_INIT = 0;private static final int ST_CANCELLED = 1;private static final int ST_EXPIRED = 2;private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER;static {AtomicIntegerFieldUpdater<HashedWheelTimeout> updater =PlatformDependent.newAtomicIntegerFieldUpdater(HashedWheelTimeout.class, "state");if (updater == null) {updater = AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");}STATE_UPDATER = updater;}private final TimerWheel timer;private final TimerTask task;private final long deadline;@SuppressWarnings({"unused", "FieldMayBeFinal", "RedundantFieldInitialization" })private volatile int state = ST_INIT;// remainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the// HashedWheelTimeout will be added to the correct HashedWheelBucket.long remainingRounds;String argv;// This will be used to chain timeouts in HashedWheelTimerBucket via a double-linked-list.// As only the workerThread will act on it there is no need for synchronization / volatile.HashedWheelTimeout next;HashedWheelTimeout prev;// The bucket to which the timeout was addedHashedWheelBucket bucket;HashedWheelTimeout(TimerWheel timer, TimerTask task, long deadline, String argv) {this.timer = timer;this.task = task;this.deadline = deadline;this.argv = argv;}@Overridepublic Timer timer() {return timer;}@Overridepublic TimerTask task() {return task;}@Overridepublic boolean cancel() {// only update the state it will be removed from HashedWheelBucket on next tick.if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {return false;}// If a task should be canceled we put this to another queue which will be processed on each tick.// So this means that we will have a GC latency of max. 1 tick duration which is good enough. This way// we can make again use of our MpscLinkedQueue and so minimize the locking / overhead as much as possible.timer.cancelledTimeouts.add(this);return true;}void remove() {HashedWheelBucket bucket = this.bucket;if (bucket != null) {bucket.remove(this);}}public boolean compareAndSetState(int expected, int state) {return STATE_UPDATER.compareAndSet(this, expected, state);}public int state() {return state;}@Overridepublic boolean isCancelled() {return state() == ST_CANCELLED;}@Overridepublic boolean isExpired() {return state() == ST_EXPIRED;}public void expire() {if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {return;}try {task.run(this, argv);} catch (Throwable t) {if (logger.isWarnEnabled()) {logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);}}}@Overridepublic String toString() {final long currentTime = System.nanoTime();long remaining = deadline - currentTime + timer.startTime;StringBuilder buf = new StringBuilder(192).append(StringUtil.simpleClassName(this)).append('(').append("deadline: ");if (remaining > 0) {buf.append(remaining).append(" ns later");} else if (remaining < 0) {buf.append(-remaining).append(" ns ago");} else {buf.append("now");}if (isCancelled()) {buf.append(", cancelled");}return buf.append(", task: ").append(task()).append(')').toString();}}/*** Bucket that stores HashedWheelTimeouts. These are stored in a linked-list like datastructure to allow easy* removal of HashedWheelTimeouts in the middle. Also the HashedWheelTimeout act as nodes themself and so no* extra object creation is needed.*/private static final class HashedWheelBucket {// Used for the linked-list datastructureprivate HashedWheelTimeout head;private HashedWheelTimeout tail;/*** Add {@link HashedWheelTimeout} to this bucket.*/public void addTimeout(HashedWheelTimeout timeout) {assert timeout.bucket == null;timeout.bucket = this;if (head == null) {head = tail = timeout;} else {tail.next = timeout;timeout.prev = tail;tail = timeout;}}/*** Expire all {@link HashedWheelTimeout}s for the given {@code deadline}.*/public void expireTimeouts(long deadline) {HashedWheelTimeout timeout = head;// process all timeoutswhile (timeout != null) {boolean remove = false;if (timeout.remainingRounds <= 0) {if (timeout.deadline <= deadline) {timeout.expire();} else {// The timeout was placed into a wrong slot. This should never happen.throw new IllegalStateException(String.format("timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));}remove = true;} else if (timeout.isCancelled()) {remove = true;} else {timeout.remainingRounds --;}// store reference to next as we may null out timeout.next in the remove block.HashedWheelTimeout next = timeout.next;if (remove) {remove(timeout);}timeout = next;}}public void remove(HashedWheelTimeout timeout) {HashedWheelTimeout next = timeout.next;// remove timeout that was either processed or cancelled by updating the linked-listif (timeout.prev != null) {timeout.prev.next = next;}if (timeout.next != null) {timeout.next.prev = timeout.prev;}if (timeout == head) {// if timeout is also the tail we need to adjust the entry tooif (timeout == tail) {tail = null;head = null;} else {head = next;}} else if (timeout == tail) {// if the timeout is the tail modify the tail to be the prev node.tail = timeout.prev;}// null out prev, next and bucket to allow for GC.timeout.prev = null;timeout.next = null;timeout.bucket = null;}/*** Clear this bucket and return all not expired / cancelled {@link Timeout}s.*/public void clearTimeouts(Set<Timeout> set) {for (;;) {HashedWheelTimeout timeout = pollTimeout();if (timeout == null) {return;}if (timeout.isExpired() || timeout.isCancelled()) {continue;}set.add(timeout);}}private HashedWheelTimeout pollTimeout() {HashedWheelTimeout head = this.head;if (head == null) {return null;}HashedWheelTimeout next = head.next;if (next == null) {tail = this.head =  null;} else {this.head = next;next.prev = null;}// null out prev and next to allow for GC.head.next = null;head.prev = null;head.bucket = null;return head;}}
}

编写测试类Main.java

package com.tanghuachun.timer;
import java.util.concurrent.TimeUnit;/*** Created by darren on 2016/11/17.*/
public class Main implements TimerTask{final static Timer timer = new TimerWheel();public static void main(String[] args) {TimerTask timerTask = new Main();for (int i = 0; i < 10; i++) {timer.newTimeout(timerTask, 5, TimeUnit.SECONDS, "" + i );}}@Overridepublic void run(Timeout timeout, String argv) throws Exception {System.out.println("timeout, argv = " + argv );}
}

然后就可以看到运行结果啦。
工程代码下载(以maven的方式导入)。

Java时间轮算法的实现相关推荐

  1. java 时间轮算法_时间轮算法(TimingWheel)是如何实现的?

    前言 我在2. SOFAJRaft源码分析-JRaft的定时任务调度器是怎么做的?这篇文章里已经讲解过时间轮算法在JRaft中是怎么应用的,但是我感觉我并没有讲解清楚这个东西,导致看了这篇文章依然和没 ...

  2. java 时间轮_基于时间轮的定时任务

    JobScheduleHelper是一个单列. 1-来看看类中的一些属性: public static final long PRE_READ_MS = 5000; // pre read priva ...

  3. java 时间轮算法_时间轮算法解析(Netty HashedWheelTimer源码解读)

    1.背景 时间轮算法可以用于高效的执行大量的定时任务. 在Netty中的一个典型应用场景是判断某个连接是否idle,如果idle(如客户端由于网络原因导致到服务器的心跳无法送达),则服务器会主动断开连 ...

  4. fp增长树java实现_FP-Tree算法的实现(Java版)

    在关联规则挖掘领域最经典的算法法是Apriori,其致命的缺点是需要多次扫描事务数据库.于是人们提出了各种裁剪(prune)数据集的方法以减少I/O开支,韩嘉炜老师的FP-Tree算法就是其中非常高效 ...

  5. 图文详解:java时间轮定时器

    一面问题:MySQL+Redis+Kafka+线程+算法 mysql知道哪些存储引擎,它们的区别 mysql索引在什么情况下会失效 mysql在项目中的优化场景,慢查询解决等 mysql有什么索引,索 ...

  6. Java一致性Hash算法的实现

    哈希hash hash的意思是散列,目的是将一组输入的数据均匀的分开.打散,往往用来配合路由算法做负载均衡,多用在分布式系统中.比如memcached,它只提供了K V的存储.读取,如果使用了多台me ...

  7. knn算法java版_KNN算法的实现详解

    #!/usr/bin/env python #-*-coding:utf-8-*- #knn 的具体实现 import csv import random import math import ope ...

  8. 53.垃圾回收算法的实现原理、启动Java垃圾回收、Java垃圾回收过程、垃圾回收中实例的终结、对象什么时候符合垃圾回收的条件、GC Scope 示例程序、GC OutOfMemoryError的示例

    53.垃圾回收算法的实现原理 53.1.目录 53.2.启动Java垃圾回收 53.3.Java垃圾回收过程 53.4.垃圾回收中实例的终结 53.5.对象什么时候符合垃圾回收的条件? 53.5.1. ...

  9. 时间轮-Java实现篇

    在前面的文章<时间轮-理论篇>讲了时间轮的一些理论知识,然后根据理论知识.我们自己来实现一个简单的时间轮. 1. 理论抽象 将时间轮的理论进行抽象,主要有两个方面: 时间轮的转动 每一个时 ...

最新文章

  1. g-gdb调试core文件
  2. python类实例_类和实例
  3. spring resttemplate 中文参数_SpringBoot使用RestTemplate访问第三方接口
  4. 老华为可以升级鸿蒙,真良心!五年前老机型都能升级华为鸿蒙
  5. 游戏角色坐标的保存间隔_游戏岗位看这里鸭——
  6. linux修改文件打开最大数(ulimit命令)
  7. 上采样层什么意思_佛前上三柱香、磕三个头是什么意思!
  8. 黑马程序员—18-oc协议与block
  9. Android Cursor浅析
  10. 计算机视觉论文-2021-08-03
  11. 获取中国各省市区县经纬度的方法
  12. 华为交换机常用查询命令
  13. webpack v4 常用配置(以配置vue ssr的webpack为例)
  14. 图片自适应手机横屏竖屏的宽高
  15. 马里兰大学本科计算机科学,2020年马里兰大学本科专业设置
  16. 打开Adobe Premiere Pro提示“由于找不到msvcp110.dll,无法继续执行代码。重新安装程序可能会解决此问题”。【方法适用于几乎所有dll文件丢失时】
  17. 出一本自己的书,是一种怎样的体验?
  18. 精伦身份证阅读器php_华视CVR-100身份证阅读器BS开发包
  19. Output输出缓存区大小只有1024KB,超过大小限制的就会被清除
  20. [原创] PS超写实:手绘铅笔

热门文章

  1. oracle create bigfile tablespace,create bigfile tablespace
  2. 微信小程序 星级评分(例如店铺小数点评分)
  3. Unity 获取 两个点的中心点
  4. vim的安装以及基础使用方法;
  5. iOS清除WKWebView缓存
  6. 故障:ID8003 的 bowser 错误日志
  7. 计算机初中教师教材知识测试,开县中小学教师新教材考试初中信息技术
  8. 王者荣耀最低战力查询-王者战区修改
  9. nginx配置https
  10. 交互式电子杂志_HUAWEI Magazine电子杂志阅读平台上线