线程的启动与终止

一、线程的创建

1.Oracle 官网描述

There are two ways to create a new thread of execution.

One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.

有两种方式可以创建新的执行线程。一种方法是定义一个 Thread 类的子类,该子类重写 Thread 类的 run 方法。然后可以分配并启动子类的实例。创建线程的另一种方法是定义一个实现 Runnable 接口的类,然后可以分配该类的实例,在创建 Thread 时将其作为参数传递并启动。

1.1 继承 Thread 类

public class ThreadStyle extends Thread {

    @Override
    public void run() {
        System.out.println("用 Thread 方式创建线程");
    }

    public static void main(String[] args) {
        new ThreadStyle().start();
    }
}

定义一个类继承 Thread 类,然后重写 run 方法,直接通过该类的实例启动线程,输出结果为:用 Thread 方式创建线程。

1.2 实现 Runnable 接口

public class RunnableStyle implements Runnable {

    @Override
    public void run() {
        System.out.println("用 Runnable 方式实现线程");
    }

    public static void main(String[] args) {
        new Thread(new RunnableStyle()).start();
    }
}

定义一个类实现 Runnable 接口并重写 run 方法,通过将该 Runnable 实例传入 Thread 类参数中启动线程,输出结果为:用 Runnable 方式实现线程。

1.3 两种方法对比

准确的讲,创建线程都是通过构造 Thread 类这一种方式实现,实现线程的执行单元有两种方式。

public class Thread implements Runnable {
    /** 省略 */
    
    private Runnable target;
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}
  • 方法一(继承 Thread 类):Thread 类本身也是实现了 Runnable 接口然后重写 run 方法的,当通过继承 Thread 类创建线程时,Thread 类的整个 run 方法都会被重写。
  • 方法二(实现 Runnable 接口):Thread 类中有一个名为 target 的 Runnable 变量,在 Thread(Runnable target) 构造方法中传入 Runnable 实例会初始化 target 属性,通过该方法创建线程,最终只会调用 target.run() 方法,并不会重写整个 run 方法。

通过方法二创建线程的方式其实更好:

  • 实现 Runnable 接口的方式与 Thread 类解耦。
  • 接口可以多实现,继承 Thread 类的方式限制了可扩展性。
  • 继承 Thread 类的话,每次新建线程都会去创建一个独立的线程,开销大,不适合资源共享。实现 Runnable 接口的话,则很容易的实现资源共享,而且可以利用线程池等工具,大大减小创建线程和销毁线程带来的损耗。

同时使用这两种方法创建线程:

先通过在 Thread 类构造器中传入匿名内部类(Runnable 实例)的方式创建线程,然后在此基础上重写了 Thread 类的 run 方法,最终输出:使用 Thread 方式创建。因为传入 Runnable 实例创建线程是调用 run 方法中的 target.run() 执行的,但是后面重写了 run 方法,导致此方法失效。

public class BothRunnableAndThread {

    public static void main(String[] args) {
        new Thread(() -> System.out.println("使用 Runnable 方式创建")) {
            // 重写了 run 方法,覆盖了 run 里的三行代码
            // runnable 传入进去却没有运行
            @Override
            public void run() {
                System.out.println("使用 Thread 方式创建");
            }
        }.start();
    }
}

创建线程的方式通常分为两类,除此之外,通过实现 Callable 接口、使用线程池等方式也可以创建线程,但是本质上都是继承 Thread 类或实现 Runnable 接口这两种方法。

1.4 线程初始化

不管通过哪种方式创建线程,都会调用线程初始化函数 init,可以通过不同的构造函数初始化线程的部分参数。如下 init 函数所示,一个新构造的线程对象是由其父线程来进行空间分配的,而子线程继承了父线程的 daemon、priority 等属性,同时还会分配一个唯一的 id 来标识这个线程。

public class Thread implements Runnable {
    
    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    
    public Thread(String name) {
        init(null, null, name, 0);
    }
    
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
        // 省略部分代码
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        // 当前线程就是该线程的父线程
        Thread parent = currentThread();
        // 复制父线程的 daemon 和 priority 属性
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        this.target = target;
        setPriority(priority);
        // 复制父线程的 inheritableThreadLocals
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // 分配线程 ID
        tid = nextThreadID();
    }
}

2.实现 Callable 接口

public class CallableDemo implements Callable {

