并发编程(十五)——定时器 ScheduledThreadPoo…
2019-01-21 02:40:15来源:博客园 阅读 ()
在上一篇线程池的文章《并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)》中从ThreadPoolExecutor源码分析了其运行机制。限于篇幅,留下了ScheduledThreadPoolExecutor未做分析,因此本文继续从源代码出发分析ScheduledThreadPoolExecutor的内部原理。
类声明
1 public class ScheduledThreadPoolExecutor 2 extends ThreadPoolExecutor 3 implements ScheduledExecutorService {
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,实现了ScheduledExecutorService。因此它具有ThreadPoolExecutor的所有能力。所不同的是它具有定时执行,以周期或间隔循环执行任务等功能。
这里我们先看下ScheduledExecutorService的源码:
ScheduledExecutorService
1 //可调度的执行者服务接口 2 public interface ScheduledExecutorService extends ExecutorService { 3 4 //指定时延后调度执行任务,只执行一次,没有返回值 5 public ScheduledFuture<?> schedule(Runnable command, 6 long delay, TimeUnit unit); 7 8 //指定时延后调度执行任务,只执行一次,有返回值 9 public <V> ScheduledFuture<V> schedule(Callable<V> callable, 10 long delay, TimeUnit unit); 11 12 //指定时延后开始执行任务,以后每隔period的时长再次执行该任务 13 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, 14 long initialDelay, 15 long period, 16 TimeUnit unit); 17 18 //指定时延后开始执行任务,以后任务执行完成后等待delay时长,再次执行任务 19 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, 20 long initialDelay, 21 long delay, 22 TimeUnit unit); 23 }
其中schedule方法用于单次调度执行任务。这里主要理解下后面两个方法。
- scheduleAtFixedRate:该方法在initialDelay时长后第一次执行任务,以后每隔period时长,再次执行任务。注意,period是从任务开始执行算起的。开始执行任务后,定时器每隔period时长检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务,看下图示例
-
scheduleWithFixDelay:该方法在initialDelay时长后第一次执行任务,以后每当任务执行完成后,等待delay时长,再次执行任务,看下图示例。
使用例子
1、schedule(Runnable command,long delay, TimeUnit unit)
1 /** 2 * @author: ChenHao 3 * @Date: Created in 14:54 2019/1/11 4 */ 5 public class Test1 { 6 public static void main(String[] args) throws ExecutionException, InterruptedException { 7 // 延迟1s后开始执行,只执行一次,没有返回值 8 ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10); 9 ScheduledFuture<?> result = executorService.schedule(new Runnable() { 10 @Override 11 public void run() { 12 System.out.println("gh"); 13 try { 14 Thread.sleep(3000); 15 } catch (InterruptedException e) { 16 // TODO Auto-generated catch block 17 e.printStackTrace(); 18 } 19 } 20 }, 1000, TimeUnit.MILLISECONDS); 21 System.out.println(result.get()); 22 } 23 }
运行结果:
2、schedule(Callable<V> callable, long delay, TimeUnit unit);
1 public class Test2 { 2 public static void main(String[] args) throws ExecutionException, InterruptedException { 3 // 延迟1s后开始执行,只执行一次,有返回值 4 ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10); 5 ScheduledFuture<String> result = executorService.schedule(new Callable<String>() { 6 @Override 7 public String call() throws Exception { 8 try { 9 Thread.sleep(3000); 10 } catch (InterruptedException e) { 11 // TODO Auto-generated catch block 12 e.printStackTrace(); 13 } 14 return "ghq"; 15 } 16 }, 1000, TimeUnit.MILLISECONDS); 17 // 阻塞,直到任务执行完成 18 System.out.print(result.get()); 19 } 20 }
运行结果:
3、scheduleAtFixedRate
1 /** 2 * @author: ChenHao 3 * @Date: Created in 14:54 2019/1/11 4 */ 5 public class Test3 { 6 public static void main(String[] args) throws ExecutionException, InterruptedException { 7 ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10); 8 // 从加入任务开始算1s后开始执行任务,1+2s开始执行,1+2*2s执行,1+n*2s开始执行; 9 // 但是如果执行任务时间大于2s则不会并发执行后续任务,当前执行完后,接着执行下次任务。 10 ScheduledFuture<?> result = executorService.scheduleAtFixedRate(new Runnable() { 11 @Override 12 public void run() { 13 System.out.println(System.currentTimeMillis()); 14 } 15 }, 1000, 2000, TimeUnit.MILLISECONDS); 16 } 17 }
运行结果:
4、scheduleWithFixedDelay
1 /** 2 * @author: ChenHao 3 * @Date: Created in 14:54 2019/1/11 4 */ 5 public class Test4 { 6 public static void main(String[] args) throws ExecutionException, InterruptedException { 7 //任务间以固定时间间隔执行,延迟1s后开始执行任务,任务执行完毕后间隔2s再次执行,任务执行完毕后间隔2s再次执行,依次往复 8 ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10); 9 ScheduledFuture<?> result = executorService.scheduleWithFixedDelay(new Runnable() { 10 @Override 11 public void run() { 12 System.out.println(System.currentTimeMillis()); 13 } 14 }, 1000, 2000, TimeUnit.MILLISECONDS); 15 16 // 由于是定时任务,一直不会返回 17 result.get(); 18 System.out.println("over"); 19 } 20 }
运行结果:
源码分析
构造器
1 public ScheduledThreadPoolExecutor(int corePoolSize) { 2 super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, 3 new DelayedWorkQueue()); 4 }
内部其实都是调用了父类ThreadPoolExecutor的构造器,因此它具有ThreadPoolExecutor的所有能力。
通过super方法的参数可知,核心线程的数量即传入的参数,而线程池的线程数为Integer.MAX_VALUE,几乎为无上限。
这里采用了DelayedWorkQueue任务队列,也是定时任务的核心,是一种优先队列,时间小的排在前面,所以获取任务的时候就能先获取到时间最小的执行,可以看我上篇文章《并发编程(十四)—— ScheduledThreadPoolExecutor 实现原理与源码深度解析 之 DelayedWorkQueue》。
由于这里队列没有定义大小,所以队列不会添加满,因此最大的线程数就是核心线程数,超过核心线程数的任务就放在队列里,并不重新开启临时线程。
我们先来看看几个入口方法的实现:
1 public ScheduledFuture<?> schedule(Runnable command, 2 long delay, 3 TimeUnit unit) { 4 if (command == null || unit == null) 5 throw new NullPointerException(); 6 RunnableScheduledFuture<?> t = decorateTask(command, 7 new ScheduledFutureTask<Void>(command, null, 8 triggerTime(delay, unit))); 9 delayedExecute(t); 10 return t; 11 } 12 13 public <V> ScheduledFuture<V> schedule(Callable<V> callable, 14 long delay, 15 TimeUnit unit) { 16 if (callable == null || unit == null) 17 throw new NullPointerException(); 18 RunnableScheduledFuture<V> t = decorateTask(callable, 19 new ScheduledFutureTask<V>(callable, 20 triggerTime(delay, unit))); 21 delayedExecute(t); 22 return t; 23 } 24 25 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, 26 long initialDelay, 27 long period, 28 TimeUnit unit) { 29 if (command == null || unit == null) 30 throw new NullPointerException(); 31 if (period <= 0) 32 throw new IllegalArgumentException(); 33 ScheduledFutureTask<Void> sft = 34 new ScheduledFutureTask<Void>(command, 35 null, 36 triggerTime(initialDelay, unit), 37 unit.toNanos(period)); 38 RunnableScheduledFuture<Void> t = decorateTask(command, sft); 39 sft.outerTask = t; 40 delayedExecute(t); 41 return t; 42 } 43 44 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, 45 long initialDelay, 46 long delay, 47 TimeUnit unit) { 48 if (command == null || unit == null) 49 throw new NullPointerException(); 50 if (delay <= 0) 51 throw new IllegalArgumentException(); 52 ScheduledFutureTask<Void> sft = 53 new ScheduledFutureTask<Void>(command, 54 null, 55 triggerTime(initialDelay, unit), 56 unit.toNanos(-delay)); 57 RunnableScheduledFuture<Void> t = decorateTask(command, sft); 58 sft.outerTask = t; 59 delayedExecute(t); 60 return t; 61 }
这几个方法都是将任务封装成了ScheduledFutureTask,上面做的首先把runnable装饰为delay队列所需要的格式的元素,然后把元素加入到阻塞队列,然后线程池线程会从阻塞队列获取超时的元素任务进行处理,下面看下队列元素如何实现的。
ScheduledFutureTask
ScheduledFutureTask是一个延时定时任务,它可以返回任务剩余延时时间,可以被周期性地执行。
属性
1 private class ScheduledFutureTask<V> 2 extends FutureTask<V> implements RunnableScheduledFuture<V> { 3 /** 是一个序列,每次创建任务的时候,都会自增。 */ 4 private final long sequenceNumber; 5 6 /** 任务能够开始执行的时间 */ 7 private long time; 8 9 /** 10 * 任务周期执行的时间 11 * 0表示不是一个周期定时任务 12 * 正数表示固定周期时间去执行任务 13 * 负数表示任务完成之后,延时period时间再去执行任务 14 */ 15 private final long period; 16 17 /** 表示再次执行的任务,在reExecutePeriodic中调用 */ 18 RunnableScheduledFuture<V> outerTask = this; 19 20 /** 21 * 表示在任务队列中的索引位置,用来支持快速从队列中删除任务。 22 */ 23 int heapIndex; 24 }
ScheduledFutureTask继承了 FutureTask 和 RunnableScheduledFuture
属性说明:
- sequenceNumber: 是一个序列,每次创建任务的时候,都会自增。
- time: 任务能够开始执行的时间。
- period: 任务周期执行的时间。0表示不是一个周期定时任务。
- outerTask: 表示再次执行的任务,在reExecutePeriodic中调用
- heapIndex: 表示在任务队列中的索引位置,用来支持快速从队列中删除任务。
构造器
-
创建延时任务
1 /** 2 * 创建延时任务 3 */ 4 ScheduledFutureTask(Runnable r, V result, long ns) { 5 // 调用父类的方法 6 super(r, result); 7 // 任务开始的时间 8 this.time = ns; 9 // period是0,不是一个周期定时任务 10 this.period = 0; 11 // 每次创建任务的时候,sequenceNumber都会自增 12 this.sequenceNumber = sequencer.getAndIncrement(); 13 } 14 15 /** 16 * 创建延时任务 17 */ 18 ScheduledFutureTask(Callable<V> callable, long ns) { 19 // 调用父类的方法 20 super(callable); 21 // 任务开始的时间 22 this.time = ns; 23 // period是0,不是一个周期定时任务 24 this.period = 0; 25 // 每次创建任务的时候,sequenceNumber都会自增 26 this.sequenceNumber = sequencer.getAndIncrement(); 27 }
我们看看super(),其实就是FutureTask 里面的构造方法,关于FutureTask 可以看看我之前的文章《Java 多线程(五)—— 线程池基础 之 FutureTask源码解析》
1 public FutureTask(Runnable runnable, V result) { 2 this.callable = Executors.callable(runnable, result); 3 this.state = NEW; // ensure visibility of callable 4 } 5 public FutureTask(Callable<V> callable) { 6 if (callable == null) 7 throw new NullPointerException(); 8 this.callable = callable; 9 this.state = NEW; // ensure visibility of callable 10 }
- 创建延时定时任务
1 /** 2 * 创建延时定时任务 3 */ 4 ScheduledFutureTask(Runnable r, V result, long ns, long period) { 5 // 调用父类的方法 6 super(r, result); 7 // 任务开始的时间 8 this.time = ns; 9 // 周期定时时间 10 this.period = period; 11 // 每次创建任务的时候,sequenceNumber都会自增 12 this.sequenceNumber = sequencer.getAndIncrement(); 13 }
延时定时任务不同的是设置了period,后面通过判断period是否为0来确定是否是定时任务。
run()
1 public void run() { 2 // 是否是周期任务 3 boolean periodic = isPeriodic(); 4 // 如果不能在当前状态下运行,那么就要取消任务 5 if (!canRunInCurrentRunState(periodic)) 6 cancel(false); 7 // 如果只是延时任务,那么就调用run方法,运行任务。 8 else if (!periodic) 9 ScheduledFutureTask.super.run(); 10 // 如果是周期定时任务,调用runAndReset方法,运行任务。 11 // 这个方法不会改变任务的状态,所以可以反复执行。 12 else if (ScheduledFutureTask.super.runAndReset()) { 13 // 设置周期任务下一次执行的开始时间time 14 setNextRunTime(); 15 // 重新执行任务outerTask 16 reExecutePeriodic(outerTask); 17 } 18 }
这个方法会在ThreadPoolExecutor的runWorker方法中调用,而且这个方法调用,说明肯定已经到了任务的开始时间time了。这个方法我们待会会再继续来回看一下
- 先判断当前线程状态能不能运行任务,如果不能,就调用cancel()方法取消本任务。
- 如果任务只是一个延时任务,那么调用父类的run()运行任务,改变任务的状态,表示任务已经运行完成了。
- 如果任务只是一个周期定时任务,那么就任务必须能够反复执行,那么就不能调用run()方法,它会改变任务的状态。而是调用runAndReset()方法,只是简单地运行任务,而不会改变任务状态。
- 设置周期任务下一次执行的开始时间time,并重新执行任务。
schedule(Runnable command, long delay,TimeUnit unit)
1 public ScheduledFuture<?> schedule(Runnable command, 2 long delay, 3 TimeUnit unit) { 4 if (command == null || unit == null) 5 throw new NullPointerException(); 6 7 //装饰任务,主要实现public long getDelay(TimeUnit unit)和int compareTo(Delayed other)方法 8 RunnableScheduledFuture<?> t = decorateTask(command, 9 new ScheduledFutureTask<Void>(command, null, 10 triggerTime(delay, unit))); 11 //添加任务到延迟队列 12 delayedExecute(t); 13 return t; 14 }
获取延时执行时间
1 private long triggerTime(long delay, TimeUnit unit) { 2 return triggerTime(unit.toNanos((delay < 0) ? 0 : delay)); 3 } 4 5 /** 6 * Returns the trigger time of a delayed action. 7 */ 8 long triggerTime(long delay) { 9 //当前时间加上延时时间 10 return now() + 11 ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay)); 12 }
上述的decorateTask方法把Runnable任务包装成ScheduledFutureTask,用户可以根据自己的需要覆写该方法:
1 protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable, RunnableScheduledFuture<V> task) { 2 return task; 3 }
schedule的核心是其中的delayedExecute方法:
1 private void delayedExecute(RunnableScheduledFuture<?> task) { 2 if (isShutdown()) // 线程池已关闭 3 reject(task); // 任务拒绝策略 4 else { 5 //将任务添加到任务队列,会根据任务延时时间进行排序 6 super.getQueue().add(task); 7 // 如果线程池状态改变了,当前状态不能运行任务,那么就尝试移除任务, 8 // 移除成功,就取消任务。 9 if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) 10 task.cancel(false); // 取消任务 11 else 12 // 预先启动工作线程,确保线程池中有工作线程。 13 ensurePrestart(); 14 } 15 }
这个方法的主要作用就是将任务添加到任务队列中,因为这里任务队列是优先级队列DelayedWorkQueue,它会根据任务的延时时间进行排序。
-
如果线程池不是RUNNING状态,不能执行延时任务task,那么调用reject(task)方法,拒绝执行任务task。
-
将任务添加到任务队列中,会根据任务的延时时间进行排序。
-
因为是多线程并发环境,就必须判断在添加任务的过程中,线程池状态是否被别的线程更改了,那么就可能要取消任务了。
-
将任务添加到任务队列后,还要确保线程池中有工作线程,不然任务也不为执行。所以ensurePrestart()方法预先启动工作线程,确保线程池中有工作线程。
1 void ensurePrestart() { 2 // 线程池中的线程数量 3 int wc = workerCountOf(ctl.get()); 4 // 如果小于核心池数量,就创建新的工作线程 5 if (wc < corePoolSize) 6 addWorker(null, true); 7 // 说明corePoolSize数量是0,必须创建一个工作线程来执行任务 8 else if (wc == 0) 9 addWorker(null, false); 10 }
通过ensurePrestart可以看到,如果核心线程池未满,则新建的工作线程会被放到核心线程池中。如果核心线程池已经满了,ScheduledThreadPoolExecutor不会像ThreadPoolExecutor那样再去创建归属于非核心线程池的工作线程,加入到队列就完了,等待核心线程执行完任务再拉取队列里的任务。也就是说,在ScheduledThreadPoolExecutor中,一旦核心线程池满了,就不会再去创建工作线程。
这里思考一点,什么时候会执行else if (wc == 0)创建一个归属于非核心线程池的工作线程?
答案是,当通过setCorePoolSize方法设置核心线程池大小为0时,这里必须要保证任务能够被执行,所以会创建一个工作线程,放到非核心线程池中。
看到 addWorker(null, true); 并没有将任务设置进入,而是设置的null, 则说明线程池里线程第一次启动时, runWorker中取到的 firstTask为null,需要通过 getTask() 从队列中取任务,这里可以看看我之前写的关于线程池的文章《并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)》。
getTask()中 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();如果是存在核心线程则调用take(),如果传入的核心线程为0,则存在一个临时线程,调用poll(),这两个方法都会先获取时间,看看有没有达到执行时间,没有达到执行时间则阻塞,可以看看我上一篇文章,达到执行时间,则取到任务,就会执行下面的run方法。
1 public void run() { 2 // 是否是周期任务 3 boolean periodic = isPeriodic(); 4 // 如果不能在当前状态下运行,那么就要取消任务 5 if (!canRunInCurrentRunState(periodic)) 6 cancel(false); 7 // 如果只是延时任务,那么就调用run方法,运行任务。 8 else if (!periodic) 9 ScheduledFutureTask.super.run(); 10 // 如果是周期定时任务,调用runAndReset方法,运行任务。 11 // 这个方法不会改变任务的状态,所以可以反复执行。 12 else if (ScheduledFutureTask.super.runAndReset()) { 13 // 设置周期任务下一次执行的开始时间time 14 setNextRunTime(); 15 // 重新执行任务outerTask 16 reExecutePeriodic(outerTask); 17 } 18 } 19 20 public boolean isPeriodic() { 21 return period != 0; 22 }
schedule不是周期任务,那么调用父类的run()运行任务,改变任务的状态,表示任务已经运行完成了。
scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
1 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, 2 long initialDelay, 3 long period, 4 TimeUnit unit) { 5 if (command == null || unit == null) 6 throw new NullPointerException(); 7 if (period <= 0) 8 throw new IllegalArgumentException(); 9 //装饰任务类,注意period=period>0,不是负的 10 ScheduledFutureTask<Void> sft = 11 new ScheduledFutureTask<Void>(command, 12 null, 13 triggerTime(initialDelay, unit), 14 unit.toNanos(period)); 15 RunnableScheduledFuture<Void> t = decorateTask(command, sft); 16 sft.outerTask = t; 17 //添加任务到队列 18 delayedExecute(t); 19 return t; 20 }
如果是周期任务则执行上面run()方法中的第12行,调用父类中的runAndReset(),这个方法同run方法比较的区别是call方法执行后不设置结果,因为周期型任务会多次执行,所以为了让FutureTask支持这个特性除了发生异常不设置结果。
执行完任务后通过setNextRunTime方法计算下一次启动时间:
1 private void setNextRunTime() { 2 long p = period; 3 //period=delay; 4 if (p > 0) 5 time += p;//由于period>0所以执行这里,设置time=time+delay 6 else 7 time = triggerTime(-p); 8 } 9 10 long triggerTime(long delay) { 11 return now() + 12 ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay)); 13 }
scheduleAtFixedRate会执行到情况一,下一次任务的启动时间最早为上一次任务的启动时间加period。
scheduleWithFixedDelay会执行到情况二,这里很巧妙的将period参数设置为负数到达这段代码块,在此又将负的period转为正数。情况二将下一次任务的启动时间设置为当前时间加period。
然后将任务再次添加到任务队列:
1 /** 2 * 重新执行任务task 3 */ 4 void reExecutePeriodic(RunnableScheduledFuture<?> task) { 5 // 判断当前线程池状态能不能运行任务 6 if (canRunInCurrentRunState(true)) { 7 // 将任务添加到任务队列,会根据任务延时时间进行排序 8 super.getQueue().add(task); 9 // 如果线程池状态改变了,当前状态不能运行任务,那么就尝试移除任务, 10 // 移除成功,就取消任务。 11 if (!canRunInCurrentRunState(true) && remove(task)) 12 task.cancel(false); 13 else 14 // 预先启动工作线程,确保线程池中有工作线程。 15 ensurePrestart(); 16 } 17 }
这个方法与delayedExecute方法很像,都是将任务添加到任务队列中。
- 如果当前线程池状态能够运行任务,那么任务添加到任务队列。
- 如果在在添加任务的过程中,线程池状态是否被别的线程更改了,那么就要进行判断,是否需要取消任务。
- 调用ensurePrestart()方法,预先启动工作线程,确保线程池中有工作线程。
ScheduledFuture的get方法
既然ScheduledFuture的实现是ScheduledFutureTask,而ScheduledFutureTask继承自FutureTask,所以ScheduledFuture的get方法的实现就是FutureTask的get方法的实现,FutureTask的get方法的实现分析在ThreadPoolExecutor篇已经写过,这里不再叙述。要注意的是ScheduledFuture的get方法对于非周期任务才是有效的。
推荐博客
https://www.cnblogs.com/chen-haozi/p/10227797.html
ScheduledThreadPoolExecutor总结
-
ScheduledThreadPoolExecutor是实现自ThreadPoolExecutor的线程池,构造方法中传入参数n,则最多会有n个核心线程工作,空闲的核心线程不会被自动终止,而是一直阻塞在DelayedWorkQueue的take方法尝试获取任务。构造方法传入的参数为0,ScheduledThreadPoolExecutor将以非核心线程工作,并且最多只会创建一个非核心线程,参考上文中ensurePrestart方法的执行过程。而这个非核心线程以poll方法获取定时任务之所以不会因为超时就被回收,是因为任务队列并不为空,只有在任务队列为空时才会将空闲线程回收,详见ThreadPoolExecutor篇的runWorker方法,之前我以为空闲的非核心线程超时就会被回收是不正确的,还要具备任务队列为空这个条件。
-
ScheduledThreadPoolExecutor的定时执行任务依赖于DelayedWorkQueue,其内部用可扩容的数组实现以启动时间升序的二叉树。
-
工作线程尝试获取DelayedWorkQueue的任务只有在任务到达指定时间才会成功,否则非核心线程会超时返回null,核心线程一直阻塞。
-
对于非周期型任务只会执行一次并且可以通过ScheduledFuture的get方法阻塞得到结果,其内部实现依赖于FutureTask的get方法。
-
周期型任务通过get方法无法获取有效结果,因为FutureTask对于周期型任务执行的是runAndReset方法,并不会设置结果。周期型任务执行完毕后会重新计算下一次启动时间并且再次添加到DelayedWorkQueue中。
原文链接:https://www.cnblogs.com/java-chen-hao/p/10283413.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:我的第一篇博客
- 因为命名被diss无数次。简单聊聊编程最头疼的事情之一:命名 2020-06-10
- Java3个编程题整理 2020-06-09
- (易忘篇)java基础编程难点4 2020-06-08
- (易忘篇)java基础编程难点3 2020-06-05
- 国外大佬总结的 10 个 Java 编程技巧! 2020-06-04
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash