美文网首页
多线程详解之基础

多线程详解之基础

作者: 大大纸飞机 | 来源:发表于2019-05-28 07:28 被阅读0次

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

    线程的状态

    在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;
    }
    

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

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

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

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

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

    创建线程

    在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 != 0)
            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-0
    

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

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

    不同状态下的线程对中断的响应方式也有区别,NEWTERMINATED 肯定是不会响应的,RUNNABLEBLOCKED 则是会接收到中断信号,而 WAITINGTIMED_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.
    

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

    总结

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

    相关文章

      网友评论

          本文标题:多线程详解之基础

          本文链接:https://www.haomeiwen.com/subject/peqvaqtx.html