【Java】多线程 - 线程状态切换函数

Posted by 西维蜀黍 on 2019-02-25, Last Modified on 2021-09-21

Java的线程状态

Java语言中定义了6种线程状态,在任意一个时间点,一个线程有且只有其中一种状态,这6种状态是:

  • 新建(New):一个已经被实例化线程对象但未被启动(start)的线程处于这种状态。
  • 可运行(Runnable):包括了操作系统线程状态中的 Running 和 Ready。因此,处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间(CPU时间片)
  • 无限期等待(Waiting):线程处于无限期等待表示它需要等待并获得其他线程的指示后才能继续运行。处于这种状态的线程不会被分配CPU执行时间,直到当其他的一个线程完成了一个操作后显式地通知这个线程。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由操作系统自动唤醒,以结束限期等待状态。
  • 阻塞(Blocked):线程被阻塞了,在等待进入同步区域(synchronized)时,线程将进入这种状态。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
  • 结束(Terminated):已终止的线程状态,线程已经结束执行。

注意,以上所指的线程状态均是 Java 线程在 JVM 中的状态,因此并不意味着此时对应于操作系统中的线程状态。

线程状态切换方法

Thread.sleep(sleeptime) - 线程主动休眠

public static native void sleep(long millis) 方法是 Thread 类的一个静态原生方法。

当一个线程调用了 Thread.sleep(sleeptime) 后,它会休眠指定的时间(本质是进入限期等待状态)。在经过了指定时间后,会切换成就绪状态,等待在被 JVM 调度器重新切换为运行状态。

需要注意的是,线程调用 Thread.sleep(sleeptime) 后,仅仅释放 CPU 使用权,而所持有的锁仍然占用。

换句话说,如果当前线程已经获得了锁,在调用了 Thread.sleep(sleeptime) 方法后并不会释放锁。这也是 sleep 方法经常拿来与 Object.wait() 方法进行比较的点。Object.wait() 方法会释放CPU执行权 和 占有的锁。

Thread.yield() - 主动让出CPU时间片

public static native void yield()是 Thread 类的一个静态原生方法。

yield 意味着放手,放弃,投降。一个调用 yield() 方法的线程告诉虚拟机,它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。

换句话说,Thread.yield() 的调用会显式地让当前线程让出CPU时间片,此后当前线程会从运行态切换为就绪态。

因此,Thread.yield() 仅释放CPU执行权,锁仍然占用。线程会被放入就绪队列,会在短时间内再次被执行。

Thread.join() - 线程间协作

public final synchronized void join(long millis) 是 Thread 实例的一个方法,是线程间协作的一种方式。

当线程A调用了线程B实例的join()方法时,线程A会进入无限期等待状态(被阻塞在join()方法的调用位置),直到当线程B执行结束(线程B进入结束(Terminated)状态)时,线程A才从无限期等待状态切换为就绪状态。

很多时候,一个线程的输入依赖于另一个线程的输出,这个线程会等待,直到另一个线程已经获取到数据后,这个线程才会继续执行后续操作。

比如:

import java.util.Date;
import java.util.concurrent.TimeUnit;

public class Main implements Runnable {
    private String name;

    public Main(String name) {
        this.name = name;
    }

    public void run() {
        System.out.printf("Thread %s begins: %s\n", name, new Date());
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("Thread %s has finished: %s\n", name, new Date());
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Main("One"));
        Thread thread2 = new Thread(new Main("Two"));
        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.out.println("Main thread is finished");
    }
}

运行结果

Thread One begins: Fri Feb 01 15:26:22 CST 2019
Thread Two begins: Fri Feb 01 15:26:22 CST 2019
Thread Two has finished: Fri Feb 01 15:26:26 CST 2019
Thread One has finished: Fri Feb 01 15:26:26 CST 2019
Main thread is finished

分析

由于 Main 主线程调用了 thread1.join();thread2.join(); ,调用后,主线程会进入无限期等待状态。当线程 One 和 线程 Two 都执行完成时(都进入了结束状态),主线程才会从无限期等待状态切换为就绪状态,最终打印Main thread is finished

若主线程不调用thread1.join();thread2.join();,运行结果将会是:

Main thread is finished
Thread One begins: Fri Feb 01 15:29:18 CST 2019
Thread Two begins: Fri Feb 01 15:29:18 CST 2019
Thread One has finished: Fri Feb 01 15:29:22 CST 2019
Thread Two has finished: Fri Feb 01 15:29:22 CST 2019