    @Override
    public String call() {
        return "hncboy";
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        FutureTask ft = new FutureTask<>(new CallableStyle());
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

3.使用线程池

public class ThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(new TaskThread());
    }

    private static class TaskThread implements Runnable {

        @Override
        public void run() {
            System.out.println("hncboy");
        }
    }
}

4.使用定时器

public class TimerTaskDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hncboy");
            }
        }, 1000, 1000);
    }
}

二、线程的启动

1.start() 方法

1.1 方法含义

启动新线程:通知 JVM 在有空闲的情况下启动线程,本质是请求 JVM 来运行我们的线程,线程何时运行由线程调度器来确定。该线程启动的同时会启动两个线程:第一个是用来执行 start 方法的父线程或主线程,第二个是被创建的子线程。

准备工作:让线程处于就绪状态(已经获得了除 CPU 以外的其他资源,如已经设置了上下文,线程状态,栈等),做完准备工作后,才能被 JVM 或操作系统调度到执行状态获取 CPU 资源,然后才会执行 run 方法。

重复调用 start() :抛出异常 Exception in thread "main" java.lang.IllegalThreadStateException。一旦线程 start 以后,就从 NEW 状态进入到其他状态,比如 RUNNABLE,只有处于 NEW 状态的线程才能调用 start() 方法。

1.2 原理分析

通过 threadStatus 属性来判断是否重复启动并抛出异常,实际的启动方法是 native 方法 start0()。

public class Thread implements Runnable {
    
    /**
     * 线程状态,初始化为 0,表示还未启
     */
    private volatile int threadStatus = 0;
    
    public synchronized void start() {
        // 判断线程的状态,也就是判断是否启动,重复启动时抛出 IllegalThreadStateException
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        // 将线程加入线程组
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    // 告知线程组该线程启动失败
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {}
        }
    }
    
    private native void start0();
}

通过 /src/share/native/java/lang/Thread.c 可知,start0() 方法对应 JVM_StartThread 方法

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
};

位于 /src/hotspot/share/prims/jvm.cpp 的 JVM_StartThread 方法中有段注释

// Since JDK 5 the java.lang.Thread threadStatus is used to prevent
// re-starting an already started thread, so we should usually find
// that the JavaThread is null. However for a JNI attached thread
// there is a small window between the Thread object being created
// (with its JavaThread set) and the update to its threadStatus, so we
// have to check for this

该段注释说自从 JDK5 后 使用 Thread 类的 threadStatus 属性去方式线程重复启动,接下来看下 /src/share/vm/runtime/thread.cpp 中的 start 方法,该方法中判断如果该线程是 Java 线程,则将该线程的状态改为 RUNNABLE。

