本文分析了Eclipse中多线程程序的实现讨论了在Eclipse客户端程序开发中应用多线程的方法和要注意的问题同时也讨论了多线程程序的一些调试和问题解决的方法
Eclipse作为一个开发平台使用越来越广泛基于Eclipse Rich Client Platform开发的客户端程序也越来越多在当今越来越复杂的应用环境中我们的客户端程序不可避免的要同时进行多任务的处理一个优异的客户端程序都会允许用户同时启动多个任务从而大大提高用户的工作效率以及用户体验本文中我们来谈谈Eclipse中实现多任务的方式
在我们基于Eclipse的Java程序中我们有很多种方式提供多任务的实现熟悉Java的朋友立即会想到Java的Thread类这是Java中使用最多的一个实现多任务的类Eclipse平台为多任务处理提供了自己的API那就是Job以及UIJobEclipse中的Job是对Java Thread的一个封装为我们实现多任务提供了更方便的接口以下是Job的基本用法
清单 Job用法示例
Job job = new Job(Job Name){protected IStatus run(IProgressMonitor monitor) {// 在这里添加你的任务代码return StatusOK_STATUS;}};jobschedule(delayTime);
在Eclipse中我们也会经常用到DisplayasynchExec() 和DisplaysynchExec()来启动任务的执行这两个方法主要为了方便我们完成界面操作的任务以下是DisplayasynchExec()的用法DisplaysynchExec()和它类似
清单 DisplaysynchExec()用法示例
DisplaygetDefault()asyncExec(new Runnable() {public void run() {// 在这里添加你的任务代码}});
通常在Eclipse中我们最好使用Eclipse提供的Job接口来实现多任务而不是使用Java的thread为什么呢?主要有以下几个原因
Job是可重用的工作单元一个Job我们可以很方便的让它多次执行
Job提供了方便的接口使得我们在处理中能够很方便的与外界交流报告当前的执行进度
Eclipse提供了相应的机制使得程序员可以方便的介入Job的调度例如我们可以方便的实现每次只有一个同一类型的Job在运行
Eclipse缺省提供了Job管理的程序可以查看当前所有的Job和它们的进度也提供UI终止暂停继续指定的Job
使用Job可以提高程序的性能节省线程创建和销毁的开销Eclipse中的Job封装了线程池的实现当我们启动一个Job时Eclipse不会马上新建一个Thread它会在它的线程池中寻找是否有空闲的线程如果有空闲线程就会直接用空闲线程运行你的Job一个Job终止时它所对应的线程也不会立即终止它会被返回到线程池中以备重复利用这样我们可以节省创建和销毁线程的开销
下面我们从几个方面来讨论Eclipse中Job的实现和使用方面的问题
Eclipse中Job的实现
Eclipse的核心包中提供了一个JobManager类它实现了IJobManager接口Eclipse中Job的管理和调度都是由JobManager来实现的 JobManager维护有一个线程池用来运行Job当我们调用Job的schedule方法后这个Job会被JobManager首先放到一个Job运行的等待队列中去之后JobManager会通知线程池有新的Job加入了运行等待队列线程池会找出一个空闲的线程来运行Job如果没有空闲线程线程池会创建一个新的线程来运行Job一旦Job运行完毕运行Job的线程会返回到线程池中以备下次使用从上面Job运行的过程我们可以看到JobManager介入了一个Job运行的全过程它了解Job什么时候开始什么时候结束每一时候Job的运行状态JobManager将这些Job运行的信息以接口的方式提供给用户同时它也提供了接口让我们可以介入Job的调度等从而我们拥有了更加强大的控制Job的能力
为了我们更方便的了解Job所处的状态JobManager设置Job的一个状态标志位我们可以通过Job的getState方法获得Job当前的状态值以了解其状态
NONE当一个Job刚构造的时候Job就会处于这种状态当一个Job执行完毕(包括被取消)后Job的状态也会变回这种状态
WAITING:当我们调用了Job的shedule方法JobManager会将Job放入等待运行的Job队列这时Job的状态为WAITING
RUNNING:当一个Job开始执行Job的状态会变为RUNNING
SLEEPING:当我们调用Job的sleep方法后Job会变成这一状态当我们调用schudule方法的时候带上延时的参数Job的状态也会转入这一状态在这一段延时等待的时间中Job都处于这一状态这是一种睡眠状态Job在这种状态中时不能马上转入运行我们可以调用Job的wakeup方法来将Job唤醒这样Job又会转入WAITING状态等待运行
Eclipse中的UI线程
另外在Eclipse的线程处理中有一个UI线程的概念Eclipse程序中的主线程是一个特殊的线程程序启动后会先执行这个线程也就是我们的main()函数所在的线程作为桌面应用程序我们的主线程主要负责界面的响应以及绘制界面元素所以通常我们也叫它UI线程
以下代码编过SWT应用程序的读者会非常熟悉它一般出现在main函数的结尾下面来仔细分析一下它的详细情况
//当窗口未释放时while (!shellisDisposed()) { //如果display对象事件队列中没有了等待的事件就让该线程进入等待状态 if (!displayreadAndDispatch()) displaysleep();}
上面的程序实际上就是我们UI线程的处理逻辑当程序启动后UI线程会读取事件等待队列看有没有事件等待处理如果有它会进行相应处理如果没有它会进入睡眠状态如果有新的事件到来它又会被唤醒进行处理UI线程所需要处理的事件包括用户的鼠标和键盘操作事件操作系统或程序中发出的绘制事件一般来说处理事件的过程也就是响应用户操作的过程
一个好的桌面应用程序需要对用户的操作作出最快的响应也就是说我们的UI线程必须尽快的处理各种事件从我们程序的角度来说在UI线程中我们不能进行大量的计算或者等待否则用户操作事件得不到及时的处理通常如果有大量的计算或者需要长时间等待(例如进行网络操作或者数据库操作)时我们必须将这些长时间处理的程序单独开辟出一个线程来执行这样虽然后台运行着程序但也不会影响界面上的操作
除主线程之外的所有线程都是非UI线程在Eclipse程序中我们所有对界面元素的操作都必须放到UI线程中来执行否则会抛出Exception所以我们要区分出UI线程和非UI线程保证我们对UI的操作都在UI线程中执行
如何判断当前线程是否UI线程: 你可以通过调用DisplaygetCurrent()来知道当前线程是否是UI线程如果DisplaygetCurrent()返回为空表示当前不是UI线程
Eclipse中使用线程的几种典型情况
控制Job的并发运行
对于某些Job为了避免并发性问题我们希望同时只有一个这样的Job在运行这时我们需要控制Job的并发运行在另一种情况下我们也需要控制Job的并发运行我们在程序中对于一个任务我们有可能会启动一个Job来执行对于少量的任务来说这是可行的但是如果我们预测可能会同时有大量的任务如果每一个任务启动一个Job我们同时启动的Job就会非常多这些Job会侵占大量的资源影响其他任务的执行我们可以使用Job的rule来实现控制Job的并发执行简单的我们可以通过下面的代码实现我们先定义一个如下rule
private ISchedulingRule Schedule_RULE = new ISchedulingRule() {public boolean contains(ISchedulingRule rule) {return thisequals(rule);}public boolean isConflicting(ISchedulingRule rule) {return thisequals(rule);}};
对于需要避免同时运行的Job我们可以将它们的rule设成上面定义的rule如
myjobsetRule(Schedule_RULE);myjobsetRule(Schedule_RULE);
这样对于myjob和myjob这两个Job它们不会再同时执行Myjob会等待myjob执行完再执行这是由Eclipse的JobManager来提供实现的JobManager可以保证所有启动的Job中任意两个Job的rule是没有沖突的我们在上面定义的rule是最简单的我们可以重写isConflicting函数来实现一些更加复杂的控制比如控制同时同类型的Job最多只有指定的个数在运行但是我们要注意isConflicting方法不能过于复杂一旦一个Job的rule与其他Job的rule有沖突isConflicting方法会调用很多次如果其中的计算过于复杂会影响整体的性能
根据需要执行Job
由于我们有的Job有可能不是立即执行的在有些情况下等到该Job准备执行的时候该Job所要执行的任务已经没有意义了这时我们可以使用Job的shouldSchedule()和shouldRun()来避免Job的运行在我们定义一个Job时我们可以重载shouldSchedule和shouldRun方法在这些方法中我们可以检查Job运行的一些先决条件如果这些条件不满足我们就可以返回falseJobManager在安排Job运行时它会先调用该Job的shouldSchedule方法如果返回为falseJobManager就不会再安排这个Job运行了同样JobManager在真正启动一个线程运行一个Job前它会调用该Job的shouldRun方法如果返回false它不再运行这个Job在下面的例子中我们希望启动一个Job在十秒钟之后更新文本框中的内容为了保证我们的Job运行时是有意义的我们需要确保我们要更新的文本框没有被销毁我们重载了shouldSchedule和shouldRun方法
Text text = new Text(parentSWTNONE);UIJob refreshJob = new UIJob(更新界面){public IStatus runInUIThread(IProgressMonitor monitor) {textsetText(新文本);return StatusOK_STATUS;}public boolean shouldSchedule(){return !textisDisposed();}public boolean shouldRun(){return !textisDisposed();}};refreshJobschedule();
在UI线程中涉及长时间处理的任务
我们经常碰到这样一种情况用户操作菜单或者按钮会触发查询大量数据数据查询完后更新表格等界面元素用户点击菜单或者按钮所触发的处理程序一般处于UI线程为了避免阻塞UI我们必须把数据查询等费时的工作放到单独的Job中执行一旦数据查询完毕我们又必须更新界面这时我们又需要使用UI线程进行处理下面是处理这种情况的示例代码
buttonaddSelectionListener(new SelectionListener(){public void widgetSelected(SelectionEvent e){perform();}public void widgetDefaultSelected(SelectionEvent e){perform();}private void perform(){Job job = new Job(获取数据){protected IStatus run(IProgressMonitor monitor){// 在此添加获取数据的代码DisplaygetDefault()asyncExec(new Runnable(){public void run(){// 在此添加更新界面的代码}});}};jobschedule();}});
延时执行Job避免无用的Job运行
我们经常需要根据选中的对象刷新我们部分的界面元素如果我们连续很快的改变选择而每次刷新界面涉及到的区域比较大时界面会出现闪烁从用户的角度来说我们很快的改变选择希望看到的只是最后选中的结果中间的界面刷新都是不必要的
在Jface中StructuredViewer提供了addPostSelectionChangedListener方法如果我们使用这个方法监听selectionChanged事件当用户一直按着方向键改变选中时我们只会收到一个selectionChanged事件这样我们可以避免过度的刷新界面
实际上Jface中就是通过延时执行Job来实现这一功能的我们也可以自己实现类似功能
private final static Object UPDATE_UI_JOBFAMILY = new Object();tableviewer addSelectionChangedListener (new ISelectionChangedListener (){public void selectionChanged(SelectionChangedEvent event){JobgetJobManager()cancel(UPDATE_UI_JOBFAMILY);new UIJob(更新界面) { protected IStatus runInUIThread (IProgressMonitor monitor) { //更新界面 return StatusOK_STATUS; }public boolean belongsTo(Object family){return family== UPDATE_UI_JOBFAMILY;} }schedule();}});
首先我们需要将界面更新的代码放到一个UIJob中同时我们将Job延时毫秒执行(我们可以根据需要改变延时的时间)如果下一个selectionChanged事件很快到来我们的调用JobgetJobManager()cancel(UPDATE_UI_JOBFAMILY)将以前未运行的Job取消这样只有最后一个Job会真正运行
在UI线程中等待非UI线程的结束
有时我们在UI线程中需要等待一个非UI线程执行完我们才能继续执行例如我们在UI线程中要显示某些数据但是这些数据又需要从数据库或者远程网络获取于是我们会启动一个非UI的线程去获取数据而我们的UI线程必须要等待这个非UI线程执行完成我们才能继续执行当然一种简单的实现方法是使用join我们可以在UI线程中调用非UI线程的join方法这样我们就可以等待它执行完了我们再继续但是这会有一个问题当我们的UI线程等待时意味着我们的程序不会再响应界面操作也不会刷新这样用户会觉得我们的程序象死了一样没有反应这时我们可以使用ModalContext类你可以将你要执行的获取数据的任务用ModalContext的run方法来运行(如下)ModalContext会将你的任务放到一个独立的非UI线程中执行并且等待它执行完再继续执行与join方法不同的是ModalContext在等待时不会停止UI事件的处理这样我们的程序就不会没有响应了
try { ModalContextrun(new IRunnableWithProgress(){ public void run(IProgressMonitor monitor) throws InvocationTargetException InterruptedException { /*需要在非UI线程中执行的代码*/ ModalContextcheckCanceled(monitor); } } true new NullProgressMonitor() DisplaygetCurrent()); } catch (InvocationTargetException e) { } catch (InterruptedException e) { }
针对相关联的Job统一进行处理
有时我们需要对相关联的Job一起处理例如需要同时取消这些Job或者等待所有这些Job结束这时我们可以使用Job Family对于相关联的Job我们可以将它们设置成同一个Job Family我们需要重载Job的belongsTo方法以设置一个Job的Job Family
Private Object MY_JOB_FAMILY = new Object();Job job = new Job(Job Name){protected IStatus run(IProgressMonitor monitor) {// 在这里添加你的任务代码return StatusOK_STATUS;}public boolean belongsTo(Object family){return MY_JOB_FAMILYequals(family);}};
我们可以使用JobManager的一系列方法针对Job Family进行操作
JobgetJobManager()cancel(MY_JOB_FAMILY); //取消所有属于MY_JOB_FAMILY的所有JobJobgetJobManager()join(MY_JOB_FAMILY); //等待属于MY_JOB_FAMILY的所有Job结束JobgetJobManager()sleep(MY_JOB_FAMILY); //将所有属于MY_JOB_FAMILY的Job转入睡眠状态JobgetJobManager()wakeup(MY_JOB_FAMILY); //将所有属于MY_JOB_FAMILY的Job唤醒
线程死锁的调试和解决技巧
一旦我们使用了线程我们的程序中就有可能有死锁的发生一旦发生死锁我们发生死锁的线程会没有响应导致我们程序性能下降如果我们的UI线程发生了死锁我们的程序会没有响应必须要重启程序所以在我们多线程程序开发中发现死锁的情况解决死锁问题对提高我们程序的稳定性和性能极为重要
如果我们发现程序运行异常(例如程序没有响应)我们首先要确定是否发生了死锁通过下面这些步骤我们可以确定是否死锁以及死锁的线程
在Eclipse中以Debug模式运行程序
执行响应的测试用例重现问题
在Eclipse的Debug View中选中主线程(Thread[main])选择菜单Run>Suspend这时Eclipse会展开主线程的函数调用栈我们就可以看到当前主线程正在执行的操作
通常Eclipse在等待用户的操作它的函数调用栈会和以下类似
图片示例
如果主线程发生死锁函数调用栈的最上层一般会是你自己的函数调用你可以查看一下你当前的函数调用以确定主线程在等待什么
使用同样的方法查看其他线程特别是那些等待UI线程的线程
我们需要找出当前线程相互的等待关系以便找出死锁的原因我们找出死锁的线程后就可以针对不同情况进行处理
减小锁的粒度增加并发性
调整资源请求的次序
将需要等待资源的任务放到独立的线程中执行
Job使用中要注意的问题
不要在Job中使用Threadsleep方法如果你想要让Job进入睡眠状态最好用Job的sleep方法虽然使用Threadsleep和Job的sleep方法达到的效果差不多但是它们实现的方式完全不同对系统的影响也不一样我们知道Eclipse中Job是由Eclipse的JobManager来管理的如果我们调用Job的sleep方法JobManager会将Job转入睡眠状态与其对应的线程也会重新放入线程池等待运行其他Job而如果我们在Job中直接调用Threadsleep方法它会直接使运行Job的线程进入睡眠状态其他Job就不可能重用这个线程了同时虽然运行该Job的线程进入了睡眠状态Job的状态还是Running(运行状态)我们也不能用Job的wakeup方法唤醒该Job了
Job的取消一般我们会直观的认为一旦调用Job的cancel方法Job就会停止运行实际上这并不一定正确当Job处于不同的状态时我们调用Job的cancel方法所起的效果是不同的当Job在WAITING状态和SLEEPING状态时一旦我们调用cancel方法JobManager会将Job直接从等待运行的队列中删除Job不会再运行了这时cancel方法会返回true但是如果Job正在运行cancel方法调用并不会立即终止Job的运行它只会设定一个标志指明这个Job已经被取消了我们可以使用Job的run方法传入的参数IProgressMonitor monitor这个参数的isCanceled方法会返回Job是否被取消的状态如果需要我们必须在我们的代码的适当位置检查Job是否被取消的标志作出适当的响应另外由于调用Job的cancel方法不一定立即终止Job如果我们需要等待被取消的Job运行完再执行我们可以用如下代码
if (!jobcancel())jobjoin();
Join方法的使用由于join方法会导致一个线程等待另一个线程一旦等待线程中拥有一个被等待线程所需要的锁就会产生死锁当我们的线程中需要用到同步时这种死锁的情况非常容易出现所以我们使用join时必须非常小心尽量以其他方法替代
避免过时的Job造成的错误由于我们启动的线程并不一定是马上执行的当我们的Job开始运行时情况可能发生了变化我们在Job的处理代码中要考虑到这些情况一种典型的情况是我们在启动一个对话框或者初始化一个ViewPart时我们会启动一些 Job去完成一些数据读取的工作一旦数据读取结束我们会启动新的UI Job更新相应的UI有时用户在打开对话框或者View后马上关闭了该对话框或者View这时我们启动的线程并没有被中断一旦在Job中再去更新UI就会出错在我们的代码中必须作相应的处理所以我们在线程中更新界面元素之前我们必须先检查相应的控件是否已经被dispose了
结束语
在我们进行基于Eclipse的客户端开发时使用多线程可以大大的提供我们的程序并发处理能力同时对于提高用户体验也有很好的帮助但是多线程程序也有其不利的一面我们也不要滥用线程
首先多线程程序会大大的提高我们程序的复杂度使得我们的开发和调试更加困难
其次过多的线程容易引发死锁数据同步等并发问题的发生
另外由于线程创建和销毁需要开销程序的整体性能可能因为过多线程的使用而下降
所以我们在使用线程时一定要谨慎本文对Eclipse线程的讨论希望能对大家使用线程有所帮助由于实际情况较为复杂文中所提到的方法仅供参考读者对于不同的实际问题需要进行具体分析从而找出最佳的解决方案