前序:
我们前面说了,我们的程序是可以分成若干个程序片段的,而每一个程序片段我们都可以用来做为一在多个任务;当然做为独立可运行的任务将更为理想化;在多线程来实现并发的手段时,我们常常将没一个任务用一个独立的线程来进行驱动;
下面是一个简单的线程来驱动我们的一个任务的执行;
1.任务的描述
/** * 一个简单的任务类 * @author liuwei 继承于Runnable接口,并提供Run方法的实现 * Runnable为描述任务的方式 */ public class SimpleTask implements Runnable { /** * 任务执行总数 */ protected int countDown = 10; private static int taskCount = 0; /** * 任务执行次数id */ private final int id = taskCount++; /** * 无参构造函数 */ public SimpleTask() { } /** * 带参构造函数 * @param countDown */ public SimpleTask(int countDown) { this.countDown = countDown; } /** * 显示任务的信息 * @return */ public String status() { return "#" + id + "(" + (countDown > 0? String.valueOf (countDown):"SimpleTask!")+ "), "; } /** * 任务的执行方法 */ public void run() { while (countDown-- > 0) { System.out.print(status()); /** * 对线程调度器的一种建议,即java线程机制的一部分, * 可以将cpu从一个线程转到另一个线程,且类似告诉 * 别人,我这里目前不需要占用现在的内存资源了; * 我们可以说它是一个上下文切换的一个动作; */ Thread.yield(); } } }
这个单独的任务,并不能够独立的执行;且也不具有产生任务内在的线程能力;而且,要实现线程行为,还必须显式的将这个任务附到线程上去;
2.线程的描述
在java中,将Runnable对象转变成工作任务的传统方式就是将它将给线程,并由线程来驱动它执行;这一点和Quartz里的Job与Scheduler有点相似;即作业不能够自己运行,它需要在Scheduler上进行注册后,由任务调度器来负责管理它;
下面我们来实现一个线程驱动任务的执行;
/** * 一个线程来驱动任务的执行 * @author liuwei */ public class TreadDriverTask { /** * 当通过线程thread的start方法调用任务的run方法的时候, * main函数所对应的线程将继续执行System的语句;它不会等待 * thread执行完毕后再执行System操作; * 我们前面说过,一个线程就是在进程中有一个单一的顺序控制流; * @param args */ public static void main(String[] args){ Thread thread=new Thread(new SimpleTask()); thread.start(); System.out.println("---->main执行的现成与线程thread是相互独立 的!"); } }
它的执行情况为:
---->main执行的现成与线程thread是相互独立的!
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(SimpleTask!),
我们来看看多个现在执行多个任务的时候,它是一个什么样的情况!
/** * 多个线程来驱动任务的执行 * @author liuwei */ public class MultThreadDriverTask { /** * 当通过线程thread的start方法调用任务的run方法的时候, * main函数所对应的线程将继续执行System的语句;它不会等待 * thread执行完毕后再执行System操作; * 我们前面说过,一个线程就是在进程中有一个单一的顺序控制流; * @param args */ public static void main(String[] args){ for(int i=0;i<5;i++){ new Thread(new SimpleTask()).start(); } System.out.println("---->waiting for ......!"); } }
它的执行效果如下:
#0(9), #1(9), ---->waiting for ......!
#0(8), #3(9), #1(8), #3(8), #2(9), #1(7), #4(9), #0(7), #2(8),
#4(8), #0(6), #2(7), #4(7), #0(5), #2(6), #4(6), #0(4), #2(5), #4(5),
#0(3), #2(4), #4(4), #0(2), #2(3), #4(3), #0(1), #3(7), #2(2), #4(2),
#1(6), #0(SimpleTask!), #3(6), #2(1), #1(5), #4(1), #3(5),
#2(SimpleTask!), #1(4), #4(SimpleTask!), #3(4), #1(3), #3(3), #1(2),
#3(2), #1(1), #3(1), #1(SimpleTask!), #3(SimpleTask!),
我们可以看出:
当通过for循环分别创建5个线程的时候,在创建的过程中是有一定的时间差的;比如我在创建第5个线程的时候,可能第一个创建的线程已经开始执行
附属任务的run方法;且所有线程创建完毕后(这个是由main所在线程内完成的),则执行了System操作;而前面创建的5个线程,他们将自己运行自
己的;
另一个特点是,他们的输出是相互混乱的;因为线程间的相互切换是由线程调度器来进行自动控制的;对于单处理器的环境,则调度器将将cpu时间片分给不同的线程进行占用;而对于多处理器的环境,则线程调度器将会在多个处理器之间默默的分发线程;
3.线程调度机制的非确定性
对于上面的结果,可能每一次运行的输出也是不一样的;因为这就是线程调度机制的非确定性,同样,对于不同的jdk版本,其运行的输入也不一样;首先对于同
一jdk版本下的运行结果的不一致,是因为对于每一次的运行,它的cpu时间片的分配可能进行不同的分配(单处理器环境),它的任务处理器也可能每次分配
的线程也不一致(多处理器环境);对于不同jdk版本,在sun的早期jdk,cpu时间的切片动作不是很频繁,可能上面的5个线程,线程1执行完后,线
程2才会执行;它启动所有线程的代价相对现有jdk的启动可能代价更高;现在的jdk,时间切片行为更为完善,每个线程看起来都会获得更加正规的服务(处
理器服务于它);
4.main函数为何没有对启动的线程进行异常捕获且没有引用的Thread没有被垃圾回收
在上面的main方法里创建Thread对象的时候,它并没有去捕获这些对象的引用;通常大家都知道,对于一个对象没有引用指向他,则它将被垃圾回收器定
时回收;但对于thread就不同了,每个thread都注册了自己,因为确实有一个引用指向它,且只有当run方法执行完毕后,垃圾回收器才会清楚它;
且我们说过了mian方法,与启动的每一个thread是相互独立的,我们不能够在A线程1去捕获B线程下可能存在的异常;