void Thread::start(Thread* thread) {
  trace("start", thread);
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
     // 这里调用 set_thread_status 方法将线程的状态修改为 RUNNALBE
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

2.run() 方法

run() 只是 Thread 类的一个基本方法

public class Thread implements Runnable {
    /** 省略 */
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

3.比较两方法

输出:main 和 Thread-0

public class StartAndRunMethod {

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
        runnable.run();
        new Thread(runnable).start();
    }
}

调用 start 方法才是真正意义上启动了一个线程,会经历线程的各个生命周期,如果直接调用 run 方法,则只是普通的调用方法,不会通过子线程去调用。

三、线程的终止

1.过期的 suspend()、resume()、stop()

这三个方法已经被废除,通过查看 Oracle 官方文档 可以得知。使用 stop() 方法停止线程会释放线程的所有 monitor,该方法在终止一个线程时不会保证线程的资源正常释放,并且抛出 ThreadDeath 异常,通常是没有给予线程完成资源释放工作的机会,因此会导致程序出现数据不同步。suspend() 方法则容易造成死锁,该方法在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入挂起状态。resume() 必须和 suspend() 一起使用,当要恢复目标线程的线程在调用 resume 之前尝试锁定这个 monitor,此时就会导致死锁。

2.volatile 标志位

通过 volatile 修饰的共享变量可以进行线程的终止。

2.1 成功案例

子线程每隔 1 秒输出:持续运行。主线程在 2 秒后将 stop 置为 true,此时子线程 while 循环停止,子线程运行结束。循环只进行了两次。

public class RightVolatileDemo {

    private static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!stop) {
                System.out.println("持续运行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        stop = true;
    }
}

运行结果如下:

持续运行
持续运行

2.3 失败案例

使用 volatile 的局限性,当线程陷入阻塞时,使用 volatile 修饰的变量无法停止线程。

通过生产者消费者例子来演示阻塞情况下 volatile 的局限性,定义一个生产者类实现 Runnable 接口重写 run 方法,在 run 中当 volatile 修饰的 canceled 变量为 false 时,生产者通过 BlockingQueue 的 put 方法不断添加数据,当阻塞队列到达上限时,put 方法会阻塞。定义一个消费者类,通过 needMoreCount 方法判断消费者是否结束消费。

在主函数中初始化一个长度为 10 的阻塞队列,构建生产者和消费者实例,当消费者结束消费时,将生产者的 canceled 属性值改为 true,但是此时生产者仍然在运行,因为生产者线程阻塞在 put 方法。这就是 volatile 标志位的局限性了。

public class WrongVolatileDemo {

    public static void main(String[] args) throws InterruptedException {
        // 定义容量为 10 的阻塞队列
        BlockingQueue storage = new ArrayBlockingQueue<>(10);

        // 启动生产者线程
        Thread producerThread = new Thread(new Producer(storage));
        producerThread.start();
        Thread.sleep(1000);

        // 启动消费者
        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreCount()) {
            System.out.println("消费者消费:" + consumer.getStorage().take());
            Thread.sleep(100);
        }
        System.out.println("消费者消费完全结束");

        // 此时生产者不应该继续生产
        Producer.canceled = true;
    }

    /**
     * 生产者
     */
    private static class Producer implements Runnable {

        static volatile boolean canceled = false;
        private BlockingQueue storage;

        public Producer(BlockingQueue storage) {
            this.storage = storage;
        }

        @Override
        public void run() {
            int count = 1;
            try {
                while (!canceled) {
                    // 如果队列满的话,put 方法会阻塞当前线程
                    storage.put(count);
                    System.out.println("生产者生产:" + count);
                    count++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者停止运行");
            }
        }
    }

    /**
     * 消费者
     */
    private static class Consumer {

        private BlockingQueue storage;

        public Consumer(BlockingQueue storage) {
            this.storage = storage;
        }

        public BlockingQueue getStorage() {
            return storage;
        }

        public boolean needMoreCount() {
            return Math.random() < 0.95;
        }
    }
}

3.interrupt 方法

interrupt 翻译为中断,中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt() 方法对其进行中断操作。

举几个例子来演示 interrupt 的不同用法。

3.1 不带阻塞的中断

该例子是最简单的中断,thread 线程启动后,休眠 1ms 再调用该对象的 interrupt 方法,此时线程中正在执行的循环检测到 Thread.currentThread().isInterrupted() 为 true 结束循环,输出 count 变量的值。

当线程调用自身的 interrupt 方法时,会将中断标记设置为 ture,线程内部循环会通过检查自身是否被中断来结束循环,而 线程内部的 isInterrupted() 方法就能判断线程是否被中断。

public class InterruptThreadWithoutSleep {
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int count = 0;
            // 检查自身是否被中断来结束循环
            while (!Thread.currentThread().isInterrupted()) {
                count++;
            }
            System.out.println(count);
        });
        thread.start();
        Thread.sleep(1);
        // 设置中断标记
        thread.interrupt();
    }
}

3.2 带有阻塞的中断

该例子演示带有 sleep 阻塞的中断方法使用。sleep 方法使用需要抛出 InterruptedException,说明该方法可以响应 interrupt 中断。在线程启动后,该线程会休眠 1s,而主线程在休眠 100ms 后会调用中断方法,此时该线程是处于阻塞状态,在阻塞状态下响应到中断,sleep 方法会抛出 InterruptedException ,但是在抛出该异常前,JVM 会先将该线程的中断标识位清除,然后才抛出 InterruptedException,此时调用 isInterrupted() 方法将会返回 false。

如果在执行过程中,每次循环都会调用 sleep 方法,那么其实可以不需要每次迭代都通过 isInterrupted() 方法检查中断,因为 sleep 方法会响应中断。

public class InterruptThreadWithSleep {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("中断标记:" + Thread.currentThread().isInterrupted());
            }
        });
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }
}

运行结果如下:

java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.hncboy.interrupt.InterruptThreadWithSleep.lambda$main$0(InterruptThreadWithSleep.java:15)
    at java.lang.Thread.run(Thread.java:748)
中断标记:false

3.3 interrupt 相关方法

3.4.1 interrupt()

