Java游戏编程---Java 线程,
Java游戏编程---Java 线程,
Java游戏编程
第一章 Java 线程
什幺是线程?
把执行多任务的服务员看成是计算机处理器,客户看成是任务。每个任务在自己的线程内进行,具有现代操作系统的处理器可以同时执行许多线程。例如,你或许在写一篇文章的同时还从互联网上下载文件。
现代操作系统通过将线程的任务划分成更小的块来实现同时执行线程。这称为并发。一个线程执行一段时间(时间片),另一个线程占先执行,等等,如图1.1所示。时间片足够小,就好象几件事情同时发生一样。
图1.1并发意味在一个处理器上执行多个线程
在多处理器计算机上,线程可能是同时执行的,这依赖于JVM的实现。
用Java创建和运行线程
设计Java时就考虑到了线程,因此,你会发现在Java中使用线程比许多其它语言更容易。要创建和启动一个新线程,只要创建一个Thead对象实例,调用start()方法:
Thead myThread=new Thread();
myThead.start();
当然,这段代码并没有做任何有意义的事情,因为没有给线程安排任务。JVM创建一个新的系统线程,启动这个线程,调用Thread对象的run()方法。缺省地,run()方法什幺也不做,于是线程死亡。
如果你要给线程分配任务,你肯定需要这样做的,就让run()方法来做。有三个基本方法:
l 扩展Thread类
l 实现Runnable接口
l 使用匿名内部类
扩展Thread类
给线程分配任务的一个快速方法就是扩展Thread类,重载run()方法:
public class MyThread extends Thread {
public void run() {
System.out.println("Do something cool here.");
}
}
然后,像前面一样创建和启动线程:
Thread myThread = new MyThread();
myThread.start();
现在,有两个线程在运行:main线程和上面启动的线程。
实现Runnable接口
扩展Thread类是容易的,但是很多时候你或许不想写一个新类,只想启动一个线程。例如,你或许想要一个类扩展了另一个类,并作为线程运行。在这种情况下,实现Runnable接口:
public class MyClass extends SomeOtherClass implements Runnable {
public MyClass() {
Thread thread = new Thread(this);
thread.start();
}
public void run() {
System.out.println("Do something cool here.");
}
}
在这个例子中,MyClass对象在构造时启动了一个新线程。在构造器中,Thread类以Runnable对象作为参数,线程启动时就执行Runnable。
使用匿名内部类
有时你想衍生一个新线程而不想创建一个新类,实现Runnable接口的方法或许不方便。这时,你可以用匿名内部类来启动一个新线程。
new Thread() {
public void run() {
System.out.println("Do something cool here.");
}
}.start();
这个例子虽然很简单,但是如果run()方法中的代码很长就会使可读性很差。尽量不要使用这个方法。
等待一个线程完成
如果你想要当前线程等待另一个线程完成,使用join()方法:
myThread.join();
当玩家退出游戏时,在执行cleanup时想要确认所有的线程都完成了,这或许是有用的。
线程睡眠
有时线程“累”了,你是好心的,让线程休息一会儿。你是不是在想,“什幺?线程也会累,这也太复杂了!”
当然,线程不会是真的累了。但是,你或许需要一个线程暂停一会儿,那就用static sleep()方法:
Thread.sleep(1000);
这个语句会使当前运行的线程睡眠1000毫秒,你也可以指定睡眠的时间。睡眠的线程不会消耗任何CPU时间------因此它甚至不做梦。
线程同步
很好,现在有了一些线程在运行,它们在执行各种任务。生活并不总是充满阳光和棒棒糖,如果有多个线程需要访问同样的对象或变量,你就会遇到线程同步问题。
为什幺需要同步?
假定你要编写一个迷宫游戏,任何线程都可以设置玩家的位置,任何线程都可以检查看看玩家是否退出。为了简单起见,假定退出位置在x=0,y=0。
public class Maze {
private int playerX;
private int playerY;
public boolean isAtExit() {
return (playerX == 0 && playerY == 0);
}
public void setPosition(int x, int y) {
playerX = x;
playerY = y;
}
}
大多数时候,这个代码没有问题。但是请记住,任何时候线程都可能被抢占。设想一下这种情况,玩家从(1,0)移动到(0,1)
1. 开始,对象变量是playerX=1,playerY=0
2. 线程A调用setPosition(0,1)
3. 代码行playerX=x;被执行,现在playerX=0
4. 线程A被线程B抢占
5. 线程B调用isAtExit()
6. 现在,playerX=0,playerY=0,因此isAtExit()返回true!
在这种情况下,报告说玩家走出了迷宫,实际上并没有。为了修正这个错误,需要确认setPosition()和isAtExit()方法没有同时执行。
如何同步
输入synchronized关键字。在迷宫例子中,如果你让方法是同步的,一次就只能有一个方法在运行。
下面是修改后的线程安全代码:
public class Maze {
private int playerX;
private int playerY;
public synchronized boolean isAtExit() {
return (playerX == 0 && playerY == 0);
}
public synchronized void setPosition(int x, int y) {
playerX = x;
playerY = y;
}
}
当JVM执行synchronized方法时,它获得了对象锁。在一个对象上一次只能获得一个锁。当方法执行完成正常返回或抛出异常时,锁被释放。
因此,如果一个synchronized方法拥有一个锁,这个锁被释放以后,其它的synchronized方法才能运行。
你可以把锁看作休息室房间门锁。房间内一次只能有一个人,当这个人离开后,或异常情况时这个人从窗户跳出时门才打开。
除了方法同步以外,对象也可以同步。
使用wait()和notify()
假定有两个线程需要互相沟通。例如,假定线程A等待线程B发送消息:
// Thread A
public void waitForMessage() {
while (hasMessage == false) {
Thread.sleep(100);
}
}
// Thread B
public void setMessage(String message) {
...
hasMessage = true;
}
虽然上面的代码行得通,但不是一个好的解决方案。线程A必须持续地每隔100毫秒就检查一下线程B是否发送了信息。线程A有可能睡过头了,没有及时获得信息。还有就是,如果几个线程等待消息又会发生什幺情况呢?当线程B发送消息时,线程A一直空闲等待通知不是更好吗?
幸运地是,wait()和notify()方法提供了这个功能。
wait()方法用在代码的同步块。当wait()方法执行时,锁被释放,线程等待通知。
notify()方法也用在代码的同步块。notify()方法通知等待同一个锁的线程。如果几个线程等待同一个锁,就随机地通知其中的一个线程。
下面是修改后的代码:
// Thread A
public synchronized void waitForMessage() {
try {
wait();
}
catch (InterruptedException ex) { }
}
// Thread B
public synchronized void setMessage(String message) {
...
notify();
}
当线程B调用了notify()以后,离开它的同步方法(释放了this上的锁),线程A获得锁,完成它的同步代码块。在这个例子中,它只是返回。
你也可以选择一个最长等待时间。wait()方法以最长等待时间作为参数:
wait(100);
如果线程一直没有被通知,这个语句相当于让线程睡眠了所指定的时间。不幸的是,没有办法判断wait()方法是因为超时还是因为被通知而返回。
第二个通知方法是notifyAll(),它通知所有等待锁的线程,而不只是通知一个线程。
Wait(),notify()和 notifyAll()方法都是Object类的方法,因此任何Java对象都有这些方法---就好象任何Java对象都可用作锁一样。
Java事件模型
你或许认为你的游戏只有一个线程,不必关心同步。但是,实际上不是这样的。所有的图形应用都至少有两个线程:main线程和AWT事件分配线程。
Main线程是程序的主线程,它在主类的main()方法中开始执行。
AWT事件分配线程处理用户输入事件:点击鼠标,按下键盘上的按键和窗口大小的调整等。我们将在后面的第三章,“交互和用户界面”中讨论这些输入事件,现在只要知道通过AWT事件分配线程,这些输入事件可以访问你的代码就行了。
何时使用线程
如果线程能够向用户提供更令人愉快的经历,就使用线程。这就是说,有时代码会延迟或需较长时间,那就在另一个线程中运行这段代码,使得用户不会感觉到游戏停止了。
例如,在下面的情况下就使用线程:
l 当从本地文件系统装载许多文件时
l 当进行网络通讯时,例如向服务器发送高分
l 当进行大量计算时,例如地形生成
何时不用线程
当你玩游戏时你会注意到许多事情在同时进行------敌人在奔跑,门被打开,子弹在飞等等。这使得有人会认为,“我知道,每个敌人都有自己的线程”,其实不是这样的。这样不仅浪费资源---同时运行太多的线程会消耗系统,而且会出现如下问题:
l 一个敌人会画到一半,会在两个不同的位置都有显示
l 每个线程的时间片会不平衡,导致不一致的运动(有时快,有时慢)
l 同步代码会导致不必要的延迟
有更有效的方法,可以同时做许多事情。我们将在将在下一章“2D图形和动画”讨论
线程池
现在已经有了Java线程的知识,让我们来创建一个有用的东西:线程池。线程池是一组线程。或许你想要限制同时进行网络或I/O连接的线程数目,或许你想控制系统中最多线程数。
列表1.1中的ThreadPool类使你可以选择线程池中的线程数,运行定义为Runnable的任务。下面是一个使用线程池的例子,创建一个有8个线程的线程池,运行一个简单任务,然后等待任务完成:
ThreadPool myThreadPool = new ThreadPool(8);
myThreadPool.runTask(new Runnable() {
public void run() {
System.out.println("Do something cool here.");
}
});
myThreadPool.join();
runTask()方法立即返回。如果池中的线程都在忙于执行任务,调用runTask()方法将会在队列中存储新的任务,直到有线程可以运行任务。
列表1.1ThreadPool.java
import java.util.LinkedList;
/**
A thread pool is a group of a limited number of threads that
are used to execute tasks.
*/
public class ThreadPool extends ThreadGroup {
private boolean isAlive;
private LinkedList taskQueue;
private int threadID;
private static int threadPoolID;
/**
Creates a new ThreadPool.
@param numThreads The number of threads in the pool.
*/
public ThreadPool(int numThreads) {
super("ThreadPool-" + (threadPoolID++));
setDaemon(true);
isAlive = true;
taskQueue = new LinkedList();
for (int i=0; i<numThreads; i++) {
new PooledThread().start();
}
}
/**
Requests a new task to run. This method returns
immediately, and the task executes on the next available
idle thread in this ThreadPool.
<p>Tasks start execution in the order they are received.
@param task The task to run. If null, no action is taken.
@throws IllegalStateException if this ThreadPool is
already closed.
*/
public synchronized void runTask(Runnable task) {
if (!isAlive) {
throw new IllegalStateException();
}
if (task != null) {
taskQueue.add(task);
notify();
}
}
protected synchronized Runnable getTask()
throws InterruptedException
{
while (taskQueue.size() == 0) {
if (!isAlive) {
return null;
}
wait();
}
return (Runnable)taskQueue.removeFirst();
}
/**
Closes this ThreadPool and returns immediately. All
threads are stopped, and any waiting tasks are not
executed. Once a ThreadPool is closed, no more tasks can
be run on this ThreadPool.
*/
public synchronized void close() {
if (isAlive) {
isAlive = false;
taskQueue.clear();
interrupt();
}
}
/**
Closes this ThreadPool and waits for all running threads
to finish. Any waiting tasks are executed.
*/
public void join() {
// notify all waiting threads that this ThreadPool is no
// longer alive
synchronized (this) {
isAlive = false;
notifyAll();
}
// wait for all threads to finish
Thread[] threads = new Thread[activeCount()];
int count = enumerate(threads);
for (int i=0; i<count; i++) {
try {
threads[i].join();
}
catch (InterruptedException ex) { }
}
}
/**
A PooledThread is a Thread in a ThreadPool group,
designed to run tasks (Runnables).
*/
private class PooledThread extends Thread {
public PooledThread() {
super(ThreadPool.this,
"PooledThread-" + (threadID++));
}
public void run() {
while (!isInterrupted()) {
// get a task to run
Runnable task = null;
try {
task = getTask();
}
catch (InterruptedException ex) { }
// if getTask() returned null or was interrupted,
// close this thread by returning.
if (task == null) {
return;
}
// run the task, and eat any exceptions it throws
try {
task.run();
}
catch (Throwable t) {
uncaughtException(this, t);
}
}
}
}
}
ThreadPool类使用的我们还没有提到的唯一概念是ThreadGroup类的使用。ThreadGroup正是你所想的---一组线程和修改线程的一些功能。例如,在上面的代码中,ThreadGroup的interrupt()方法用来在关闭线程池时终止所有等待线程。
现在,让我们用ThreadGroupTest类测试一下ThreadPool,如列表1.2所示。ThreadPoolTest启动线程池中的几个线程,并且还让ThreadPool执行几个任务。在这个例子中,每个任务只是向控制台输出消息。
这个程序以任务数和线程数作为参数。要运行测试程序,在命令行输入如下:
java ThreadPoolTest 8 4
当然,你可以任意指定任何任务数和线程数!
列表1.2ThreadPoolTest.java
public class ThreadPoolTest {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Tests the ThreadPool task.");
System.out.println(
"Usage: java ThreadPoolTest numTasks numThreads");
System.out.println(
" numTasks - integer: number of task to run.");
System.out.println(
" numThreads - integer: number of threads " +
"in the thread pool.");
return;
}
int numTasks = Integer.parseInt(args[0]);
int numThreads = Integer.parseInt(args[1]);
// create the thread pool
ThreadPool threadPool = new ThreadPool(numThreads);
// run example tasks
for (int i=0; i<numTasks; i++) {
threadPool.runTask(createTask(i));
}
// close the pool and wait for all tasks to finish.
threadPool.join();
}
/**
Creates a simple Runnable that prints an ID, waits 500
milliseconds, then prints the ID again.
*/
private static Runnable createTask(final int taskID) {
return new Runnable() {
public void run() {
System.out.println("Task " + taskID + ": start");
// simulate a long-running task
try {
Thread.sleep(500);
}
catch (InterruptedException ex) { }
System.out.println("Task " + taskID + ": end");
}
};
}
}
因为向控制台输出消息几乎不花时间,ThreadPoolTest通过睡眠500毫秒来模拟长时间运行的任务。
相关文章
- 暂无相关文章
用户点评