伟大的理想只有经过忘我的斗争和牺牲才能胜利实现.
本篇为[Dali 王的技术博客] Java 并发编程系列第二篇, 讲讲有关线程的那些事儿. 主要内容是如下这些:
线程概念
线程基础操作
线程概念
进程代表了运行中的程序, 一个运行的 Java 程序就是一个进程. 在 Java 中, 当我们启动 main 函数时就启动了一个 JVM 的进程, 而 main 函数所在的线程就是这个进程中的一个线程, 称为主线程.
进程和线程的关系如下图所示:
由上图可以看出来, 一个进程中有多个线程, 多个线程共享进程的堆的方法区资源, 但是每个线程有自己的程序计数器和栈区域.
线程基础操作
线程创建与运行
Java 中有三种线程创建方式, 分别为: 继承 Thread 类并重写 run 方法, 实现 Runnable 接口的 run 方法, 使用 FutureTask 方式.
先看继承 Thread 方式的实现, 代码示例如下:
- public class ThreadDemo {
- public static class DemoThread extends Thread {
- @Override
- public void run() {
- System.out.println("this is a child thread.");
- }
- }
- public static void main(String[] args) {
- System.out.println("this is main thread.")
- DemoThread thread = new DemoThread();
- thread.start();
- }
- }
上面代码中 DemoThread 类继承了 Thread 类, 并重写了 run 方法. 在 main 函数里创建了一个 DemoThread 的实例, 然后调用其 start 方法启动了线程.
tips: 调用 start 方法后线程并没有马上执行, 而是处于就绪状态, 也就是这个线程已经获取了除 CPU 资源外的其他资源, 等待获取 CPU 资源后才会真正处于运行状态.
使用继承方式, 好处在于通过 this 就可以获取当前线程, 缺点在于 Java 不支持多继承, 如果继承了 Thread 类, 那么就不能再继承其他类. 而且任务与代码耦合严重, 一个线程类只能执行一个任务, 使用 Runnable 则没有这个限制.
来看实现 Runnable 接口的 run 方法的方式, 代码示例如下:
- public class RunnableDemo {
- public static class DemoRunnable implements Runnable {
- @Override
- public void run() {
- System.out.println("this is a child thread.");
- }
- }
- public static void main(String[] args) {
- System.out.println("this is main thread.");
- DemoRunnable runnable = new DemoRunnable();
- new Thread(runnable).start();
- new Thread(runnable).start();
- }
- }
上面代码两个线程共用一个 Runnable 逻辑, 如果需要, 可以给 RunnableTask 添加参数进行任务区分. 在 Java8 中, 可以使用 Lambda 表达式对上述代码进行简化:
- public static void main(String[] args) {
- System.out.println("this is main thread.");
- Thread t = new Thread(() -> System.out.println("this is child thread"));
- t.start();
- }
上面两种方式都有一个缺点, 就是任务没有返回值, 下面看第三种, 使用 FutureTask 的方式. 代码示例如下:
- public class CallableDemo implements Callable<JsonObject> {
- @Override
- public JsonObject call() throws Exception {
- return new JsonObject();
- }
- public static void main(String[] args) {
- System.out.println("this is main thread.");
- FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo()); // 1. 可复用的 FutureTask
- new Thread(futureTask).start();
- try {
- JsonObject result = futureTask.get();
- System.out.println(result.toString());
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- }
- // 2. 一次性的 FutureTask
- FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> {
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("name", "Dali");
- return jsonObject;
- });
- new Thread(innerFutureTask).start();
- try {
- JsonObject innerResult = innerFutureTask.get();
- System.out.println(innerResult.toString());
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- }
- }
- }
如上代码, CallableDemo 实现了 Callable 接口的 call 方法, 在 main 函数中使用 CallableDemo 的实例创建了一个 FutureTask, 然后使用创建的 FutureTask 对象作为任务创建了一个线程并启动它, 最后通过 FutureTask 等待任务执行完毕并返回结果.
同样的, 上面的操作过程适合于需要复用的任务, 如果对于一次性的任务, 大可以通过 Lambda 来简化代码, 如注释 2 处.
等待线程终止
在项目中经常会遇到一个场景, 就是需要等待某几件事情完成后才能继续往下执行. Thread 类中有一个 join 方法就可以用来处理这种场景. 直接上代码示例:
- public static void main(String[] args) throws InterruptedException {
- System.out.println("main thread starts");
- Thread t1 = new Thread(() -> System.out.println("this is thread 1"));
- Thread t2 = new Thread(() -> System.out.println("this is thread 2"));
- t1.start();
- t2.start();
- System.out.println("main thread waits child threads to be over");
- t1.join();
- t2.join();
- System.out.println("child threads are over");
- }
上面代码在主线程里启动了两个线程, 然后分别调用了它们的 join 方法, 主线程会在调用 t1.join()后被阻塞, 等待其执行完毕后返回; 然后主线程调用 t2.join()后再次被阻塞, 等待 t2 执行完毕后返回. 上面代码的执行结果如下:
- main thread starts
- main thread waits child threads to be over
- this is thread 1
- this is thread 2
- child threads are over
需要注意的是, 线程 1 调用线程 2 的 join 方法后会被阻塞, 当其他线程调用了线程 1 的 interrupt 方法中断了线程 1 时, 线程 1 会抛出一个 InterruptedException 异常而返回.
让线程睡眠
Thread 类中有一个 static 的 sleep 方法, 当一个执行中的线程调用了 Thread 的 sleep 方法后, 调用线程会暂时让出指定时间的执行权, 也就是在这期间不参与 CPU 的调度, 但是该线程所拥有的监视器资源, 比如锁还是不让出的. 指定的睡眠时间到了后该函数会正常返回, 线程就处于就绪状态, 然后等待 CPU 的调度执行.
tips: 面试当中 wait 和 sleep 经常会被用来比较, 需要多加体会二者的区别.
调用某个对象的 wait()方法, 相当于让当前线程交出此对象的 monitor, 然后进入等待状态, 等待后续再次获得此对象的锁; notify()方法能够唤醒一个正在等待该对象的 monitor 的线程, 当有多个线程都在等待该对象的 monitor 的话, 则只能唤醒其中一个线程, 具体唤醒哪个线程则不得而知.
调用某个对象的 wait()方法和 notify()方法, 当前线程必须拥有这个对象的 monitor, 因此调用 wait()方法和 notify()方法必须在同步块或者同步方法中进行(synchronized 块或者 synchronized 方法).
看一个线程睡眠的代码示例:
- private static final Lock lock = new ReentrantLock();
- public static void main(String[] args) {
- Thread t1 = new Thread(() -> {
- // 获取独占锁
- lock.lock();
- System.out.println("thread1 get to sleep");
- try {
- Thread.sleep(1000);
- System.out.println("thread1 is awake");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- });
- Thread t2 = new Thread(() -> {
- // 获取独占锁
- lock.lock();
- System.out.println("thread2 get to sleep");
- try {
- Thread.sleep(1000);
- System.out.println("thread2 is awake");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- });
- t1.start();
- t2.start();
- }
上面的代码创建了一个独占锁, 然后创建了两个线程, 每个线程在内部先获取锁, 然后睡眠, 睡眠结束后会释放锁. 执行结果如下:
- thread1 get to sleep
- thread1 is awake
- thread2 get to sleep
- thread2 is awake
从执行结果来看, 线程 1 先获取锁, 然后睡眠, 再被唤醒, 之后才轮到线程 2 获取到锁, 也即在线程 1sleep 期间, 线程 1 并没有释放锁.
需要注意的是, 如果子线程在睡眠期间, 主线程中断了它, 子线程就会在调用 sleep 方法处抛出了 InterruptedException 异常.
线程让出 CPU
Thread 类中有一个 static 的 yield 方法, 当一个线程调用 yield 方法时, 实际就是暗示线程调度器当前线程请求让出自己的 CPU 使用, 如果该线程还有没用完的时间片也会放弃, 这意味着线程调度器可以进行下一轮的线程调度了.
当一个线程调用 yield 方法时, 当前线程会让出 CPU 使用权, 然后处于就绪状态, 线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程, 当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权.
请看代码示例:
- public static void main(String[] args) {
- Thread t1 = new Thread(() -> {
- for (int i = 0; i <10; i++) {
- if (i == 8) {
- System.out.println("current thread:" + Thread.currentThread() + "yield cpu");
- }
- Thread.yield(); // 2
- }
- System.out.println("current thread:" + Thread.currentThread() + "is over");
- });
- Thread t2 = new Thread(() -> {
- for (int i = 0; i < 10; i++) {
- if (i == 8) {
- System.out.println("current thread:" + Thread.currentThread() + "yield cpu");
- }
- Thread.yield(); // 1
- }
- System.out.println("current thread:" + Thread.currentThread() + "is over");
- });
- t1.start();
- t2.start();
- }
在如上的代码中, 两个线程的功能一样, 运行多次, 同一线程的两行输出是顺序的, 但是整体顺序是不确定的, 取决于线程调度器的调度情况.
当把上面代码中 1 和 2 处代码注释掉, 会发现结果只有一个, 如下:
- current thread: Thread[Thread-1,5,main] yield CPU
- current thread: Thread[Thread-0,5,main] yield CPU
- current thread: Thread[Thread-1,5,main] is over
- current thread: Thread[Thread-0,5,main] is over
从结果可知, Thread.yiled 方法生效使得两个线程分别在执行过程中放弃 CPU, 然后在调度另一个线程, 这里的两个线程有点互相谦让的感觉, 最终是由于只有两个线程, 最终还是执行完了两个任务.
tips:sleep 和 yield 的区别:
当线程调用 sleep 方法时, 调用线程会阻塞挂起指定的时间, 在这期间线程调度器不会去调度该线程. 而调用 yield 方法时, 线程只是让出自己剩余的时间片, 并没有被阻塞挂起, 而是出于就绪状态, 线程调度器下一次调度时就可能调度到当前线程执行.
线程中断
Java 中的线程中断是一种线程间的协作模式. 每个线程对象里都有一个 boolean 类型的标识 (通过 isInterrupted() 方法返回), 代表着是否有中断请求 (interrupt() 方法). 例如, 当线程 t1 想中断线程 t2, 只需要在线程 t1 中将线程 t2 对象的中断标识置为 true, 然后线程 2 可以选择在合适的时候处理该中断请求, 甚至可以不理会该请求, 就像这个线程没有被中断一样.
在上面章节中也讲到了线程中断的一些内容, 此处就不再用代码来展开了.
Java 并发编程大纲
继续附上 Java 编程的系统学习大纲以供参考:
Java 并发编程. PNG
[参考资料]
《Java 并发编程之美》
来源: https://www.cnblogs.com/mcbye/p/12543234.html