设置中断标记,最终调用 native 的 interrupt0() 方法设置中断标记。

public void interrupt() {
    if (this != Thread.currentThread())
        // 权限检查
        checkAccess();
    
    synchronized (blockerLock) {
        // IO 读写相关
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();
            b.interrupt(this);
            return;
        }
    }
    // 该方法一定会执行
    interrupt0();
}

private native void interrupt0();

找到 interrupt0 方法对应的 JVM_Interrupt 方法,找到该方法代码。

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");

  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
JVM_END

找到关键方法 Thread::interrupt 的代码。

void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}

找到关键方法 os::interrupt 的代码,此时找到了设置中断标记的方法,Java 中的每个线程都与操作系统的线程一一对应,一个 osthread 就对应 Java 中的一个线程,如果 osthread 没有被设置为中断,则设置中断标记为 true。

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();
  // 如果线程没有被中断 
  if (!osthread->interrupted()) {
    // 设置中断标记为 true
    osthread->set_interrupted(true);
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    OrderAccess::fence();
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();

  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}

3.4.2 isInterrupted() 和 interrupted()

返回线程的中断状态。interrupted 为静态方法,两个方法都调用了 isInterrupted 方法,不过传入的参数不一样,true 表示清除中断状态,false 表示不清除。

Thread.interrupted() 在哪个线程里被调用,就返回哪个线程的中断标志。

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

private native boolean isInterrupted(boolean ClearInterrupted);

3.4.2 综合例子

public class InterruptComprehensive {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {});

        // 启动线程
        thread.start();
        // 设置中断标志
        thread.interrupt();
        // 获取中断标志,被中断了返回 true
        System.out.println("isInterrupted: " + thread.isInterrupted());
        // 获取中断标志并重置,interrupted 静态方法调用的是执行它的线程,也就是 main 线程,返回 false
        System.out.println("isInterrupted: " + thread.interrupted());
        // 获取中断标志并重置,Main 函数没有没有被中断,返回 false
        System.out.println("isInterrupted: " + Thread.interrupted());
        // 获取中断标志,中断标记没有被清除,返回 true
        System.out.println("isInterrupted: " + thread.isInterrupted());
        thread.join();
        System.out.println("Main thread is over.");
    }
}

运行结果:

isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
Main thread is over.

3.4 能响应中断的部分方法

有些阻塞方法是不可中断的,例如 I/O 阻塞和 synchronized 阻塞,需要针对某一些锁或某一些 I/O 给出特定的方案。

  • Object.wait()/wait(long)/wait(long, int)
  • Thread.sleep(long)/sleep(long, int)
  • Thread.join()/join(long)/join(long, int)
  • java.util.concurrent.BlockingQueue.take()/put(E)
  • java.util.concurrent.locks.Lock.lockInterruptibly()
  • java.util.concurrent.CountDownLatch.await()
  • java.util.concurrent.CyclicBarrier.await()
  • java.util.concurrent.Exchanger.exchange(V)
  • java.nio.channels.InterruptibleChannel 相关方法
  • java.nio.channels.Selector 相关方法

3.5 InterruptedException 异常处理

3.5.1 传递中断

当在 run 中调用了一个有异常的方法时,该异常应该在方法中用 throws 声明,传递到 run 方法,而不是在方法中捕获,此时可能会造成不可预料的结果。throwInMethod2() 为正确做法,throwInMethod1() 为错误做法。

public class HandleInterruptedException implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new HandleInterruptedException());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while (true) {
            System.out.println("work");
            try {
                throwInMethod2();
            } catch (InterruptedException e) {
                System.out.println("保存日志、停止程序");
                e.printStackTrace();
            }
        }

        /*while (true) {
            System.out.println("go");
            throwInMethod1();
        }*/
    }

    private void throwInMethod2() throws InterruptedException {
        Thread.sleep(2000);
    }

    private void throwInMethod1() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.5.2 重新设置中断状态

因为阻塞抛出 InterruptedException 异常后,会清除中断状态。可以在 catch 子语句中调用 Thread.currentThread().interrupt() 方法来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断。

public class HandleInterruptedException2 implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new HandleInterruptedException2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("发生中断,程序运行结束");
                break;
            }
            System.out.println("work");
            reInterrupt();
        }
    }

    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

《Java 并发编程的艺术》

深入理解Thread.run()底层实现

Thread.interrupt()相关源码分析

你可能感兴趣的:(线程的启动与终止)