java多线程实例浅析.docx
- 文档编号:3884138
- 上传时间:2022-11-26
- 格式:DOCX
- 页数:23
- 大小:84.74KB
java多线程实例浅析.docx
《java多线程实例浅析.docx》由会员分享,可在线阅读,更多相关《java多线程实例浅析.docx(23页珍藏版)》请在冰豆网上搜索。
java多线程实例浅析
java多线程实例浅析
看了Thinkinginjava上的多线程后颇有感触,著文记之。
在这里我假设各位都已经拥有了awt和applet的初级知识。
所谓线程,即计算机进程内部的子执行模块,从具体表现上来说,就是独立分配到一部分系统资源而独立于其他可执行代码执行的可执行代码。
在java中,实现了语言上的多线程编程。
Thinkinginjava中举了一系列的几个例子来阐述其较为表层的特点。
对于一个拥有ui的程序来说,控制其ui的代码段一般运行在一个线程中,如果想要时刻刷新ui中某些控件的外观,则最好在另一个线程中进行操作,否则可能导致ui线程的阻塞,从而让用户感觉到应用程序停止了交互(例如点击按钮没有了反应等)。
例如下面这个取自Thinkinginjava的计时器案例:
1
package com.william.myclient;
2
3
import java.awt.*;
4
import java.awt.event.*;
5
import java.applet.*;
6
7
public class Counter1 extends Applet {
8
9
private int count = 0;
10
private Button onOff = new Button("Toggle"), start = new Button("Start");
11
private TextField t = new TextField(10);
12
private boolean runFlag = true;
13
14
public void init() {
15
add(t);
16
start.addActionListener(new StartL());
17
add(start);
18
onOff.addActionListener(new OnOffL());
19
add(onOff);
20
}
21
22
public void go() {
23
while (true) {
24
try {
25
Thread.currentThread().sleep(100);
26
}
27
catch (InterruptedException e) {
28
29
}
30
if (runFlag)
31
t.setText(Integer.toString(count++));
32
}
33
}
34
35
class StartL implements ActionListener {
36
public void actionPerformed(ActionEvent e) {
37
go();
38
}
39
}
40
41
class OnOffL implements ActionListener {
42
public void actionPerformed(ActionEvent e) {
43
runFlag = !
runFlag;
44
}
45
}
46
47
public static void main(String[] args) {
48
Counter1 applet = new Counter1();
49
Frame aFrame = new Frame("Counter1");
50
aFrame.addWindowListener(new WindowAdapter() {
51
public void windowClosing(WindowEvent e) {
52
System.exit(0);
53
}
54
});
55
aFrame.add(applet, BorderLayout.CENTER);
56
aFrame.setSize(300, 200);
57
applet.init();
58
applet.start();
59
aFrame.setVisible(true);
60
}
61
}
按下start,运行这个程序后会发现,当按下暂停键时,程序不会做出任何的响应——尽管我们已经给它设置了让运行停止的监听器。
而事实上是,除了
最大化最小化还有点反应外,基本上其他按键都已经失去响应了。
这个是为什么呢?
让我们从代码中取证。
12行定义了一个布尔值的flag变量,这个变量用于标识是否我们还需要更新TextField中的数值,从而让我们感觉是否计时器还在计时。
当为false时停
止计时,为true时继续计时——这是程序设计者的原意(你和我很清楚的知道,这个良好的意图泡汤了)。
22-33行是实现计时器功能的核心代码——一个叫做go的方法。
在这个方法里面有一个无限的循环,在循环里面先是通过Thread类的静态方法让当前线
程sleep了100毫秒,之后马上判断我们的flag是否为true,为true则进行TextField的内容更新,为false则停止更新。
35-39为一个内部类,这个内部类其实充当了Start键的监听器的角色。
当start键被单击时,这个监听器会被使用,并回调它里面的actionPerformed
方法。
在这个监听器里面的该方法中,直接调用了之前定义的go方法。
也就是说,当我们点击start按钮时,go方法会被调用,从而代表了我们的计时器开始
运行了。
41-45为另一个内部类的定义。
它是toggle键的监听器类。
在这个监听器里面设置了flag的值——将它取反。
也就是说,当点击toggle键时就会将flag
从true设为false(或从false设为true),这样go方法中的循环便会在紧接着的一次循环中察觉到这个改变,从而做出停止更新(或开始更新)TextField
的操作,从而看起来像是停止(或开始)了计时工作。
以上的分析从某种意义上来说,是从设计者的理想上面出发的。
真正运行这个小程序时你会发现,一旦开始之后,我们的start键、toggle键以及关闭窗
口键都已经失去了响应——情况糟糕透了。
为什么会这样呢?
我无法顺利关闭它,也无法停止它——简直是糟透了
还记得刚开始说的那段话么?
这个包含简易ui的小程序的ui控制都是包含在一个线程里面的——也就是刚才我们展示的整个代码所被执行的地方。
当我们
开始了go方法时,这个线程就专注于处理这一段代码的执行去了。
而处在同一个线程中的Button的监听等操作则被“繁忙”的线程“忽略”掉了。
而且更糟糕
的是,go方法里面是一个无限循环——也就是说,一旦我们按下了start键开始了go方法的执行,这整个代码被执行的线程便会永远的专注于这个贪婪的go方
法而无暇顾及其他的代码段——无论你是按下start键还是按下toggle键,都没有响应,因为它们不在go的考虑范围之内。
你可以想象自己是一个扫描代码的
机器,并且跟着整个代码进行一次“执行”。
到后来你会发现自己在go这个地方不断地上下点头,上下点头——这时如果一个朋友告诉你还有start监听的部
分需要你照看,你会很不耐烦的说,go这个地方已经把你完全占用了,你已经无力分身去顾及了。
刚才的代码就是一个典型的线程占用过度的例子。
这在很多包含ui的程序——例如android程序——里有体现。
例如android新手往往会把一个长时间的
http请求和ui管理放在同一个线程中,这样便导致了ui无法及时响应用户操作,导致用户觉得你的应用很慢——甚至怀疑自己机器的处理能力(-_-)。
那怎么解决刚才的那个问题呢?
答案正是本文的主题——多线程机制。
紧接下来请看如下代码:
1
package com.william.myclient;
2
3
import java.awt.*;
4
import java.awt.event.*;
5
import java.applet.*;
6
7
class SeparateSubTask extends Thread {
8
private int count = 0;
9
private Counter2 c2;
10
private boolean runFlag = true;
11
12
public SeparateSubTask(Counter2 c2) {
13
this.c2 = c2;
14
start();
15
}
16
17
public void invertFlag() {
18
runFlag = !
runFlag;
19
}
20
21
public void run() {
22
while (true) {
23
try {
24
sleep(100);
25
}
26
catch (InterruptedException e) {
27
28
}
29
if (runFlag)
30
c2.t.setText(Integer.toString(count++));
31
}
32
}
33
}
34
35
public class Counter2 extends Applet {
36
TextField t = new TextField(10);
37
private SeparateSubTask sp = null;
38
private Button onOff = new Button("Toggle"), start = new Button("Start");
39
40
public void init() {
41
add(t);
42
start.addActionListener(new StartL());
43
add(start);
44
onOff.addActionListener(new OnOffL());
45
add(onOff);
46
}
47
48
class StartL implements ActionListener {
49
public void actionPerformed(ActionEvent e) {
50
if (sp == null)
51
sp = new SeparateSubTask(Counter2.this);
52
}
53
}
54
55
class OnOffL implements ActionListener {
56
public void actionPerformed(ActionEvent e) {
57
if (sp !
= null)
58
sp.invertFlag();
59
}
60
}
61
62
public static void main(String[] args) {
63
Counter2 applet = new Counter2();
64
Frame aFrame = new Frame("Counter2");
65
aFrame.addWindowListener(new WindowAdapter() {
66
public void windowClosing(WindowEvent e) {
67
System.exit(0);
68
}
69
});
70
aFrame.add(applet, BorderLayout.CENTER);
71
aFrame.setSize(300, 200);
72
applet.init();
73
applet.start();
74
aFrame.setVisible(true);
75
}
76
}
正如刚才所说,我们的解决之道是利用java语言特性里面本身就已经包含的多线程机制。
java里面负责多线程的主要有一个接口和一个类。
接口就是java.lang.Runable接口,类就是java.lang.Thread类。
其实Thread是一个实现了Runa
ble接口的实现类,它包含了Runable接口的一些默认实现以及一些其他的工具类方法(例如开始线程的方法)供开发人员使用。
Runable接口只定义一个方法:
voidrun()。
在run方法中的代码段就是我们自定义的线程所要单独执行的代码段。
既然这些都明了了,那我们就去看看刚才的代码。
7-33行是一个继承自Thread类的新的线程类。
在这个类里面我们重写了Thread的run方法,从而让我们自己的线程类拥有我们想要的线程行为。
8-10为
该线程类所包含的一些必要字段。
count就是我们将要不断自增后显示出来的时间值。
c2是一个Counter2的引用,负责将我们的线程和Counter2类的示例联
系起来,从而可以让线程类能够更新Counter2示例中的TextField的显示——这段代码见30行。
另外在12-15行定义了我们线程类的构造器,我们先将c2进
行了初始化赋值,以将我们即将构造的线程类的示例和某个Counter2示例“挂钩”起来,紧接着我调用了SeparateSubTask的父类方法start()方法,从而
启动了线程——如此一来,我们构造SeparateSubTask的同时也启动了线程,无需再单独去调用start()方法来启动线程。
另外30行利用c2引用来获取Coun
ter2类实例中的TextField示例引用,从而改变TextField的显示的手法也是我们需要理解的。
35-76行我们定义了一个Counter2类,这个类和刚才的Counter1类无多大差别——除了37行我们保存了一个需要利用到的SeparateSubTask的引用以
及50-51行我们将这个引用指向了一个新构建的SeparateSubTask类实例(该新线程实例构建即马上运行)。
通过以上代码,我们便可以保证按钮响应顺利的同时,TextField的显示更新同时顺利进行的效果。
当我按下Toggle键时,数字很听话地停在了52.
为什么会这样呢?
思考后我们发现,ui管理的线程和计时更新的线程通过刚才的代码被成功地分配到两个独立的线程中去了——一个是Counter2所在线
程,另一个是SeparateSubTask所在线程——它们两个互不影响,各自做着各自的事情。
另外你会发现,刚才那个代码很好的将界面和功能进行了分模块化构建——较为符合MVC的设计思想。
由于方才的SeparateSubTask和Counter2类之间存在着相互引用的“亲密”关系,所以我们不妨就让它们成为一家子——让SeparateSubTask成为Co
unter2的内部类。
相关代码如下:
1
package com.william.myclient;
2
3
import java.awt.*;
4
import java.awt.event.*;
5
import java.applet.*;
6
7
public class Counter2i extends Applet {
8
private class SeparateSubTask extends Thread {
9
int count = 0;
10
boolean runFlag = true;
11
12
SeparateSubTask() {
13
start();
14
}
15
16
public void run() {
17
while (true) {
18
try {
19
sleep(100);
20
} catch (InterruptedException e) {
21
22
}
23
if (runFlag)
24
t.setText(Integer.toString(count++));
25
}
26
}
27
}
28
29
private SeparateSubTask sp = null;
30
private TextField t = new TextField(10);
31
private Button onOff = new Button("Toggle"), start = new Button("Start");
32
33
public void init() {
34
add(t);
35
start.addActionListener(new StartL());
36
add(start);
37
onOff.addActionListener(new OnOffL());
38
add(onOff);
39
}
40
41
class StartL implements ActionListener {
42
public void actionPerformed(ActionEvent e) {
43
if (sp == null)
44
sp = new SeparateSubTask();
45
}
46
}
47
48
class OnOffL implements ActionListener {
49
public void actionPerformed(ActionEvent e) {
50
if (sp !
= null)
51
sp.runFlag = !
sp.runFlag; // invertFlag();
52
}
53
}
54
55
public static void main(String[] args) {
56
Counter2i applet = new Counter2i();
57
Frame aFrame = new Frame("Counter2i");
58
aFrame.addWindowListener(new WindowAdapter() {
59
public void windowClosing(WindowEvent e) {
60
System.exit(0);
61
}
62
});
63
aFrame.add(applet, BorderLayout.CENTER);
64
aFrame.setSize(300, 200);
65
applet.init();
66
applet.start();
67
aFrame.setVisible(true);
68
}
69
}
70
这份代码和刚才的代码运行效果是一致的——不信你试试。
需要提醒的是,当两个类之间的耦合关系确实很是紧密的时候,内部类往往是一个不错的选
择。
另外还有另一种形式的处理方法:
将线程类和ui控制类组合到一起。
具体手段则是构建一个Counter3类,让它继承自Applet类并且实现Runable接口
(java中是禁止多继承的
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- java 多线程 实例 浅析