JavaSE 多线程(二)

1. 创建线程

类 Thread 的构造方法如下:

public Thread(ThreadGroup group, Runnable target, String name)

其中,group 指明了线程所属的线程组;target 是线程体 run() 方法所在对象;name 是线程的名称。

target 必须实现接口 Runnable。 在接口 Runnable 中只定义了一个方法 void run() 作为线程体。热河实现接口 Runnable 的对象都可以作为一个线程的目标对象。

类 Thread 本身也实现了接口 Runnable,因此,上述构造方法中各参数都可以为 null。

在 Java 中,用两种方法创建线程,下面将分别介绍。

1.1 创建线程的方法一 ————继承 Thread 类

java.lang.Thread 是 Java 用来表示线程的类,其中所定义的许多方法为完成现成的处理工作也提供了比较完整的功能。如果将一个类定义为 Thread 的子类,那么这个类也就可以用来表示线程。

定义一个线程类,它继承类 Thread 并重写其中的方法 run()。这时在初始化这个类的实例时,目标对象 target 可以为 null,表示这个实例本身具有线程体。由于 Java 只支持单继承,用这种方法定义的类不能再继承其他类。

用 Thread 类的子类创建线程的过程包括以下三步:
(1)从 Thread 类派生出一个子类,在类中一定要实现 run()。

class Lefthand extends Thread {
      public void run() {
      ...
      }
}

(2)用该类创建一个对象;

Lefthand left = new Lefthand();

(3)用 start() 方法启动线程:

left.start();

在程序中实现多线程,关键性操作包括:定义用户线程操作,即实现 run() 方法,并在适当的时候启动线程。

public class myThread extends Thread {
    @Override
    public void run() {
        //执行若干操作
        try {
            sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Thread t = new myThread();
        //执行若干操作
    }
}

程序只用到一个类————myThread。应用这种形式的构造方法创建线程对象时不用给出任何参数。这个类有一个重要的方法————public void run(),这个方法称为 线程体,它是整个线程的核心,线程所要完成的任务的代码都定义在线程体中,实际上不同功能的线程之间的区别就在于它们的线程体不同。

1.2 创建线程的方法二 ————实现 Runnable 接口

Runnable 是 Java 中用以实现线程的接口,从根本上讲,任何实现线程功能的类都必须实现该接口。前面所用到的 Thread 类实际上就是因为实现了 Runnable 接口,所以它的子类才具有了线程功能。

Runnale 接口中只定义了一个方法就是 run() 方法,也就是线程体。用 Runnable() 接口实现多线程时,也必须实现 run() 方法,也需用 start() 启动线程,但此时常用 Thread 类的构造方法来创建线程对象。

Thread 的构造方法中包含有一个 Runnable 实例的参数,这就是说,必须定义一个实现 Runnable 接口的类并产生一个该类的实例,对该实例的引用就是适合于这个构造方法的参数。

创建线程:

class BallThread extends Applet implements Runnable{
    @Override
    public void run(){
        thread = new Thread(this);
        thread.start()
    }
    private Thread thread;
}

编写线程体:

public class xyz implements Runnable{
    @Override
    public void run() {
        int i = 0;
        while (true) {
            System.out.println("Hello" + i++);
        }
    }
}

利用它可以构造一个线程如下:

Runnable r = new xyz();
Thread t = new Thread(r);

这样,就定义了一个由 t 表示的线程,它用来执行 xyz 类的 run() 方法中的程序代码(接口 Runnable 要求实现方法 public void run())。这个线程使用的数据由 r 所引用的 xyz 类的对象提供。

总之,线程由 Thread 对象的实例来引用。线程执行的代码来源于传递给 Thread 构造方法的参数引用的类,这个类必须实现了接口 Runnable,线程操作的数据来源于传递给 Thread 构造方法的 Runnable 实例。

1.3 关于两种方法的讨论

(1)适用于采用实现 Runnable 接口方法的情况

因为 Java 只允许单继承,如果一个类已经继承了 Thread ,就不能再继承其他类,在一些情况下,这就被迫采用实现 Runnable 的方法。

(2)适用于采用继承 Thread 方法的情况

当一个 run() 方法置于 Thread 类的子类中时,this 实际上引用的是控制当前运行系统的 Thread 实例,所以,代码可以简写为:

//原来的写法为:Thread.currentThread().getState();
getState();

2. 线程的启动

虽然一个线程已经被创建,但它实际上并没有立刻运行。要使线程真正在 Java环境中运行,必须通过方法 start() 来启动,start() 方法也在 Thread 类中。

线程机制实现的关键在于它的 “并行性”,怎样才能让一个线程让出 CPU,供其他线程使用呢?API 中提供了以下有关线程的方法:

  • start()————启动线程对象。

  • run()————用来定义线程对象被调度之后所执行的操作,用户必须重写 run() 方法。

  • yield()————强制终止线程的执行。

  • isAlive()————测试当前线程是否在活动。

  • sleep(int millsecond)————使线程休眠一段时间,时间长短由参数来决定。

