二十四、多线程基础
1. 线程简介
世间万物可以同一时间内完成多件事,例如人可以同时看书和听歌,这种思想运用在JAVA中被称为并发,而将并发完成的每一件事情称为线程。
在JAVA中,并发机制非常重要,但并不是所有的程序语言都支持线程。在以往的程序中,多以一个任务完成后再进行下一个项目的模式进行开发,这样下一个任务的开始必须等待前一个任务的结束。JAVA语言提供了并发机制,程序员可以在程序中执行多个线程,每一个线程完成一个功能,并与其他线程并发执行,这种机制被称为多线程。
-
Windows操作系统是多任务操作系统,它以进程为单位。一个进程是一个包含有自身地址的程序,每个独立执行的程序被称为进程,也就是正在执行的程序。系统可以分配给每个进程一段有限的使用CPU的时间(也可以称为CPU时间),CPU在这段时间中执行某个进程,然后下一个时间片又跳至另一个进程中去执行。由于CPU转换较快,所以使得每个进程好像是同时执行一样。
一个线程则是进程中的执行流程,一个进程中可以同时包括多个线程,每个线程也可以得到一小段程序的执行时间,这样一个进程就可以具有多个并发执行的线程。在单线程中,程序代码按照调用顺序依次往下执行,如果需要一个进程同时完成多段代码的操作,就需要多线程。
2. 实现线程的两种方式
- 在JAVA中主要提供两种方式实现线程,分别为继承java.lang.Thread类与实现java.lang.Runnable接口。
继承Thread类
- Thread类是java.lang包中的一个类,从这个类中Thre实例化的对象代表线程,若要启动一个新线程则需要建立Thread实例。Thread类中常用的两个构造方法如下:
public Thread():创建一个新的线程对象
public Thread(String threadName):创建一个名称为threadName的线程对象
- 完成线程真正功能的代码放在类的run()方法中,当一个类继承Thread类后,就可以在该类中覆盖run()方法,将实现该线程功能的代码写入run()方法中,然后同时调用Thread类中的start()方法执行线程,也就是调用run()方法。
下面看一个案例:
- 在控制台处打印出数字,先使用单线程方式打印。
package com.lzw;
public class dxc1
{
public static void main(String[] args)
{
Thread a=new threadA();
a.start();
}
}
class threadA extends Thread
{ @Override
public void run()
{
for (int i=0;i<100;i++)
{
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
- 由运行结果可以看到,单线程工作时与之前的效果完全一致,也是一个数字一个数字地打印出来。下面使用多线程工作方式在控制台处打印出数字和字母:
package com.lzw;
public class dxc1
{
public static void main(String[] args)
{
Thread a=new threadA();
a.start();
Thread b=new threadB();
b.start();
}
}
class threadA extends Thread
{ @Override
public void run()
{
for (int i=0;i<100;i++)
{
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class threadB extends Thread
{ @Override
public void run()
{
for (char j='a';j<'z';j++)
{
System.out.println(j);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
- 由运行结果可以看到,使用了多线程(该例子使用了两个线程)之后,控制台处的数字和字母是同时打印出来的。
实现Runnable接口
-
到目前为止,线程都是通过继承Thread类来创建的,如果需要继承其他类,而且还要使当前类实现多线程,那么可以通过Runnable接口来实现。例如,一个扩展JFrame类的GUI程序不可能再继承Thread类,因为JAVA语言不支持多继承,这时候就需要Runnable接口使其具有使用多线程的功能。
- 创建一个继承JFrame类的类,实现图标移动的功能,其中使用了Swing与线程结合的技术:
package lianxi1;
import java.awt.*;
import java.net.*;
import javax.swing.*;
public class SwingAndThread extends JFrame {
private static final long serialVersionUID = 1L;
private JLabel jl = new JLabel(); // 声明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.gif"); // 获取图片的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(1000); // 使线程休眠1000毫秒
} 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(); // 实例化一个SwingAndThread对象
}
}
运行结果:
- 在本实例中,为了使图标具有滚动功能,需要在类的构造方法中创建Thread实例。在创建该类的同时需要Runnable对象作为Thread类构造方法的参数,然后使用内部类形式实现run()方法。在run()方法中主要循环图标的横坐标位置,当图标横坐标到达标签最右边时,再次将图标的横坐标置于滚动的初始位置。
3. 线程的生命周期
-
线程具有生命周期,其中包含了七种状态、分别为出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。出生状态就是线程被创建时处于的状态,在用户使用该线程实例调用start()方法之前线程都处于出生状态;当用户调用start()方法后,线程就处于就绪状态;当线程得到系统资源后就进入运行状态。一旦线程进入可执行状态,它就会在就绪与运行状态下转换,同时也有可能进入等待、休眠、阻塞或死亡状态。
4. 操作线程的方法
线程的休眠
- 一种能控制线程行为的方法是调用sleep()方法,sleep()方法需要一个参数用于指定该线程休眠的时间,该时间以毫秒为单位。实际上线程的休眠的作用相当于暂停。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 由于sleep()方法的执行有可能抛出InterruptedException异常,所以将sleep()方法放在try-catch块中。
线程的加入
- 如果当前某程序为多线程程序,假设存在一个线程A,现在需要插入线程B,并要求线程B先执行完毕,然后再继续执行线程A,此时可以使用Thread类中的join()方法来完成。
- 创建一个继承JFrame类的类,该实例包括两个进度条,进度条的进度由线程来控制,通过使用join()方法使上面的进度条必须等待下面的进度条完成后才可以继续。
package lianxi1;
import java.awt.*;
import javax.swing.*;
public class JoinTest extends JFrame {
private static final long serialVersionUID = 1L;
private Thread threadA; // 定义两个线程
private Thread threadB;
final JProgressBar progressBar = new JProgressBar(); // 定义两个进度条组件
final JProgressBar progressBar2 = new JProgressBar();
int count = 0;
public static void main(String[] args) {
init(new JoinTest(), 100, 100);
}
public JoinTest() {
super();
// 将进度条设置在窗体最北面
getContentPane().add(progressBar, BorderLayout.NORTH);
// 将进度条设置在窗体最南面
getContentPane().add(progressBar2, BorderLayout.SOUTH);
progressBar.setStringPainted(true); // 设置进度条显示数字字符
progressBar2.setStringPainted(true);
// 使用匿名内部类形式初始化Thread实例子
threadA = new Thread(new Runnable() {
int count = 0;
public void run() { // 重写run()方法
while (true) {
progressBar.setValue(++count); // 设置进度条的当前值
try {
Thread.sleep(100); // 使线程A休眠100毫秒
threadB.join(); // 使线程B调用join()方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
threadA.start(); // 启动线程A
threadB = new Thread(new Runnable() {
int count = 0;
public void run() {
while (true) {
progressBar2.setValue(++count); // 设置进度条的当前值
try {
Thread.sleep(100); // 使线程B休眠100毫秒
} catch (Exception e) {
e.printStackTrace();
}
if (count == 100) // 当count变量增长为100时
break; // 跳出循环
}
}
});
threadB.start(); // 启动线程B
}
// 设置窗体各种属性方法
public static void init(JFrame frame, int width, int height) {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width, height);
frame.setVisible(true);
}
}
运行结果:
- 这个例子创建了两个线程,这两个线程分别负责进度条的滚动。在线程A的run()方法中使线程B的对象调用join()方法,而join()方法使当前运行线程暂停,直到调用join()方法的线程执行完毕后再执行,所以线程A等待线程B执行完毕后再开始执行,即下面的进度条滚动完毕后上面的进度条才开始滚动。
线程的中断
- 使用Thread类的interrupt()方法使线程离开run()方法,同时结束线程,但是线程会抛出InterruputedException异常,用户可以在处理异常时完成线程的中断业务处理。
package lianxi1;
import java.awt.*;
import javax.swing.*;
public class InterruptedSwing extends JFrame {
private static final long serialVersionUID = 1L;
Thread thread;
public static void main(String[] args) {
init(new InterruptedSwing(), 100, 100);
}
public InterruptedSwing() {
super();
final JProgressBar progressBar = new JProgressBar(); // 创建进度条
// 将进度条放置在窗体合适位置
getContentPane().add(progressBar, BorderLayout.NORTH);
progressBar.setStringPainted(true); // 设置进度条上显示数字
thread = new Thread(new Runnable() {
int count = 0;
public void run() {
while (true) {
progressBar.setValue(++count); // 设置进度条的当前值
try {
Thread.sleep(1000); // 使线程休眠1000豪秒
// 捕捉InterruptedException异常
} catch (InterruptedException e) {
System.out.println("当前线程序被中断");
break;
}
}
}
});
thread.start(); // 启动线程
thread.interrupt(); // 中断线程
}
public static void init(JFrame frame, int width, int height) {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width, height);
frame.setVisible(true);
}
}
运行结果:
5. 线程同步
- 在单线程程序中,每次只能做一件事,后面的事情需要等待前面的事情完成之后才可以进行,但是如果使用多线程,就会发生两个线程抢占资源的问题,甚至在某些情况会产生脏数据。所以在多线程编程过程中需要防止这些资源访问的冲突。JAVA提供了线程同步的机制来防止资源访问的冲突。
线程安全
- 如果线程不同步的话就会造成线程的不安全,例如火车票售卖系统,如果造成线程的不安全,则可能会出现车票数为负数的情况:
package lianxi1;
public class ThreadSafeTest implements Runnable {
int num = 10; // 设置当前总票数
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("剩余票数" + num--);
}
}
}
public static void main(String[] args) {
ThreadSafeTest t = new ThreadSafeTest(); // 实例化类对象
Thread tA = new Thread(t); // 以该类对象分别实例化4个线程
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
tA.start(); // 分别启动线程
tB.start();
tC.start();
tD.start();
}
}
运行结果:
- 可以看到剩余车票数出现了负数的情况,这就出现了线程不安全的情况。
线程同步机制
- 在JAVA中提供了同步机制,可以有效地防止资源冲突。同步机制用synchronized关键字。
package lianxi1;
public class ThreadSafeTest implements Runnable {
int num = 10;
public void run() {
while (true) {
synchronized ("") {
if (num > 0) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("剩余票数" + --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();
}
}