《JAVA:从入门到精通》part 21

二十四、多线程基础

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接口使其具有使用多线程的功能。


    实现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();
    }
}

运行结果:
线程同步之后的结果

你可能感兴趣的:(《JAVA:从入门到精通》part 21)