因为,在线程 One 和 线程 Two 被置为可执行状态后(然而还未被 JVM 的线程调度器切换为运行状态),主线程就打印了Main thread is finished

Object.wait() - 线程间协作

Object.wait() 方法会释放CPU执行权和占有的锁。

Object.wait() 与 Thread.sleep(sleeptime) 的区别

  1. sleep()方法是Thread类的静态方法,而wait是Object实例方法。
  2. wait()方法必须要在 synchronized 同步方法或者同步块中调用,也就是必须已经获得对象锁;而 sleep() 方法没有这个限制可以在任何地方种使用。另外,wait()方法的调用会释放占有的锁对象,使得该线程进入等待池(Waiting Pool)中,等待下一次获取资源。而sleep()方法只是会让出CPU但不会释放掉锁对象。
  3. 调用sleep()方法后,在休眠时间达到后,如果再次获得CPU时间片就会继续执行;而在调用wait()方法,必须在得到通知后(其他线程调用了Object.notify() /Object.notifyAll() 方法),才会离开等待池,此后进入阻塞状态。当成功获得锁后,才会进入就绪状态。

Object.notify()/notifyAll() - 线程间协作

线程间通信 - wait()/notify()的例子

特点

  • wait()、notify() 和 notifyAll() 方法均为 Object 类的方法,因此 Java 中所有的类实例都具有这三个方法。

  • wait() 和 notify() 方法要求在调用时线程已经获得了对象的锁,因此对这三个方法的调用需要放在 synchronized 方法或 synchronized 块中

  • 调用某个对象的 wait() 方法能让当前线程阻塞,并且当前线程必须拥有此对象的锁。

  • 调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程。

  • 调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程。

有朋友可能会有疑问:为何这三个方法不在 Thread 类中声明,而是 Object 类中声明的方法(当然由于 Thread 类继承了Object类,所以 Thread 也可以调用者三个方法)?

其实这个问题很简单,由于每个对象都拥有monitor(即锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作了。而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。


拥有锁对象

一个线程变为一个对象的锁的拥有者是通过下列三种方法:

  1. 执行这个对象的 synchronized 实例方法。
  2. 执行这个对象的 synchronized 语句块。这个语句块锁的是这个对象。
  3. 对于 Class 类的对象,执行那个类的 synchronized、static方法。

线程调用了 wait() 方法之后,会释放掉当前拥有的锁,并进入等待池(Wait pool) ;当这个线程收到其他线程的通知(其他线程调用 notify() )之后,等待获取锁(在未获取到锁之前,处于阻塞状态),获取锁之后继续运行。

代码

利用两个线程,对一个整形成员变量进行变化,一个线程对该变量增加,一个线程对该变量减少。利用线程间的通信(wait()/notify()),最终实现该整形变量 0101 这样交替的变更。

class NumberHolder {
    private int number;

    public synchronized void increase() {
        if (0 != number) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//

        // 能执行到这里说明已经被唤醒
        // 并且number为0
        number++;
        System.out.println(number);

        // 通知在等待的线程
        notify();
    }

    public synchronized void decrease() {
        if (0 == number) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

        // 能执行到这里说明已经被唤醒
        // 并且number不为0
        number--;
        System.out.println(number);
        notify();
    }
}


class IncreaseThread extends Thread {
    private NumberHolder numberHolder;

    public IncreaseThread(NumberHolder numberHolder) {
        this.numberHolder = numberHolder;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            // 进行一定的延时
            try {
                Thread.sleep((long) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 进行增加操作
            numberHolder.increase();
        }
    }
}


class DecreaseThread extends Thread {
    private NumberHolder numberHolder;

    public DecreaseThread(NumberHolder numberHolder) {
        this.numberHolder = numberHolder;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            // 进行一定的延时
            try {
                Thread.sleep((long) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 进行减少操作
            numberHolder.decrease();
        }
    }
}


public class Main {
    public static void main(String[] args) {
        NumberHolder numberHolder = new NumberHolder();

        Thread t1 = new IncreaseThread(numberHolder);
        Thread t2 = new DecreaseThread(numberHolder);

        t1.start();
        t2.start();
    }
}

运行结果

1
0
1
0
1
0
1
0
1
0

Condition

Condition 是在 Java 1.5 中才出现的,它用来替代传统的 Object 的 wait()、notify() 实现线程间的协作,相比使用 Object 的 wait()、notify(),使用Condition 的 await()、signal() 这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用 Condition。

  • Condition是个接口,基本的方法就是await()和signal()方法;
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

Reference