学习多线程的初衷是期望本学期能够开发出一个入门版的 Tank War 。作为初学者,我不得不从基础概念抓手,结合其基本框架,通过简单的案例展现来得到可观的进步和提升
-
1.多线程的基本概念
-
1.1 线程定义
-
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程。线程有时也被称为轻量级进程。例如,当你运行一个软件时,这个软件本身就是一个进程,而进程中的多个任务,如数据读取、界面绘制等,可能由不同的线程来执行。
-
-
1.2 进程,线程?
-
进程是程序运行的实例,是系统进行资源分配和调度的基本单位。而线程是进程中的一个实体,它共享进程的资源,如内存地址空间、文件句柄等。线程的创建、切换和销毁比进程要轻量得多。在同一进程中,线程之间的通信比进程之间要简便,因为它们共享内存空间。
-
-
1.3 线程的基本特征
-
轻量级:线程的创建和销毁开销较小,切换速度也比进程快很多,因为它们不需要涉及用户态和内核态的切换。
-
并发性:多线程可以在单个进程中实现并发执行,提高程序的响应速度和效率。在多核处理器系统中,可以真正实现并行运行。
-
-
-
2. 多线程的基本结构
-
2.1线程的生命周期
- 对于多线程的生命周期可以简单地理解为上图
-
2.1.1 新建(NEW)
-
定义:线程对象被创建,但还未启动。
-
特点:
-
此时线程已经分配了内存空间,但还没有分配CPU时间片。线程不会执行任何任务,直到调用
start()
方法。
-
- 示例(以我的项目)
-
public class GameThread extends Thread{ ...... } public GameThread gt; //创建线程对象 gt = new GameThread(g, balls);
-
-
-
2.1.2 就绪 (Stand By)
-
定义:线程已经准备好运行,等待操作系统分配CPU时间片
-
特点:
-
线程在就绪队列中等待调度。
-
操作系统会根据调度算法选择线程执行。
-
-
- 示例:
-
//启动线程,每个线程只能启动一次 gt.start();
-
-
-
2.1.3 运行(Running)
- 定义:线程正在执行任务。
-
特点:
-
此时线程获得了CPU时间片,执行线程体中的代码。
-
线程可能会因为运行时间片用完、调用
yield()
方法或者发生I/O操作等原因进入阻塞状态。
-
- 使用:
-
run()
方法的作用-
在多线程编程中,
run()
方法是线程执行的核心逻辑。当线程被启动(通过start()
方法)时,JVM 会调用线程的run()
方法,从这里开始执行线程的逻辑任务。 -
线程中的主操作代码都应写在
run()
方法中,你可以在这里实现线程需要完成的所有任务。
-
-
线程的启动与执行
-
当你调用
thread.start()
时,JVM 会为线程分配资源,并将其置于就绪状态(Runnable)。一旦线程获得 CPU 时间片,它就会调用run()
方法,开始执行线程的任务。 -
run()
方法中的代码是线程的主体部分,它会一直执行,直到方法执行完毕或者被明确终止。
-
-
代码示例
-
public void run() { System.out.println(Thread.currentThread().getName() + ": 线程运行中..."); while (true) { //延时 try { Thread.sleep(10); } catch (InterruptedException ex) { throw new RuntimeException(ex); } for (int i = 0; i < balls.size(); i++) { Ball ball = balls.get(i); ball.drawBall(g); } } }
-
-
-
2.1.4 阻塞(Blocked)
-
定义:线程暂时停止执行,等待某种条件满足。
-
原因:
-
等待锁资源:线程尝试获取一个被其他线程持有的锁。
-
I/O操作:线程等待I/O操作完成(如文件读取、网络请求)。
-
线程模拟睡眠:调用
sleep()
方法导致线程阻塞一段时间。
-
- 代码示例:
-
while (true) { //延时 try { Thread.sleep(15); } catch (InterruptedException ex) { throw new RuntimeException(ex); }
-
-
-
2.1.5 等待(Waiting)
-
定义:线程处于等待状态,等待其他线程的通知。
-
原因:
-
调用
wait()
方法进入等待状态,直到其他线程调用notify()
或notifyAll()
唤醒。 -
进入等待状态后,线程释放锁资源。
-
- 触发方式
-
主要通过以下几种方式进入等待状态:
-
调用
Object.wait()
方法。 -
调用
Thread.join()
方法。 -
调用
Lock
或Condition
相关的等待方法(如Condition.await()
)。
-
-
- 特点
-
等待中的线程不会自动结束等待,必须等待其他线程的通知(如
Object.notify()
或Object.notifyAll()
)才会从等待状态退出。 -
等待中的线程会释放之前持有的对象锁,以便其他线程可以获取该锁并执行相关操作。
-
处于等待状态的线程不会消耗CPU资源,因为它不会执行任何任务。
-
- 退出等待状态的方式
-
通过
notify()
或notifyAll()
方法:-
如果线程是通过调用
Object.wait()
进入的等待状态,那么其他线程需要通过调用同一个对象的notify()
方法唤醒一个等待线程,或者调用notifyAll()
唤醒所有等待线程。
-
-
线程中断:
-
如果等待中的线程被中断(通过
Thread.interrupt()
方法),它会抛出InterruptedException
并退出等待状态。因此,在编写代码时需要捕获这个异常并进行处理。
-
-
- 代码示例:
-
public void run() { System.out.println(Thread.currentThread().getName() + ": 线程运行中..."); while (true) { synchronized (lock) { //使用 synchronized 块确保线程安全。 while (!running) { try { lock.wait();//等待唤醒 } catch (InterruptedException e) { throw new RuntimeException(e); } } for (int i = 0; i < balls.size(); i++) { Ball ball = balls.get(i); ball.drawBall(g); } } //延时 try { Thread.sleep(10); } catch (InterruptedException ex) { throw new RuntimeException(ex); } } } public void pause() {//暂停线程 synchronized (lock) { running = false; } } public void resumeThread() {//唤醒线程 synchronized (lock) { running = true; lock.notify();//唤醒线程 } }
-
@Override public void mouseClicked(MouseEvent e) { int x = e.getX(); int y = e.getY(); //判断鼠标左右建 int key = e.getButton(); if (key == 1) { //左键 ball = new Ball(x, y, 50); balls.add(ball); } else if (key == 3) {//右键 gt.pause(); } else if (key == 2) {//滚轮 gt.resumeThread(); } }
-
-
-
2.1.6 超时等待状态(TimedWaiting)
-
定义:超时等待状态是指线程在等待某个条件时设置了超时时间。如果在指定的时间内没有满足条件,线程会自动退出等待状态。
- 触发方式:
-
主要通过以下几种方式进入超时等待状态:
-
调用
Object.wait(long timeout)
方法。 -
调用
Thread.sleep(long millis)
方法。 -
调用
Lock
或Condition
相关的超时等待方法(如Condition.await(long timeout, TimeUnit unit)
)。
-
-
- 特点:
-
与普通等待状态相比,超时等待状态具有时间限制,线程不会无限期地等待。
-
超时等待中的线程同样会释放之前持有的对象锁(如果是通过
Object.wait
或Condition.await
进入的话)。 -
如果在超时时间到达之前满足了条件(如被其他线程唤醒),线程会提前退出超时等待状态。
-
- 退出超时等待
-
条件满足:
-
如果其他线程在超时时间到达之前调用了唤醒方法(如
notify()
或notifyAll()
),则线程会提前退出超时等待状态。
-
-
超时时间到达:
-
如果在设置的超时时间内没有满足条件,线程会自动退出超时等待状态。
-
-
线程中断:
-
如果等待中的线程被中断,它会抛出
InterruptedException
并退出超时等待状态。
-
-
-
-
2.1.7 死亡(Dead)
-
定义
-
死亡状态是指线程已经完成了它的任务,或者由于某些原因(如异常、强制终止等)而停止运行。
-
一旦线程进入死亡状态,它就无法再次启动,线程对象的生命周期结束。
-
- 原因
- 1. 正常完成 “run()” 任务
- 2. 因异常终止
- 3. 线程死亡的终点
-
不可恢复性:一旦线程进入死亡状态,它就无法再次启动。如果需要重新执行任务,必须创建一个新的线程对象。
-
资源释放:线程死亡后,系统会回收线程占用的资源(如栈空间、线程本地存储等)。
-
线程对象仍然存在:虽然线程已经死亡,但线程对象仍然存在于内存中,直到被垃圾回收器回收。
-
- 注意
-
避免使用
Thread.stop()
-
Thread.stop()
方法会强制终止线程,可能导致资源泄漏或数据不一致。例如,如果线程持有某个对象的锁,调用stop()
方法后,锁不会被释放,可能导致其他线程永远无法获取该锁。 -
推荐使用标志位(如
volatile boolean running
)来优雅地终止线程。
-
-
处理未捕获的异常
-
如果线程因未捕获的异常而终止,可能会导致程序的不稳定。可以通过
Thread.setUncaughtExceptionHandler()
方法为线程设置异常处理器,捕获并处理未捕获的异常。
-
-
线程死亡后的资源清理
-
在线程死亡之前,确保释放所有占用的资源,如关闭文件、释放锁等。
-
可以在
run()
方法的最后或通过finally
块进行资源清理。
-
-
- 总结
-
线程死亡是线程生命周期的终点,线程完成任务或因异常、强制终止而进入死亡状态。
-
线程死亡后无法再次启动,如果需要重复执行任务,必须创建新的线程。
-
在编程中,应尽量避免使用
Thread.stop()
方法,推荐使用标志位来优雅地终止线程。 -
处理未捕获的异常和清理资源是线程编程中的重要注意事项,确保程序的稳定性和可靠性。
-
-
-
2.2 线程的同步与互斥
-
- s