线程的基础

2020-07-10 00:00:00 线程 调用 方法 状态 中断

安全是多线程编程的核心主题,但并不是只要使用多线程就一定会引发安全问题。要了解哪些操作是安全的,哪些是不安全的,就必须先掌握如何使用多线程。不过在操作多线程之前,我们先了解一下多线程的几种状态。

线程的状态

在Thread的实现中,包含一个名为State的enum类,用来标识线程运行中的各种状态,其中定义了以下几个类型:

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */

    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */

    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */

    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * ...
     */

    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */

    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */

    TERMINATED;
}

首先,NEW 和 TERMINATED 是两种特殊的状态,前者表示线程还未开始运行,后者表示线程已经运行完毕。在这两种状态下,对线程进行一些操作是没有意义的,因为线程根本没有运行,也就不会去响应中断、睡眠等请求了。

当执行了start方法之后或者线程正在运行时,线程会进入RUNNABLE状态,这时线程或者正在运行,或者正在等待CPU调度。

BLOCKED表示线程正在等待锁,也就是说此时线程要执行的代码是同步的,有其他线程正在运行此代码,所以线程需要等待获取锁。

WAITINGTIMED_WAITING都表示线程要等待一段时间之后再运行,只是后者会有一个超时处理。

线程的执行就是在以上这些状态中不断切换,当然 NEW 和 TERMINATED 这两种表示线程起止的状态,在线程的生命周期中只会执行一次。

创建线程

在Java中创建一个线程有两种方式:继承Thread和实现Runnable。其实这两种方式并没有很大的差别,只是Java仅支持单继承,实现Runnable的方式更灵活一些,但是一个Runnable对象本身是无法执行的,需要用一个Thread对象来帮助它启动,就像这样:

new Thread(new MyRunnable()).start();

启动线程

线程的启动要使用start方法,而不是调用Runnable的run方法,不过如果多次调用start方法会抛出异常:

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */

    if (threadStatus != )
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        // ...
    }
}

通过判断threadStatus的值来确保线程仅被启动一次,threadStatus对应一个枚举的线程状态,前面已经分析过它。

调用start方法之后并不是说线程马上就开始运行了,因为CPU可能处于忙碌中,没有多余的时间片,因此start方法只是把Thread的状态变为RUNNABLE,等待CPU调度。

线程休眠

如果要让线程停止一段时间再继续运行,可以使用sleep(long millis)方法,sleep会让当前线程进入TIMED_WAITING状态,经过millis时间之后线程会自动苏醒,并重新进入RUNNABLE状态等待系统调度。

yield放弃时间片

当一个线程获得CPU时间片之后,可以通过调用yield方法放弃所获得的时间片,并重新进入RUNNABLE状态等待系统调度。和sleep不同之处在于,我们无法知道yield方法调用后线程会等待多久,因为它和其他所有的线程一样处于RUNNABLE状态,那么CPU就可能在任何时候调度它,也可能永远不会调度它。

中断停止线程

通过start方式可以启动线程,我们很容易就会想到使用stop方法来停止,然而stop方法已经过时了,如下:

@Deprecated
public final void stop() {
    // ...
}

在说明如何正确的停止线程之前,我们先说明一下为什么stop方法会过时。stop会释放持有的锁,以使数据可以被其他线程访问,我们知道计算机解决任何问题都不是一蹴而就的,完成一个任务需要很多步骤,每一步都会对结果产生一定的影响,而如果让线程在某一步直接停止,就很可能得到一个不完整的数据。例如给一个用户依次设置姓名和昵称,如果stop正好发生在设置姓名和设置昵称之间,我们得到的用户信息就不再完整。

正确的停止线程方法是使用中断。中断不是说会立即打断线程的执行,而是给线程发送一个中断的信号,由线程来决定何时响应,这样我们就有了足够的时间对数据进行清理。

既然有发送信号,那就一定有办法判断是否接收到了中断信号,Thread中有两种方式来判断是否中断:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);
}

这两种方式终都是调用了一个native方法实现的:

/**
* Tests if some Thread has been interrupted.  The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/

private native boolean isInterrupted(boolean ClearInterrupted);

可以看到,两种方式的区别在于,interrupted是判断当前的线程是否中断,并且会清除中断标记;而isInterrupted是判断调用此方法的Thread对象是否中断,并且不会清除中断标记。我们要区别线程对象和当前线程的区别,前者表示的就是某个线程,而当前线程表示的是执行的某段代码所处的线程,例如,在main线程中,执行以下两处代码时,当前线程currentThread值是不同的:

public class MyThread extends Thread {
    public MyThread() {
        System.out.println("MyThread constructor :" + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        super.run();
        System.out.println("MyThread run :" + Thread.currentThread().getName());
    }
}

在主线程中,调用该线程的start方法,可以得到以下的输出结果:

MyThread constructor :main
MyThread run :Thread-

也就是说,调用的代码是在哪个线程中执行的,Thread.currentThread的值就是哪个线程。明白了这个区别,我们就知道了 interrupted 和 isInterrupted 两个方法在何时有区别了。

前面说过,中断只是给线程发送了一个信号,至于如何响应还是由线程决定,可以不理会中断的信号,也可以根据中断的信号做一些数据的处理之后再结束掉当前的线程。

不同状态下的线程对中断的响应方式也有区别,NEW 和 TERMINATED 肯定是不会响应的,RUNNABLE 和 BLOCKED 则是会接收到中断信号,而 WAITING 和 TIMED_WAITING 则是会抛出InterruptedException,并且会清除中断标记,这从 sleep 和 wait 函数的定义中就可以看出:

// Thread.java
/**
* ...
@throws  InterruptedException if any thread interrupted the
*             current thread before or while the current thread
*             was waiting for a notification.  The <i>interrupted
*             status</i> of the current thread is cleared when
*             this exception is thrown.
* ...
*/

public static native void sleep(long millis) throws InterruptedException;
// Object.java
public final native void wait(long timeout) throws InterruptedException;

了解了中断的概念,中断一个线程就简单多了。例如在 RUNNABLE 状态下,只需要判断是否接收到了中断信号,就可以在合适的时间中断线程。

public class Worker extends Thread {
    public void run() {
        System.out.println("Worker started.");
        while (!isInterrupted()) {
            System.out.println("doing something.");
        }
        System.out.println("Worker stopped.");
    }
}

然后,给线程发送一个中断信号:

Worker thread = new Worker();
thread.start();
// 让线程运行起来
Thread.sleep(10);
thread.interrupt();

就可以得到以下的输出:

Worker started.
doing something.
doing something.
doing something.
...
Worker stopped.

如果线程有睡眠等行为时,就可以利用中断异常来停止线程,修改Worker线程的run方法如下:

public class Worker extends Thread {
    public void run() {
        System.out.println("Worker started.");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("Worker Interrupted.");
        }
        System.out.println("Worker stopped.");
    }
}

可以看到如下输出:

Worker started.
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at chapter01.section1.Interrupt$Worker.run(Interrupt.java:42)
Worker Interrupted.
Worker stopped.

线程从睡眠中被打断,并立即结束。

总结

了解线程的基本概念,是学习线程的步。但是至此我们只是学会了如何使用一个线程,接下来我们继续研究多个线程交互时,如何处理数据安全的问题。

相关文章