【JUC源码解析】ThreadPoolExecutor
简介
ThreadPoolExecutor,线程池的基石。
概述
线程池,除了用HashSet承载一组线程做任务以外,还用BlockingQueue承载一组任务。corePoolSize和maximumPoolSize,分别表示线程弛里存活的最小和最大线程数目,keepAliveTime表示不干活的线程的存活时间。当过来一个任务时,如果线程池里的线程数目小于corePoolSize,那么直接创建一个线程去处理它;如果线程数目大于等corePoolSize并且小于maximumPoolSize,那么,将这个任务放进任务队列里;如果任务队列已满,则继续创建线程处理该任务;如果线程弛数目等于maximumPoolSize,此时任务队列肯定已经满了,那么采取饱和策略。如果线程池里的线程空闲时间达到keepAliveTime,并且数量大于corePoolSize,此刻任务队列肯定是空的,那么销毁该线程。
线程池的控制状态
// 线程池的控制状态,高3位表示运行状态,低29位表示线程的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // 29位的偏移量
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 最大容量 private static final int RUNNING = -1 << COUNT_BITS; // 正在运行,接受新任务,并且处理任务队列里的任务
private static final int SHUTDOWN = 0 << COUNT_BITS; // 关闭,不再接受新任务,但是处理队列里的任务
private static final int STOP = 1 << COUNT_BITS; // 停止,不再接受新任务,不处理任务队列里的任务,并且中断正在运行的任务
private static final int TIDYING = 2 << COUNT_BITS; // 整理中,所有的任务都已经停止,线程数为0,并调用terminate(钩子)方法
private static final int TERMINATED = 3 << COUNT_BITS; // 终止,terminate方法运行完毕 private static int runStateOf(int c) { // 获取线程池的运行状态
return c & ~CAPACITY;
} private static int workerCountOf(int c) { // 获取线程池的线程数量
return c & CAPACITY;
} private static int ctlOf(int rs, int wc) { // 反推线程池的控制状态
return rs | wc;
}
状态转换关系
RUNNING -> SHUTDOWN // shutdown()方法被调用
(RUNNING or SHUTDOWN) -> STOP // shutdownNow()方法被调用
SHUTDOWN -> TIDYING // 线程池和任务队列都为空
STOP -> TIDYING // 线程池为空
TIDYING -> TERMINATED // terminated()方法运行完毕
属性
private final BlockingQueue<Runnable> workQueue; // 任务队列
private final ReentrantLock mainLock = new ReentrantLock(); // 可重入锁
private final HashSet<Worker> workers = new HashSet<Worker>(); // 线程集合
private final Condition termination = mainLock.newCondition(); // 终止条件
private int largestPoolSize; // 最大线程池容量
private long completedTaskCount; // 已完成任务数量
private volatile ThreadFactory threadFactory; // 线程工厂
private volatile RejectedExecutionHandler handler; // 饱和策略
private volatile long keepAliveTime; // 线程等待时间
private volatile boolean allowCoreThreadTimeOut; // 是否允许核心线程超时
private volatile int corePoolSize; // 核心线程池大小
private volatile int maximumPoolSize; // 最大线程池大小
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); // 默认饱和策略
Worker
继承关系
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {}
属性
final Thread thread; // 承载的线程
Runnable firstTask; // 首任务
volatile long completedTasks; // 已完成任务数量
构造方法
Worker(Runnable firstTask) {
setState(-1); // 执行任务之前,禁止中断
this.firstTask = firstTask; // 初始化首任务
this.thread = getThreadFactory().newThread(this); // 初始化线程
}
主要方法
public void run() { // 重写Runnable的run方法
runWorker(this);
} protected boolean isHeldExclusively() { // 是否被独占,0否,1是
return getState() != 0;
} protected boolean tryAcquire(int unused) { // 尝试获取锁
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
} protected boolean tryRelease(int unused) { // 尝试释放锁
setExclusiveOwnerThread(null);
setState(0);
return true;
} public void lock() { // 获取锁
acquire(1);
} public boolean tryLock() { // 尝试获取锁
return tryAcquire(1);
} public void unlock() { // 释放锁
release(1);
} public boolean isLocked() { // 是否被独占
return isHeldExclusively();
} void interruptIfStarted() { // 中断
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {// 1. 状态大于等于0 2.线程不为空 3该线程没被中断
try {
t.interrupt(); // 中断
} catch (SecurityException ignore) {
}
}
}
}
execute(Runnable)
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get(); // 获取线程池的控制状态
if (workerCountOf(c) < corePoolSize) { // 1. worker数量小于corePoolSize,
// 创建线程
if (addWorker(command, true))
return; // 成功返回
c = ctl.get(); // 失败,再次获取控制状态(调用addWorker方法,该状态更改过)
}
if (isRunning(c) && workQueue.offer(command)) {// 2.查看线程池是否处于运行状态,是则说明worker数量不满足条件1,因此任务入队;
// 否则,线程池处于非运行状态,进入3(最后会reject);要么入队失败,也进入3(也许会成功)
int recheck = ctl.get(); // 再次获取控制状态,因为已经入队成功,万一状态改变,需要将任务出队(回滚)
if (!isRunning(recheck) && remove(command)) // 状态改变,回滚
reject(command); // 拒绝
else if (workerCountOf(recheck) == 0) // 入队成功,但线程池已空,此时需要创建线程处理它
addWorker(null, false); // 创建线程
} else if (!addWorker(command, false)) // 若添加失败,拒绝
reject(command);
}
状态再检查,进一步推广到变量可见性。1.两次使用之间,显式更新了变量,此时要重新获取,以便得到最新值。2.根据此变量的值,执行某项策略,需要回过头来再次检查,如果改变,则回滚。(乐观锁)
addWorker(Runnable, boolean)
private boolean addWorker(Runnable firstTask, boolean core) {
retry: for (;;) { // A. runState变化,重试
int c = ctl.get(); // ctl
int rs = runStateOf(c); // runState
// 等价于
// if (!(rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())))
// return false;
// 也即是
// if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
// 接着往下走:a. rs == RUNNING; 或者,b. rs == SHUTDOWN, 并且,firstTask == null, 再并且工作队列不为空
// a. 不必解释
// b. rs == SHUTDOWN时,此时,不再接受新任务,但是,工作队列里的任务还是要处理的,若是线程池里没有线程了,还是需要新增线程处理这些任务的
// 如何与由新任务发起的创建线程做区分呢?答案就是firstTask是否为null,不为null就是新任务发起的
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false; for (;;) { // B.workerCount变化,重试
int wc = workerCountOf(c);
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) // 校验workerCount
return false;
if (compareAndIncrementWorkerCount(c)) // 竞态点,可能失败:1.runState变化 -> A 2.workerCount变化 -> B
break retry;
c = ctl.get(); // 重新读取ctl
if (runStateOf(c) != rs) // 如果runState发生变化,转向A;否则,转向B
continue retry;
}
} boolean workerStarted = false; // 记录该线程是否已经启动
boolean workerAdded = false; // 记录是否添加成果
Worker w = null;
try {
w = new Worker(firstTask); // 新建工作者
final Thread t = w.thread; // 工作者线程
if (t != null) { // 若线程为空,直接失败
final ReentrantLock mainLock = this.mainLock; // 可重入锁
mainLock.lock(); // 因为要对workers操作,加锁
try {
int rs = runStateOf(ctl.get()); // 再次读取runState, 因为在此过程中,可能会有别的线程操作此变量
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { // C 第二次校验runState,与上面不同的是,这次没有校验工作队列是否为空
if (t.isAlive()) // 检验该线程是否已经启动,正常没有启动
throw new IllegalThreadStateException();
workers.add(w); // 添加到线程池里
int s = workers.size(); // 工作者的实际数量
if (s > largestPoolSize)
largestPoolSize = s; // 记录工作者最大数量
workerAdded = true; // 添加成功
}
} finally {
mainLock.unlock(); // 解锁,别的线程可以操作workers了
}
if (workerAdded) { // 如果添加成功
t.start(); // 启动线程
workerStarted = true; // 线程启动成功
}
}
} finally {
if (!workerStarted) // 如果线程启动失败
addWorkerFailed(w); // 回滚
}
return workerStarted; // 返回结果
}
代码注释C处,第二次校验runState时,为什么没有校验工作队列workQueue了呢?
考虑这样一种场景
- 线程池里的线程没有空闲的,都在工作,执行任务。
- 其中一个线程由于某种原因,跳出了while 循环。也许是某一时刻,workQueue里的任务被其他线程取空了,到此线程时,阻塞在workQueue.take()方法上了,而后又刚好遇到中断(各种原因),于是返回null并跳出了循环。
- 后来,workQueue又持续添加了新的任务,其他线程接着工作,中间没收到打扰。
- 接着,runState变为SHUTDOWN或者在第8步之后变为SHUTDOWN,此时还是RUNNING ,那个跳出循环的线程,我们暂且称为JUMP线程吧,走到addWork(null,false)方法。
- 在双重for循环检测时,恰巧满足条件
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
于是,会继续往下执行,此时,其他的线程仍在紧锣密鼓地工作。
- 注意,JUMP线程已经执行过tryTerminate()方法了,并且此线程马上就消亡了,调用addWork(null,false)方法也是为了创建新的线程替代它的。
- JUMP线程走到创建工作者那里时,也就是再次(第二次)检验runState的地方。
- 恰巧这时,workQueue被其他线程取空了,更巧的是,所有其他的线程都阻塞在了workQueue.take()方法上,从getTask() 方法逻辑可知,这是发生在runState变为SHUTDOWN之前,不然后面的线程会检查runState状态,直接return null了。
- 好了,时间停在了这一刻:JUMP线程准备第二次校验runState,其他线程阻塞在workQueue.take()方法上;不过,在runState变为SHUTDOWN时,也就是调用shutdown()方法,会调用tryTerminate()方法。
- 在tryTerminate()方法里,检查runState为SHUTDOWN,workQueue为空,但threadPool不为空,于是会走interruptIdleWorkers(ONLY_ONE)方法,随意中断一个空闲线程,注意,这里没有走到terminated()方法。
- 一个阻塞在workQueue.take()方法上的线程被唤醒,并跳出while循环,runWorker(Worker)里的逻辑,然后调用tryTerminate()方法,同步骤10,接着调用addWorker(null, false),当然在方法开始处,双重for循环那里就返回了,因为不满足条件,此刻workQueue已经为空。
- 就这样,10->11->10->11,一个接一个地调用,中断后面的阻塞着的线程并传播下去,直到最后一个,调用tryTerminate()方法,由于workCount不为0.,因为JUMP线程在双重for循环那里通过了检查,使得workCount加1了,于是,这最后一个被中断叫醒的线程也是走的第10步,只是没有空闲线程可以中断了。
- 最后,JUMP在第二次检查runState时,不应该再检查workQueue是否为空了,如果检查,由于workQueue为空,那么将会回滚,JUMP线程没有加入到threadPool里面去,那么便没有线程调用最终的terminated()方法了。
- 由于都是异步的,以上步骤并不具有严格的时间顺序。
- 第13步有误,即便由于检查了workQueue为空而回滚,也会调用terminated()方法的,因为回滚的时候会调用addWorkerFailed(Worker)方法,这个方法会调用tryTerminate()方法,因为此时满足了条件继而terminated()方法。
- 但是不检查workQueue是否为空也没错,因为两方面费的力气差不多,因此,只在必要的时候检查workQueue,就像第一次那样。
addWorkerFailed(Worker)
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w);
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}
runWorker(Worker w)
final void runWorker(Worker w) {
Thread wt = Thread.currentThread(); // 当前线程
Runnable task = w.firstTask; // 首任务
w.firstTask = null; // 置位
w.unlock(); // 允许中断(interruptIdleWorkers()方法),因为即便不unlock(),也阻止不了interruptWorkers()方法中断此线程
boolean completedAbruptly = true; // 是否是突然完成,即异常情况
try {
while (task != null || (task = getTask()) != null) { // 如果首任务不为空,执行首任务;否则,从任务队列里取任务
w.lock(); // 加锁,防止中断(interruptIdleWorkers()方法)
// 如果线程池停止了,中断此线程,否则,复位可能的中断,若此线程中断过,需要再次检查线程池是否停止
// 有可能这边刚把中断复位,那边就把线程池停止了
// !第一次检查线程池是否停止,是因为,线程池停止了,应该直接中断此线程
// !第二次检查线程池是否停止,是因为,如果线程被中断了,刚好把它复位,很有可能是前一瞬间线程池停止导致的中断,所以要再次确认线程池的状态
if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
&& !wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task); // 前置钩子
Throwable thrown = null;
try {
task.run(); // 执行任务
} catch (RuntimeException x) {
thrown = x;
throw x;
} catch (Error x) {
thrown = x;
throw x;
} catch (Throwable x) {
thrown = x;
throw new Error(x);
} finally {
afterExecute(task, thrown); // 后置钩子
}
} finally {
task = null;
w.completedTasks++;
w.unlock(); // 释放锁,允许中断(interruptIdleWorkers()方法)
}
}
completedAbruptly = false; // 平滑结束
} finally {
processWorkerExit(w, completedAbruptly); // 处理后续工作
}
}
getTask()
private Runnable getTask() {
boolean timedOut = false; // 记录上次workQueue.poll是否超时 for (;;) { // 循环
int c = ctl.get(); // 得到ctl
int rs = runStateOf(c); // 得到runState // 等价于,if !(rs < SHUTDOWN || (rs < STOP && !workQueue.isEmpty()))
// 即是,if !(rs == RUNNING || (rs == SHUTDOWN && !workQueue.isEmpty()))
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount(); // workerCount - 1
return null; // 返回null
} int wc = workerCountOf(c); // 得到workerCount // 是否允许超时
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 1. workerCount > maximumPoolSize
// 2. timed为真,并且timeOut也为真,即上次已超时,而且,此刻workerCount大于1,或者workQueue为空
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // workerCount减1
return null;
continue; // 否则,重试
} try {
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); // 允许超时,调用poll()方法,否则take()方法
if (r != null)
return r; // 不为空,返回
timedOut = true; // 否则,继续
} catch (InterruptedException retry) {
timedOut = false; // 中断,清除超时标记
}
}
processWorkerExit(Worker, boolean)
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // 如果是异常结束,需要调整workCount(-1),因为正常结束的,会在getTask()方法里调用decrementWorkerCount()方法
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock; // 可重入锁,因为要对works操作
mainLock.lock(); // 加锁
try {
completedTaskCount += w.completedTasks; // 添加当前线程完成的任务数量加到总的完成任务数量
workers.remove(w); // 从线程池中移除该线程
} finally {
mainLock.unlock(); // 释放锁
} tryTerminate(); // 调用tryTerminate()方法,看是否满足结束条件 int c = ctl.get(); // 获得ctl
if (runStateLessThan(c, STOP)) { // 线程池没有停止
if (!completedAbruptly) { // 该线程平滑结束
int min = allowCoreThreadTimeOut ? 0 : corePoolSize; // 得出最小线程数
if (min == 0 && !workQueue.isEmpty()) // 最少保留1个
min = 1;
if (workerCountOf(c) >= min) // 如果不小于最小线程数,不必再添加
return;
}
addWorker(null, false); // 如果是异常结束,则说明任务队列里应该还有任务,那么直接添加新的线程替换它
}
}
shutdown()
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 获得锁
try {
checkShutdownAccess(); // 检查权限
advanceRunState(SHUTDOWN); // 设置状态
interruptIdleWorkers(); // 中断空闲线程
onShutdown(); // 钩子 ScheduledThreadPoolExecutor
} finally {
mainLock.unlock(); // 释放锁
}
tryTerminate(); // 尝试终止线程池
}
shutdownNow()
public List<Runnable> shutdownNow() {
List<Runnable> tasks; // 存放未执行的任务
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 获得锁
try {
checkShutdownAccess(); // 检查权限
advanceRunState(STOP); // 设置状态
interruptWorkers(); // 中断线程
tasks = drainQueue(); // 拉取任务队列里的任务
} finally {
mainLock.unlock(); // 释放锁
}
tryTerminate(); // 尝试终止线程池
return tasks; // 返回任务列表
}
interruptIdleWorkers()
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
} private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 获得可重入锁
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) { // 线程没被中断,并且获得锁
try {
t.interrupt(); // 中断线程
} catch (SecurityException ignore) {
} finally {
w.unlock(); // 释放锁
}
}
if (onlyOne) // 如果仅仅中断一个,跳出循环
break;
}
} finally {
mainLock.unlock(); // 释放可重入锁
}
}
interruptWorkers()
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock; // 获得可重入锁
mainLock.lock(); // 加锁
try {
for (Worker w : workers)
w.interruptIfStarted(); // 只要线程已经启动,就中断它
} finally {
mainLock.unlock(); // 释放锁
}
}
tryTerminate()
final void tryTerminate() {
for (;;) {
int c = ctl.get(); // 获得ctl
// 1. 线程池正在运行
// 2. 线程池已经结束或正在整理
// 3. 线程池已经SHUTDOWN,但是workQueue不为空
if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && !workQueue.isEmpty()))
return;
// workerCount不等于0,只中断一个空闲线程,保证中断传播下去
if (workerCountOf(c) != 0) {
interruptIdleWorkers(ONLY_ONE);
return;
} final ReentrantLock mainLock = this.mainLock; // 可重入锁
mainLock.lock(); // 加锁
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { // 设置状态为TIDYING
try {
terminated(); // 调用terminated()方法,钩子,用户实现
} finally {
ctl.set(ctlOf(TERMINATED, 0)); // 设置状态为TERMINATED
termination.signalAll(); // 通知信号
}
return;
}
} finally {
mainLock.unlock(); // 解锁
}
}
}
行文至此结束。
尊重他人的劳动,转载请注明出处:http://www.cnblogs.com/aniao/p/aniao_tpe.html
【JUC源码解析】ThreadPoolExecutor的更多相关文章
- 【JUC源码解析】ScheduledThreadPoolExecutor
简介 它是一个线程池执行器(ThreadPoolExecutor),在给定的延迟(delay)后执行.在多线程或者对灵活性有要求的环境下,要优于java.util.Timer. 提交的任务在执行之前支 ...
- 【JUC源码解析】ForkJoinPool
简介 ForkJoin 框架,另一种风格的线程池(相比于ThreadPoolExecutor),采用分治算法,工作密取策略,极大地提高了并行性.对于那种大任务分割小任务的场景(分治)尤其有用. 框架图 ...
- 【JUC源码解析】SynchronousQueue
简介 SynchronousQueue是一种特殊的阻塞队列,该队列没有容量. [存数据线程]到达队列后,若发现没有[取数据线程]在此等待,则[存数据线程]便入队等待,直到有[取数据线程]来取数据,并释 ...
- 【JUC源码解析】DelayQueue
简介 基于优先级队列,以过期时间作为排序的基准,剩余时间最少的元素排在队首.只有过期的元素才能出队,在此之前,线程等待. 源码解析 属性 private final transient Reentra ...
- 【JUC源码解析】CyclicBarrier
简介 CyclicBarrier,一个同步器,允许多个线程相互等待,直到达到一个公共屏障点. 概述 CyclicBarrier支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后 ...
- 【JUC源码解析】ConcurrentLinkedQueue
简介 ConcurrentLinkedQueue是一个基于链表结点的无界线程安全队列. 概述 队列顺序,为FIFO(first-in-first-out):队首元素,是当前排队时间最长的:队尾元素,当 ...
- 【JUC源码解析】Exchanger
简介 Exchanger,并发工具类,用于线程间的数据交换. 使用 两个线程,两个缓冲区,一个线程往一个缓冲区里面填数据,另一个线程从另一个缓冲区里面取数据.当填数据的线程将缓冲区填满时,或者取数据的 ...
- Jdk1.6 JUC源码解析(12)-ArrayBlockingQueue
功能简介: ArrayBlockingQueue是一种基于数组实现的有界的阻塞队列.队列中的元素遵循先入先出(FIFO)的规则.新元素插入到队列的尾部,从队列头部取出元素. 和普通队列有所不同,该队列 ...
- Jdk1.6 JUC源码解析(13)-LinkedBlockingQueue
功能简介: LinkedBlockingQueue是一种基于单向链表实现的有界的(可选的,不指定默认int最大值)阻塞队列.队列中的元素遵循先入先出 (FIFO)的规则.新元素插入到队列的尾部,从队列 ...
- Jdk1.6 JUC源码解析(6)-locks-AbstractQueuedSynchronizer
功能简介: AbstractQueuedSynchronizer(以下简称AQS)是Java并发包提供的一个同步基础机制,是并发包中实现Lock和其他同步机制(如:Semaphore.CountDow ...
随机推荐
- 8、Web Service-IDEA-jaxws规范下的 spring整合CXF
前提:开发和之前eclipse的开发有很大的不同! 1.服务端的实现 1.新建项目 此时创建的是web项目 2.此时创建的项目是不完整的需要开发人员手动补充完整 3.对文件夹的设置(满满的软件使用方法 ...
- php memcache分布式和要注意的问题
Memcache的分布式介绍 memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能.服务器端仅包括内存存储功能,其实现非常简单.至于memcached的分布式,则是完全由客户 ...
- LVS (Linux Virtual Server) 思维导图笔记
- 安装IIS步骤图解
这几日好些网友来找iis安装包,但是因为新浪爱问的共享资料已关闭导致下载链接不可用,笔者在新浪微盘的备份资料只有5.1版,现共享链接如下: IIS5.1 for windows xp下载链接http: ...
- JNI由浅入深_3_Hello World
1.需要准备的工具,eclipse,cdt(c++)插件,cygwin(unix)和 android ndk. 在cygwin的etc目录下将ndk的路径引入到profile文件中,可以在cygwin ...
- CC2640R2F&TI-RTOS 拿到 TI CC2640R2F 开发板 第一件事就是移植串口驱动,重定向 printf
/* * board_uart.c * * Created on: 2018年7月3日 * Author: admin */ #include "board_uart.h" #in ...
- 黑少微服务商店之Iron Cloud微服务开发云
近日,由黑少微服务研发团队推出的Iron Cloud微服务开发云已经正式对外提供服务,这是国内第一家基于云端操作的微服务专业开发工具. Iron Cloud 微服务开发云(www.ironz.com) ...
- 对于PHP绘图技术的理解
要使用PHP绘图,就得在php.ini文件中设置一下 找到这个位置 ;extension=php_gd2.dll,然后把前面的分号去掉,重启下apache就可以了 几乎每行代码我都写了注释,方便看懂 ...
- Windows 安装配置memcached+php的教程,以及相关资源下载
第一步:安装 Memcached 服务 第二步:让php加载memcached.dll扩展 详情步骤如下: 第一步:安装 Memcached 服务 1.下载 Memcached 密码:jzay.压缩 ...
- XML中需要转义的字符
有的数据库表的字段名,对于某些数据库来讲,是属于关键字或者保留字来处理识别的,那么这个时候,需要对这样的字段名进行转义处理,一般在执行sql 语句脚本的情况,处理办法是在这个字段名外加上双引号就可以识 ...