  • void wait()————使线程处于等待状态。

3. 线程的调度

在一台实际上只具有一个 CPU 的机器上,CPU 在同一时间只能分配给一个线程做一件事。那么现在就必须考虑,当有多于一个线程工作时,CPU 是如何分配的。

在 Java 中,线程调度通常是抢占式的,而不是时间片式的。抢占式调度是指可能有多个线程准备运行,但只有一个在真正运行。一个线程获得执行权,这个线程将持续运行下去,直到它运行结束或因为某种原因而阻塞,再或者有另一个高优先级线程就绪,最后一种情况称为低优先级线程被高优先级线程所抢占。

Java 的线程调度采用如下的优先级策略:

  • 优先级高的先执行,优先级低的后执行。

  • 多线程系统会自动为每个线程分配一个优先级,默认时,继承其父类的优先级。

  • 任务紧急的线程,其优先级较高。

  • 同优先级的线程按 “先进先出” 的原则。

Thread 类有三个与线程优先级有关的静态量:

  • MAX_PRIORITY:最高优先级,值为 10。

  • MIN_PRIORITY:最低优先级,值为 1。

  • NORM_PRIORITY:默认优先级,值为 5。

java.lang.Thread 类中有几个常用的有关优先级的方法有:

  • void setPriority(int newPriority): 重置线程优先级。

  • int getPriority():获得当前线程的优先级。

  • static void yield():是当前进程放弃执行权。

一个线程被阻塞的原因也是多种多样的,可能是因为执行了 Thread.sleep() 调用,故意让它暂停一段时间;也可能是因为需要等待一个较慢的外部设备,例如磁盘或用户操作的键盘。

所有被阻塞的线程按次序排列,组成一个阻塞队列。

而所有就绪但没有运行的线程则根据其优先级排入一个就绪队列。当 CPU 空闲的时候,如果就绪队列不空,队列中的第一个具有最高优先级的线程将运行。当一个线程被抢占而停止运行时,它的运行态被改变并放到了就绪队列的队尾;同样,一个被阻塞(可能因为睡眠或等待 I/O 设备)的线程就绪后通常也放到就绪队列的队尾。

由于 Java 线程调度不是时间片式的,所以在程序运行时要合理安排不同线程之间的运行顺序,以保证给其他线程留有执行的机会。为此,可以通过间隔地调用 sleep() 做到这一点。

sleep() 是类 Thread 中的静态方法,因此可以通过 Thread.sleep(x) 直接调用。参数 x 指定了线程在再次启动前必须休眠的最短时间,以毫秒为单位。同时,该方法可能引发中断异常 InterruptedException,因此要进行捕获和处理。这里说“最短时间”,是因为这个方法只保证在一段时间后线程回到就绪态,至于它是否能够获得 CPU 运行,则要视线程调度而定,所以,通常线程实际被暂停的时间都比指定的时间要长。

除 sleep() 方法外,类 Thread 中的另一个方法 yield() 可以给其他同等优先级线程一个运行的机会。如果就绪队列中有其他同优先级的线程,yield() 把调用者放入就绪队列尾,并允许其他线程运行;如果没有这样的线程,则 yield() 把调用者放入就绪队列尾,并允许其他线程运行;如果没有这样的线程,则 yield() 不做任何工作。

应注意,sleep() 调用允许低优先级进程运行,而 yield() 方法只给同优先级进程以运行机会。

4. 线程的基本控制

4.1 结束线程

结束一个线程有两种情况,第一种情况是当一个线程从 run() 方法的结尾返回时,它自动消亡并不能再被运行,可以将其理解为自然死亡;另一种情况是遇到异常使得线程结束,可以将其理解为强迫死亡。

在程序代码中,可以利用 Thread 类中的静态方法 currentThread() 来引用正在运行的线程。

Thread.currentThread().interrupt();

4.2 检查线程

有时候可能不知道一个线程的运行状态(当程序代码没有直接控制该线程时,会发生此种情况),这时可以利用方法 isAlive() 来获取一个线程是否还在活跃状态的信息。活动状态不意味着这个线程正在执行,而只说明这个线程已被启动,并且既没有运行 stop(),也尚未运行完方法 run()。

4.3 挂起线程

有几种方法可以用来暂停一个线程的运行,暂停一个线程也称为挂起。在挂起之后,必须重新唤醒线程进入运行。挂起线程的方法有以下几种:
(1)sleep()

方法 sleep() 在上一节中已经介绍过,它用于暂时停止一个线程的执行。通常,线程不是休眠期满后就立刻被唤醒,因为此时其他线程可能正在执行,重新调度只在以下几种情况下才会发生:

  • 被唤醒的线程具有更高的优先级。

  • 正在执行的线程因为其他原因被阻塞。

  • 程序处于支持时间片的系统中。

大多数情况下,后两种情况不会立刻发生。

(2)wait() 和 notify()/notifyAll()

wait() 方法导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,才能唤醒线程。

(3)join()

方法 join() 将引起现行线程等待,直至方法 join() 所调用的线程结束。例如已经生成并运行了一个线程 tt,而在另一线程中执行方法 timeout()。

public void timeout() {
      //暂停该线程,等待其他线程结束
      tt.join();
      //其他线程结束后,继续执行该线程
      ...
}

这样,在执行方法 timeout() 后,现行的线程阻塞,直至 tt 运行结束。

join() 方法在调用时也可以使用一个一毫秒计的时间值:

void join(long timeout);

此时 join 方法将挂起现行线程 timeout 毫秒,或直到调用的线程结束,实际挂起时间以二者中时间较少的为准。

原文地址:https://www.cnblogs.com/john1015/p/13938966.html