JavaSE进阶篇【7】
2022年7月3日
第七部分 多线程
7.1 线程简介
在 Java 语言中,并发机制非常重要,但不是所有程序语言都支持线程。在以往的编程语言中,多以一个任务完成后再执行下一个项目的模式进行开发,这样下一个任务的开始必须等待前一个任务的结束。Java 语言提供了并发机制,程序员可以在程序中执行多个线程,每一个线程完成一个功能,并与其他线程并发执行,这种机制被称为多线程。
Java 中的多线程在每个操作系统中的运行方式也存在差异的。用 Windows 系统下来说,Windows 操作系统是多任务操作系统给,它以进程为单位,一个进程是一个包含有自身地址的程序,每个独立执行的程序都称为进程,也就是正在执行的程序。系统可以分配给每个进程一段有限的使用 CPU 的时间(也可以称为 CPU 时间片),CPU 在这段时间中执行某个进程,然后下一个时间片又跳至另一个进程中去执行。(由于 CPU 转换较快,所以使得每个进程好像是同时执行一样)
一个线程则是进程中的执行流程,一个进程中可以同时包括多个线程,每个线程也可以得到一小段程序的执行时间,这样一个进程就可以具有多个并发执行的线程。在单线程中,程序代码按调用顺序依次往下执行,如果需要一个进程同时完成多段代码的操作,就需要产生多线程。
7.2 实现线程的两种方式
1. 继承 Thread 类
Thread 类是 java.lang 包中的一个类,从这个类中实例化的对象代表多线程,启动一个新线程需要建立 Thread 实例。Thread 类中常用的两个构造方法如下:
- public Thread() 方法:创建一个新的线程对象
- public Thread(String threadName) 方法:创建一个名称为 threadName 的线程对象
继承 Thread 类创建以一个新的线程的语法如下:
public class ThreadTest extends Thread{
}
完成线程真正功能的代码放在类的 run() 方法中,当一个类继承 Thread 类后,就可以在该类中覆盖 run() 方法,将实现该线程功能的代码写入 run() 方法中,然后同时调用 Thread 类中的 start() 方法执行线程,也就是调用 run() 方法。
Thread 对象需要一个任务来执行,任务是指线程在启动时执行的工作,该工作的功能代码被写在 run() 方法中。run() 方法必须遵循以下的语法格式:
public void run(){
}
当执行一个线程程序时,就自动产生一个线程,主方法正是在这个线程上运行的。当不再启动其他线程时,该程序就为单线程程序。下面看一个继承 Thread 类的实例。
package part7;
public class ThreadTest extends Thread {
private int count = 10;
public void run() {
while(true) {
System.out.print(count+" ");
if(--count==0) {
return;
}
}
}
public static void main(String[] args) {
new ThreadTest().start();
}
}
在上述实例中,继承了 Thread 类,然后在类中覆盖了 run() 方法。通常在 run() 方法中使用无限循环的形式,使得线程一直运行下去,所以要指定一个跳出循环的条件,如本实例中使用变量 count 递减为 0 作为跳出循环的条件。
在 main 方法中,使线程执行需要调用 Thread 类中的 start() 方法,start() 方法调用被覆盖的 run() 方法,如果不调用 start() 方法,线程永远不会启动,在主方法没有调用 start() 方法之前,Thread 对象只是一个实例,而不是一个正真的线程。
2. 实现 Runnable 接口
如果需要继承其他非 Thread 类,而且还要使当前类实现多线程,那么可以通过 Runnable 接口来实现。实现 Runnable 接口的语法如下:
public class Thread extends Object implements Runnable;
实现 Runnable 接口的程序会创建一个 Thread 对象,并将 Runnable 对象与 Thread 对象相关联。Thread 类中有以下两个构造方法:
- public Thread(Runnable target)
- public Thread(Runnable target, Striung name)
使用 Runnable 接口启动新的线程的步骤如下:
① 创建 Runnable 接口的类,建立 Runnable 对象
② 使用参数为 Runnable 对象的构造方法创建 Thread 实例
③ 调用 start() 方法启动线程
线程最引入注目的部分应该时与 Swing 相结合创建 GUI 的功能。如下面的一个例子:
package part7;
import java.awt.Container;
import java.net.URL;
import javax.swing.*;
public class SwingAndThread extends JFrame {
private JLabel jl = new JLabel();
private static Thread t;
private int count = 0;
private Container container = getContentPane(); //声明容器
public SwingAndThread() {
setBounds(300,200,250,100); //绝对定位窗体大小与位置
container.setLayout(null); //使窗体不使用任何布局管理器
URL url = SwingAndThread.class.getResource("1.png"); //获取图片的 URL(与该类在同一路径下即可)
Icon icon = new ImageIcon(url); //实例化一个 Icon
jl.setIcon(icon); //将图标放置在标签中
jl.setHorizontalAlignment(SwingConstants.LEFT); //设置图片在标签的最左方
jl.setBounds(10, 10, 200, 50); //设置标签的位置与大小
jl.setOpaque(true);
t = new Thread(new Runnable() { //定义内部匿名类,该类实现 Runnable接口
public void run() { //重写 run()方法
while(count<=200) { //设置循环条件
jl.setBounds(count, 10, 200, 50); //将标签的横坐标用向量表示
try {
Thread.sleep(10); //使线程休眠 10ms
}catch(Exception e) {
e.printStackTrace();
}
count += 4; //使横坐标每次增加 4
if(count==200) { //当图标达到最右边时,使其返回到标签的最左边
count = 10;
}
}
}
});
t.start(); //启动线程
container.add(jl);//将标签添加到容器中
setVisible(true);//使窗体可见
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
public static void main(String[] args) {
new SwingAndThread();
}
}
7.3 线程的生命周期
线程具有生命周期,其中包括 7 种状态,分别为 出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。出生状态就是线程被创建时处于的状态,在用户使用该线程实例调用 start() 方法之前线程都处于出生状态;当用户调用 start() 方法之后,线程就处于就绪状态(又被称为可执行状态);当线程得到系统资源之后就进入运行状态。
一旦线程进入可执行状态,它会在就绪与运行状态下转换,同时也有可能进入等待、休眠、阻塞或死亡状态。当处于运行状态下的线程调用 wait() 方法时,该线程便进入等待状态,进入等待状态的线程必须调用 Thread 类种的 notify() 方法才能被唤醒,而 notifyAll() 方法是将所有处于等待状态下的线程唤醒;当线程调用 Thread 类中的 sleep() 方法时,则会进入休眠状态。如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态,在其等待输入/输出结束时线程进入就绪状态,对于阻塞的线程来说,即使系统资源空闲,线程依然不能回到运行状态。当线程的 run() 方法执行完毕时,线程进入死亡状态。
从上图中可以总结出使线程处于就绪状态有以下几种方法:
① 调用 sleep() 方法
② 调用 wait() 方法
③ 等待输入/输出完成
当线程处于就绪状态后,可以用以下几种该方法使线程再次进入运行状态:
① 线程调用 notify() 方法
② 线程调用 notifyAll() 方法
③ 线程调用 interrupt() 方法
④ 线程的休眠时间结束
⑤ 输入/输出结束
7.4 操作线程的方法
1. 线程的休眠
一种能控制线程的行为对的方法是调用 sleep() 方法,sleep() 方法需要一个参数用于指定该线程的休眠的时间,该事件以毫秒为单位。sleep() 方法通常是在 run() 方法内的循环中被使用。
sleep() 方法的语法如下:
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
上述代码会使得线程在 2 秒内不会进入休眠状态。由于 sleep() 方法的执行有可能抛出 InterruptedException 异常,但是并不能保证它醒来后进入运行状态,只能保证它进入就绪状态。
2. 线程的加入
如果当前程序为多线程程序,加入现在存在线程 A,现在需要插入线程 B,并要求线程 B 先执行完毕,然后再继续执行线程 A,此时可以使用 Thread 类中的 join() 方法来完成。
当某个线程使用 join() 方法加入到另一个线程时,另一个线程会等待该线程执行完毕后再继续执行。
用法可以参考以下的代码片段:
try{
Thread.sleep(2000);
threadB.join();
}catch(InterruptedException e){
e.printStackTrace();
}
3. 线程的中断
线程的中断如今都提倡在 run() 方法中使用无限循环的形式,然后使用一个布尔型标记控制循环的停止。如下面的代码片段所示:
public void run(){
while(true){
//...
try{
Thread.sleep(2000);
threadB.join();
}catch(InterruptedException e){
e.printStackTrace();
if(isContinue) //当 isContinue变量为 true时,中断线程
break;
}
}
}
4. 线程的礼让
Thread 类中提供了一种礼让方法,使用 yield() 方法表示,它只是给当前正处于运行状态的线程一个提醒,告知它可以将资源礼让给其他线程,但并没有强制性。
yield() 方法使得具有同样优先级的线程有进入可执行状态的机会,当当前线程放弃执行权时就会重新回到就绪状态。
7.5 线程的优先级
每个线程都具有各自的优先级,线程的优先级可以表明在程序中该线程的重要性以及执行的先后顺序。
Thread 类中包含的成员变量代表了线程的某些优先级,如 Thread.MIN_PRIORITY(常数1)、Thread.MAX_PRIORITY(常数10)、Thread.NORM_PRIORITY(常数5)等。其中每个线程的优先级都在 Thread.MIN_PRIORITY~Thread.MX_PRIORITY 之间,在默认情况下优先级都是 Thread.NORM_PRIORITY。每个新产生的线程都继承父线程的优先级。
在多任务操作系统中,每个线程都会得到一小段 CPU 时间片运行,在时间结束时,将轮换另一个线程进入运行状态,这时系统会选择与当前线程优先级相同的线程予以运行。系统始终选择就绪状态下优先级较高的线程进入运行状态。
线程的优先级可以使用 setPriority() 方法来调整,如果使用该方法设置的优先级不在 1~10 之间,将产生 IllegalArgumentException 异常。
public static void setPriority(String threadName, int priority, Thread t){
t.setPriority(priority);
t.setName(threadName);
t.start();
}
7.6 线程的同步
在单线程程序中,每次只能做一件事,后面的事情需要等待前面的事情完成之后才可以进行,但如果是使用多线程程序,就会发生两个线程抢占资源的问题。所以在多线程编程中需要防止这些资源访问的冲突。Java 提供了线程同步的机制来防止资源访问的冲突。
在 Java 中提供了同步机制,可以有效地防止资源冲突。同步机制使用 synchronized 关键字。如下面的例子所示:
package part7;
public class ThreadSafeTest implements Runnable{
int num = 10;
public void run() {
while(true) {
synchronized("") {
if(num>0) {
try {
Thread.sleep(100);
}catch(Exception e) {
e.printStackTrace();
}
System.out.println("tickets "+--num);
}
}
}
}
public static void main(String[] args) {
ThreadSafeTest t = new ThreadSafeTest();
Thread tA = new Thread(t);
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
tA.start();
tB.start();
tC.start();
tD.start();
}
}
运行结果如下图所示:
从结果中可以看出,打印到最后的票数没有出现负数,这是因为将资源放置在了同步块中。这个同步块也被称为临界区,它使用 synchronized 关键字建立,其语法如下
synchronized(Object){
}
通常将共享资源的操作放置在 synchronized 定义的区域内,这样当其他线程也获取到这个锁时,必须等待锁被释放时才能进入该区域。Object 为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为 0 与 1,一个线程运行到同步块时首先检查该对象的标志位,如果为 0 状态,表明此同步块中存在其他线程在运行。这时该线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码为止。这时该对象的标志位被设置为 1,该线程才能执行同步块中的代码,并将 Object 对象的标志位设置为 0,防止其他线程执行同步块中的代码。
此外,同步方法就是在方法前面修饰 synchronized 关键字的方法,定义如下:
synchronized void f(){}
当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为 synchronized,否则就会出错。