关于java:线程的启动与终止

45次阅读

共计 14822 个字符,预计需要花费 38 分钟才能阅读完成。

一、线程的创立

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<String> {

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

    public static void main(String[] args) throws InterruptedException, ExecutionException {FutureTask<String> 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<Integer> 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<Integer> storage;

        public Producer(BlockingQueue<Integer> 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<Integer> storage;

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

        public BlockingQueue<Integer> 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() 底层实现

正文完
 0