51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Java 面试之多线程与并发

进程和线程的区别 {#进程和线程的区别}

先来看看它们的由来:

  1. 串行阶段:初期的计算机只能串行执行任务,并且需要长时间等待用户输入。

    计算机的出现是为了解决复杂的数学计算问题,最初只能接受特定的指令,用户输入一个指令,计算机再做相应的操作,当用户在思考或输入数据时,此时计算机只能等待,这就造成了计算机使用效率的低下的问题,因为大量的时间用来等待用户输入指令而被浪费。

  2. 批处理阶段:预先将用户的指令集中成一份清单,然后批量串行处理清单中的用户指令,但仍然无法并发执行。

    批处理操作系统的问题是:若有两个任务 A 和 B,任务 A 在执行到一半时,需要读取大量的数据输入时,即 IO 操作,此时 CPU 只能静静的等待任务 A 读取数据完成,从而使 CPU 资源白白浪费。

    如果能在任务 A 读取数据的过程中让任务 B 执行,当任务 A 读取数据完毕之后,暂停任务 B,CPU 切回继续执行任务 A,就能够充分利用 CPU 资源了。

    即便这样做却还有一个问题:原来每次都是一个程序在计算机中运行,内存始终只有一个程序的运行数据,如果想让任务 A 在执行 IO 操作的过程中使任务 B 抢占 CPU 资源执行,必然内存中要装入多个程序,那么多个程序之间的数据如何辨别?程序被暂停后又要如何恢复到之前的状态?此时进程应运而生。

  3. 进程阶段:进程独占内存空间,保存各自运行状态,相互间不干扰且可以互相切换,并为并发处理任务提供了可能。

  4. 线程阶段:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行。

    随着计算机的普及,用户对实时性有了要求,由于一个进程在一段时间内只做一件事,如果进程含有多个子任务,只能逐个执行这些子任务,不过子任务之间往往不存在顺序依赖,可以并发的执行,并且子任务共享内存资源,隶属于同一个进程的子任务间切换无需切换页目录以使用新的地址空间,为子任务之间更快速的切换提供了可能,这即是线程的由来。
    一个线程执行一个子任务,实现实时性的效果,对应的一个进程也就会包含多个线程。

那么进程和线程的区别是:

  • 进程是资源分配的最小单位,线程是 CPU 调度的最小单位;
  • 所有与进程相关的资源,都被记录在 PCB(进程控制块) 中;

image-20240229215142633

  • 进程是抢占处理机的调度单位,它拥有完整的虚拟内存地址空间,当进程发生调度时,不同的进程拥有不同的虚拟内存地址空间;线程属于某个进程,共享其资源;
  • 线程只由堆栈寄存器、程序计数器和 TCB(线程控制表) 组成。其中寄存器用来存储线程内的局部变量,但不能存储其它线程的相关变量。

image-20240229215703073

总结:

  • 线程不能看作独立应用,而进程可看做成独立应用;
  • 进程有独立的地址空间,互相不影响,一个进程崩溃后在保护模式下不会影响其它进程,线程只是进程的不同执行路径,当某个线程挂了,它所在的进程也会挂掉,所以多进程程序比多线程程序健壮,但在进程切换时,耗费资源较大,效率要差一些;
  • 线程有自己的堆栈和局部变量,但线程没有独立的地址空间;
  • 进程的切换比线程的切换开销大。

Java 进程和线程的关系 {#java-进程和线程的关系}

  • Java 对操作系统提供的功能进行封装,包括进程和线程;

    Java 作为与平台无关的编程语言,会对操作系统提供的功能进一步封装,提供与平台无关的编程接口供程序员使用,进程与线程作为操作系统的核心概念,更是如此。

  • 每运行一个 Java 程序就会产生一个进程,每个进程中至少包含一个线程;

  • 每个 Java 进程对应一个 JVM 实例,多个线程共享 JVM 里的堆,每个线程都有自己的栈;

  • Java 采用单线程编程模型,程序会自动创建主线程;

  • 主线程可以创建子线程,原则上要后于子线程完成执行。

以下代码可以打印出当前的线程名称:

public static void main(String[] args) {
    System.out.println("Current Thread: " + Thread.currentThread().getName());
}
/* 输出:
Current Thread: main
 */

虽然 Java 采用的是单线程模型,但并不代表 JVM 中只有一个线程,JVM 在创建的主线程同时会创建很多其它的线程,例如 GC 就是由一个垃圾收集器线程专门负责的,因此 JVM 是多线程的。

Thread 中的 start 方法和 run 方法的区别 {#thread-中的-start-方法和-run-方法的区别}

首先来看一段代码:

public class ThreadTest {
    public static void attack() {
        System.out.println("Fight");
        System.out.println("Current Thread is: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                attack();
            }
        };
        System.out.println("Current main thread is: " + Thread.currentThread().getName());
        t.run();
        t.start();
    }
    /* 输出:
    Current main thread is: main
    Fight
    Current Thread is: main
    Fight
    Current Thread is: Thread-0
     */
}

从输出结果看,直接调用 run()方法所执行的线程为主线程,而调用 start()方法输出的线程名称为 Thread-0,说明 start()创建出了一个新的线程来执行 run()方法。

由此得出 start方法和 run方法的区别是:

  • 调用 start()方法会创建一个新的子线程并启动;
  • run()方法只是 Thread 类的一个普通方法调用。

瞅瞅 start()方法的源码,看看它是如何创建出一个新的线程的:

  1. start()方法中最主要就是调用了 start0()方法。

    image-20240301231912767

  2. start0()方法却是一个由 native关键字修饰的方法,表示它调用的外部的非 Java 实现的方法。

    image-20240301232111786

  3. OpenJDK 8源码网站查看 start0()方法的实现源码。

    image-20240301232837336

    image-20240301233044865

    image-20240301233129508

    image-20240301233200413

    image-20240301233236504

    image-20240301233304952

    image-20240301233337172

  4. 点开 Thread.c发现 start0()方法调用了 JVM_StartThread()方法,此方法由 JVM_开头而在顶部又通过 #include "jvm.h"引入了 JVM 的相关方法,此时需要去找 JVM 中的 JVM_StartThread()方法源码。

    image-20240301233537311

  5. 首先退回到根目录,按照如下步骤寻找到 JVM 相关源码:

    image-20240301234148481

    image-20240301234234079

    image-20240301234310294

    image-20240301234341095

    image-20240301234404906

    image-20240301234452206

    image-20240301234533335

  6. 可以看出 JVM_StartThread()还是比较复杂的,其中关键的一句是通过 new JavaThread(&thread_entry, sz)创建一个新的线程。

    image-20240301234810623

  7. 传递的 thread_entry参数,最终会通过 call_virtual()方法调用虚拟机并传递 run方法的名字,即创建一个线程执行 Thread类中的 run方法。

    image-20240301235038835

大致分析完了源码,对于 Java 线程创建调用的关系如下总结:

image-20240301235504321

Thread#start()方法调用 JVM_StartThread()方法创建一个子线程,并通过 thread_entry()方法调用 Thread#run()方法。

Thread 和 Runnable 的关系 {#thread-和-runnable-的关系}

本质上来说 Thread是一个类,而 Runnable是一个接口。

另外 Thread类实现了 Runable接口。

image-20240302104746202

Runnable接口中只有一个抽象的 run()方法,因此它不具有多线程的特性,它依赖于 Thread类中 start()方法创建子线程,然后在子线程中执行 Thread类中实现好了的 run()方法,从而实现多线程执行业务逻辑。

那么如何使用 Thread类实现多线程呢?请看下面这个例子:

  1. 创建 MyThread类,继承 Thread类,重写 run()方法。

    public class MyThread extends Thread {
        private String name;
    
        public MyThread(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread start: " + this.name + ", i = " + i);
            }
        }
    }
    
  2. 编写 ThreadDemo类执行多线程。在输出的结果中可以看出,创建出的 3 个线程是交互执行的,甚至 Thread2还先于 Thread1执行。

    public class ThreadDemo {
        public static void main(String[] args) {
            MyThread mt1 = new MyThread("Thread1");
            MyThread mt2 = new MyThread("Thread2");
            MyThread mt3 = new MyThread("Thread3");
    
            mt1.start();
            mt2.start();
            mt3.start();
        }
        /* 输出:
        Thread start: Thread2, i = 0
        Thread start: Thread3, i = 0
        Thread start: Thread1, i = 0
        Thread start: Thread3, i = 1
        Thread start: Thread2, i = 1
        Thread start: Thread3, i = 2
        Thread start: Thread1, i = 1
        Thread start: Thread3, i = 3
        Thread start: Thread2, i = 2
        Thread start: Thread3, i = 4
        Thread start: Thread1, i = 2
        Thread start: Thread1, i = 3
        Thread start: Thread1, i = 4
        Thread start: Thread1, i = 5
        Thread start: Thread1, i = 6
        Thread start: Thread3, i = 5
        Thread start: Thread3, i = 6
        Thread start: Thread2, i = 3
        Thread start: Thread1, i = 7
        Thread start: Thread2, i = 4
        Thread start: Thread3, i = 7
        Thread start: Thread1, i = 8
        Thread start: Thread1, i = 9
        Thread start: Thread3, i = 8
        Thread start: Thread2, i = 5
        Thread start: Thread2, i = 6
        Thread start: Thread2, i = 7
        Thread start: Thread2, i = 8
        Thread start: Thread2, i = 9
        Thread start: Thread3, i = 9
         */
    }
    

Runnable接口又是如何实现多线程呢?

由于只有 Thread类中才有 start()方法,并且 Thread类提供了可接收 Runnable接口子类实例的构造函数,所以可以通过 Thread类启动 Runnable来实现多线程。

image-20240302111159904

使用 Runnable接口实现多线程例子如下:

  1. 创建 MyRunnable类,实现 Runnable接口,重写 run()方法。

    public class MyRunnable implements Runnable {
        private String name;
    
        public MyRunnable(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread start: " + this.name + ", i = " + i);
            }
        }
    }
    
  2. 编写 RunnableDemo类执行多线程。通过 Thread类构造方法接收 RunnableDemo类实例,从输出结果可以看出,创建出的 3 个线程是交互执行的,甚至 Runnable3还先于 Runnable1执行。

    public class RunnableDemo {
        public static void main(String[] args) {
            MyRunnable mr1 = new MyRunnable("Runnable1");
            MyRunnable mr2 = new MyRunnable("Runnable2");
            MyRunnable mr3 = new MyRunnable("Runnable3");
    
            Thread t1 = new Thread(mr1);
            Thread t2 = new Thread(mr2);
            Thread t3 = new Thread(mr3);
    
            t1.start();
            t2.start();
            t3.start();
        }
        /* 输出:
        Thread start: Runnable3, i = 0
        Thread start: Runnable2, i = 0
        Thread start: Runnable1, i = 0
        Thread start: Runnable2, i = 1
        Thread start: Runnable3, i = 1
        Thread start: Runnable2, i = 2
        Thread start: Runnable1, i = 1
        Thread start: Runnable1, i = 2
        Thread start: Runnable2, i = 3
        Thread start: Runnable3, i = 2
        Thread start: Runnable2, i = 4
        Thread start: Runnable2, i = 5
        Thread start: Runnable2, i = 6
        Thread start: Runnable1, i = 3
        Thread start: Runnable2, i = 7
        Thread start: Runnable3, i = 3
        Thread start: Runnable2, i = 8
        Thread start: Runnable1, i = 4
        Thread start: Runnable1, i = 5
        Thread start: Runnable1, i = 6
        Thread start: Runnable1, i = 7
        Thread start: Runnable1, i = 8
        Thread start: Runnable1, i = 9
        Thread start: Runnable2, i = 9
        Thread start: Runnable3, i = 4
        Thread start: Runnable3, i = 5
        Thread start: Runnable3, i = 6
        Thread start: Runnable3, i = 7
        Thread start: Runnable3, i = 8
        Thread start: Runnable3, i = 9
         */
    }
    

由以上两个例子可以得出**ThreadRunnable的关系是:**

  • Thread是实现 Runnable接口的类,使得 run()支持多线程;
  • 为了实现系统的可扩展性,且因类的单一继承原则,推荐业务类多使用 Runnable接口,将业务逻辑封装在 run方法中,便于后续给普通类赋予多线程的特性。

如何给 run() 方法传参 {#如何给-run-方法传参}

  • 构造函数传参;
  • 成员变量传参;
  • 回调函数传参。

如何实现处理线程的返回值 {#如何实现处理线程的返回值}

实现的方式主要有 3 种:

  • 主线程等待法;
  • 使用 Thread类的 join()方法阻塞当前线程以等待子线程处理完毕;
  • 通过 Callable接口实现:使用 FutureTask或线程池获取。

首先来看主线程等待法的例子。

如果在启动子线程之后立即打印 value的值,此时子线程还没有来得及给 value赋值,因此只能得到 null值。

public class CycleWait implements Runnable{
    private String value;

    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
    }

    public static void main(String[] args) {
        CycleWait cw = new CycleWait();
        Thread t = new Thread(cw);
        t.start();
        System.out.println("value: " + cw.value);
    }
    /* 输出:
    value: null
     */
}

加入主线程等待法相关代码。

public class CycleWait implements Runnable{
    private String value;

    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw = new CycleWait();
        Thread t = new Thread(cw);
        t.start();
        // 主线程等待法
        while (cw.value == null) {
            Thread.sleep(100);
        }
        System.out.println("value: " + cw.value);
    }
    /* 输出:
    value: we have data now
     */
}

主线程等待实现简单,缺点是需要自己实现循环等待的逻辑,当需要等待的变量一多,代码就会显得很臃肿,并且要循环多久是不知道的,无法做到精准控制。

再来看 Thread类中的 join()方法 。

此方法能够做到比主线程等待法更精准的控制,实现更简单,但是它的粒度不够细。

从输出结果可以看出 join()方法可实现同样的效果。

public class CycleWait implements Runnable{
    private String value;

    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value = "we have data now";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw = new CycleWait();
        Thread t = new Thread(cw);
        t.start();
//        while (cw.value == null) {
//            Thread.sleep(100);
//        }
        t.join();
        System.out.println("value: " + cw.value);
    }
    /* 输出:
    value: we have data now
     */
}

最后来看 Callable接口实现方式。

Callable接口只有一个无参的 call()方法,返回值是 V泛型类型。

image-20240302125939773

编写 MyCallable类实现 Callable接口重写其 call()方法,模拟返回值。

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        String value = "test";
        System.out.println("Ready to work");
        Thread.sleep(5000);
        System.out.println("task done");
        return value;
    }
}

使用 Callable接口有两种获取返回值的方式,分别是:使用 FutureTask类获取返回值和使用线程池获取返回值。

先来看使用 FutureTask类获取返回值的情况。

FutureTask类的源码非常多,目前只需将注意力集中在构造函数和另外 3 个方法上。

  • FutureTask类构造函数可以接收 Callable接口的实例对象。

    image-20240302134158829

  • isDone()方法用来判断 call()方法是否已执行完毕。

    image-20240302134318177

  • 无参 get()方法,用来阻塞当前调用它的线程,直到 call()方法执行完毕为止,然后返回 call()方法的返回值。

    image-20240302134518653

  • 有参 get()方法在无参的基础上可以设置一个超时时间,在规定时间内 call()方法还没有执行完毕,将会抛出异常。

    image-20240302134657393

另外 FutureTask实现了 RunnableFuture接口,而 RunnableFuture又继承了 RunnableFuture接口,说明 FutureTask类实例是 Runnable接口的实例,可以传递给 Thread对象调用其 start()方法创建线程执行。这也是 Runnable接口的优势,只要类或者接口实现或继承 Runnable接口就能被 Thread类所接收。

image-20240302140025606

image-20240302140054828

分析完毕后,编写例子,尝试使用 FutureTask获取线程的返回值。

从下面例子的输出结果看可以成功获取到 call()方法的返回值:

public class FutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> task = new FutureTask<>(new MyCallable());
        new Thread(task).start();
        if (!task.isDone()) {
            System.out.println("task not finished, please wait!");
        }
        System.out.println("task return: " + task.get());
    }
    /* 输出:
    task not finished, please wait!
    Ready to work
    task done
    task return: test
     */
}

再来看使用线程池获取返回值的情况。

线程池对象的 submit()方法可以返回一个 Future对象。

点进源码可以看到 Future其实是一个接口,也有 isDone()get()方法。

实际上 FutureTask类中的这两个方法就是实现了 Future接口所得来的。

image-20240302140513953

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        Future<String> future = newCachedThreadPool.submit(new MyCallable());
        if (!future.isDone()) {
            System.out.println("task not finished, please wait!");
        }
        try {
            // 获取线程执行结果
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 关闭线程池
            newCachedThreadPool.shutdown();
        }
    }
    /*
    task not finished, please wait!
    Ready to work
    task done
    test
     */
}

从结果看也是可以成功拿到线程返回值。

使用线程池的好处是可以提交多个实现 Callable接口的类,并发执行处理结果,方便对 Callable实现类的执行方式做统一的管理。

线程的状态 {#线程的状态}

按照官方说法,线程的状态分为如下这几种:

image-20240302141126271

也可在 Thread类源码中的 State枚举找到这些状态:

image-20240302141426371

六个状态:

  • 新建(New) :线程创建后尚未启动时的状态。即新创建了个 Thread对象,但还未调用 start()方法。

  • 运行(Runnable) :包含操作系统线程的 RunningReady状态。处于此状态的线程可能正在执行,也有可能正在等待 CPU 为它分配执行时间。

  • 无限期等待(Waiting):不会被分配 CPU 执行时间,需要显式的被唤醒。

    以下方法会让线程陷入无限期等待状态:

    • 没有设置 Timeout 参数的 Object.wait()方法;
    • 没有设置 Timeout 参数的 Thread.join()方法;
    • LockSupport.park()方法。
  • 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒。

    以下方法会让线程陷入限期等待等待状态:

    • Thread.sleep()方法;
    • 设置了 Timeout 参数的 Object.wait()方法;
    • 设置了 Timeout 参数的 Thread.join()方法;
    • LockSupport.parkNanos()方法;
    • LockSupport.parkUntil()方法。
  • 阻塞(Blocked) :等待获取排它锁。线程等待进入同步区域时状态将会被设置为 Blocked

    比如某个线程已进入 synchronized关键字修饰的方法或代码块中时,即获取锁才能执行,其它想进入此方法或代码块的线程只能等着,此时它们的状态便是 Blocked

  • 结束(Terminated):已终止线程的状态,线程已经结束执行。

    当线程的 run()方法或主线程 main()方法执行完毕时就认为线程终止了,线程对象此时也许还是活的,但是它已经不再是一个可以单独执行的线程了。
    线程一旦终止就不能再复生,在一个终止的线程上调用 start()方法会抛出 java.lang.IllegalThreadStateException异常。

    image-20240302143521958

sleep 和 wait 方法的区别 {#sleep-和-wait-方法的区别}

最基本的差别:

  • sleep()Thread类的方法,wait()Object类中定义的方法;

    image-20240302144820872

    image-20240302145420014

    image-20240302145301150

  • sleep()方法可以在任何地方使用;

  • wait()方法只能在 synchronized方法或 synchronized块中使用。

最主要的本质区别:

  • Thread.sleep()只会让出 CPU,不会导致锁行为的改变;
  • Object.wait()不仅让出 CPU,还会释放已经占有的同步资源锁。

来看下面这个例子,验证 wait()方法是否真的会释放同步锁:

public class WaitSleepDemo {
    public static void main(String[] args) {
        final Object lock = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread A is waiting to get lock");
                synchronized (lock) {
                    try {
                        // 获取同步锁才能执行此代码块中的内容
                        System.out.println("thread A get lock");
                        // 模拟程序执行时间
                        Thread.sleep(20);
                        System.out.println("thread A do wait method");
                        // 调用 wait 方法使线程进入限期等待状态并在 1000 毫秒后自动唤醒
                        lock.wait(1000);
                        System.out.println("thread A is done");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        try {
            // 休眠 10 毫秒确保上面的线程可以先执行
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread B is waiting to get lock");
                synchronized (lock) {
                    try {
                        // 获取同步锁才能执行此代码块中的内容
                        System.out.println("thread B get lock");
                        System.out.println("thread B is sleeping 10 ms");
                        // 模拟程序执行时间
                        Thread.sleep(10);
                        System.out.println("thread B is done");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
    /* 输出:
    thread A is waiting to get lock
    thread A get lock
    thread B is waiting to get lock
    thread A do wait method
    thread B get lock
    thread B is sleeping 10 ms
    thread B is done
    thread A is done
     */
}

从输出结果看,当线程 A 调用 wait()方法后,即输出 thread A do wait method后线程 B 立马获取到了同步锁,输出 thread B get lock,并将线程 B 中的同步代码块内容全部执行完毕,说明 wait()方法真的会释放同步锁 ,当等待 1000 毫秒过后线程 A 从限期等待中被唤醒继续执行其同步代码块剩余内容输出 thread A is done

再将 wait()方法和 sleep()调换下位置,验证 sleep()方法是否真的不改变锁行为:

public class WaitSleepDemo {
    public static void main(String[] args) {
        final Object lock = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread A is waiting to get lock");
                synchronized (lock) {
                    try {
                        // 获取同步锁才能执行此代码块中的内容
                        System.out.println("thread A get lock");
                        // 模拟程序执行时间
                        Thread.sleep(20);
                        System.out.println("thread A is sleeping 1000 ms");
                        // 调用 sleep 方法使线程休眠 1000 毫秒,判断是否会释放同步锁
                        Thread.sleep(1000);
                        System.out.println("thread A is done");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        try {
            // 休眠 10 毫秒确保上面的线程可以先执行
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread B is waiting to get lock");
                synchronized (lock) {
                    try {
                        // 获取同步锁才能执行此代码块中的内容
                        System.out.println("thread B get lock");
                        System.out.println("thread B do wait method");
                        lock.wait(10);
                        System.out.println("thread B is done");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
    /* 输出:
    thread A is waiting to get lock
    thread A get lock
    thread B is waiting to get lock
    thread A is sleeping 1000 ms
    thread A is done
    thread B get lock
    thread B do wait method
    thread B is done
     */
}

从输出结果看,线程 A 调用 sleep()方法休眠 1000 毫秒的过程中并未释放锁,等待其执行完毕输出 thread A is done后线程 B 才获取到锁,因此 sleep()方法不会导致锁行为的改变。

notify 和 notifyAll 的区别 {#notify-和-notifyall-的区别}

微调上面的例子,调用无参的 wait()方法让线程 A 进去无限期等待状态,等到线程 B 同步代码块内容执行完毕时再调用 notify()notifyAll()方法唤醒线程 A 让其继续执行。

public class WaitSleepDemo {
    public static void main(String[] args) {
        final Object lock = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread A is waiting to get lock");
                synchronized (lock) {
                    try {
                        // 获取同步锁才能执行此代码块中的内容
                        System.out.println("thread A get lock");
                        // 模拟程序执行时间
                        Thread.sleep(20);
                        System.out.println("thread A do wait method");
                        // 调用 wait 方法使线程进入无限期等待状态
                        lock.wait();
                        System.out.println("thread A is done");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        try {
            // 休眠 10 毫秒确保上面的线程可以先执行
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread B is waiting to get lock");
                synchronized (lock) {
                    try {
                        // 获取同步锁才能执行此代码块中的内容
                        System.out.println("thread B get lock");
                        System.out.println("thread B is sleeping 10 ms");
                        // 模拟程序执行时间
                        Thread.sleep(10);
                        System.out.println("thread B is done");
                        // 调用 notify() 或者 notifyAll() 唤醒线程 A
                        lock.notify();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
    /* 输出:
    thread A is waiting to get lock
    thread A get lock
    thread A do wait method
    thread B is waiting to get lock
    thread B get lock
    thread B is sleeping 10 ms
    thread B is done
    thread A is done
     */
}

由以上例子可以得出 wait确实能通过 notify()notifyAll()方法被唤醒。

在了解 notify()notifyAll()方法的区别前,先来了解两个概念:

  • 锁池 EntryList
  • 等待池 WaitSet

对于 Java 虚拟机中运行的每个对象来说都有两个池:锁池 EntryList和等待池 WaitSet。这两个池还与 Object基类的 wait()notify()notifyAll()三个方法以及 synchronized关键字息息相关。

锁池

假设线程 A 已经拥有了某个对象(不是类)的锁,而其它线程 B,C 想要调用这个对象的某个 synchronized方法(或者块),由于 B,C 线程在进入对象的 synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程 A 所占用,此时 B,C 线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。

等待池

假设线程 A 调用了某个对象的 wait()方法,线程 A 就会释放该对象的锁,同时线程 A 就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。

notify()notifyAll()的区别:

  • notifyAll()会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会;
  • notify()只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。

验证 notify()方法的行为:

public class NotificationDemo {
    // volatile 表示多个线程对其修改时,一旦某个线程修改了其值,其它的线程都能立马感知到最新被修改的值
    private volatile boolean go = false;

    public static void main(String[] args) throws InterruptedException {
        final NotificationDemo test = new NotificationDemo();

        // 该任务目的是使线程进入等待状态
        Runnable waitTask = new Runnable() {
            @Override
            public void run() {
                try {
                    test.shouldGo();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " finished Execution");
            }
        };

        // 该任务目的调用 notify() 或 notifyAll() 唤醒上面 waitTask 线程
        Runnable notifyTask = new Runnable() {
            @Override
            public void run() {
                test.go();
                System.out.println(Thread.currentThread().getName() + " finished Execution");
            }
        };

        Thread t1 = new Thread(waitTask, "WT1");  // 等待任务
        Thread t2 = new Thread(waitTask, "WT2");  // 等待任务
        Thread t3 = new Thread(waitTask, "WT3");  // 等待任务
        Thread t4 = new Thread(notifyTask, "NT1");  // 唤醒任务

        // 启动所有的等待任务
        t1.start();
        t2.start();
        t3.start();

        // 休眠 200 毫秒确保上面 3 个等待线程启动完毕
        Thread.sleep(200);

        // 启动唤醒任务
        t4.start();
    }

    /**
     * 等待方法
     */
    private synchronized void shouldGo() throws InterruptedException {
        while (go != true) {
            System.out.println(Thread.currentThread() + " is going to wait on this object");
            // 进入无限期等待状态
            wait();
            System.out.println(Thread.currentThread() + " is woken up");
        }
        go = false;
    }

    /**
     * 唤醒方法
     */
    private synchronized void go() {
        while (go == false) {
            System.out.println(Thread.currentThread() + " is going to notify all or one thread waiting on this object");
            go = true;
            // 随机唤醒一个 wait 执行
            notify();
//            notifyAll();
        }
    }
    /* 输出:
    Thread[WT1,5,main] is going to wait on this object
    Thread[WT3,5,main] is going to wait on this object
    Thread[WT2,5,main] is going to wait on this object
    Thread[NT1,5,main] is going to notify all or one thread waiting on this object
    Thread[WT1,5,main] is woken up
    NT1 finished Execution
    WT1 finished Execution
     */
}

从输出结果看,首先 3 个 WaitTask 依次获取到了锁,进入无限期等待状态,接着又执行了唤醒方法,其中的 notify()随机唤醒了 WT1 线程使其继续执行 ,而由于 go值的改变不再满足两个 while循环的条件而跳出循环继续执行之后的代码,此时 WT2 和 WT3 仍然处于无限期等待状态。

改成调用 noifyAll()方法验证其行为:

public class NotificationDemo {
    // volatile 表示多个线程对其修改,一旦某个线程修改了其值,其它的线程都能立马感知到最新被修改的值
    private volatile boolean go = false;

    public static void main(String[] args) throws InterruptedException {
        final NotificationDemo test = new NotificationDemo();

        // 该任务目的是使线程进入等待状态
        Runnable waitTask = new Runnable() {
            @Override
            public void run() {
                try {
                    test.shouldGo();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " finished Execution");
            }
        };

        // 该任务目的调用 notify() 或 notifyAll() 唤醒上面 waitTask 线程
        Runnable notifyTask = new Runnable() {
            @Override
            public void run() {
                test.go();
                System.out.println(Thread.currentThread().getName() + " finished Execution");
            }
        };

        Thread t1 = new Thread(waitTask, "WT1");  // 等待任务
        Thread t2 = new Thread(waitTask, "WT2");  // 等待任务
        Thread t3 = new Thread(waitTask, "WT3");  // 等待任务
        Thread t4 = new Thread(notifyTask, "NT1");  // 唤醒任务

        // 启动所有的等待任务
        t1.start();
        t2.start();
        t3.start();

        // 休眠 200 毫秒确保上面 3 个等待线程启动完毕
        Thread.sleep(200);

        // 启动唤醒任务
        t4.start();
    }

    /**
     * 等待方法
     */
    private synchronized void shouldGo() throws InterruptedException {
        while (go != true) {
            System.out.println(Thread.currentThread() + " is going to wait on this object");
            // 进入无限期等待状态
            wait();
            System.out.println(Thread.currentThread() + " is woken up");
        }
        go = false;
    }

    /**
     * 唤醒方法
     */
    private synchronized void go() {
        while (go == false) {
            System.out.println(Thread.currentThread() + " is going to notify all or one thread waiting on this object");
            go = true;
//            notify();
            // 唤醒所有的 wait 并执行
            notifyAll();
        }
    }
    /* 输出:
    Thread[WT1,5,main] is going to wait on this object
    Thread[WT3,5,main] is going to wait on this object
    Thread[WT2,5,main] is going to wait on this object
    Thread[NT1,5,main] is going to notify all or one thread waiting on this object
    NT1 finished Execution
    Thread[WT2,5,main] is woken up
    Thread[WT3,5,main] is woken up
    WT2 finished Execution
    Thread[WT3,5,main] is going to wait on this object
    Thread[WT1,5,main] is woken up
    Thread[WT1,5,main] is going to wait on this object
     */
}

调用 notifyAll()后 WT1、WT2、WT3 都被唤醒了。由于 WT2 线程醒的最早,在其执行完成时将 go的值又设为了 false导致 WT1 和 WT3 连个线程又执行 while循环中的内容将其状态又被设为了无限期等待,而此时唤醒任务已经结束执行输出 NT1 finished Execution,不会再有 notifyAll()唤醒 WT1 和 WT3 它俩了,它们会一直处于无限期等待状态。

yield 函数 {#yield-函数}

概念:

  • 当调用 Thread.yield()函数时,会给线程调度器一个当前线程愿意让出 CPU 使用的暗示,不过线程调度器可能会忽略这个暗示;
  • Thread.yield()函数不会改变锁行为,不会让出锁。
public class YieldDemo {
    public static void main(String[] args) {
        Runnable yieldTask = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                    if (i == 5) {
                        // 当 i 等于 5 时调用 yield() 方法给调度器一个当前线程愿意让出使用权的暗示
                        Thread.yield();
                    }
                }
            }
        };

        Thread t1 = new Thread(yieldTask, "A");
        Thread t2 = new Thread(yieldTask, "B");
        t1.start();
        t2.start();
    }
    /* 输出 1:
    A0
    B0
    A1
    B1
    A2
    A3
    A4
    A5
    B2
    A6
    A7
    A8
    B3
    A9
    B4
    B5
    B6
    B7
    B8
    B9
     */
    /* 输出 2:
    A0
    A1
    A2
    A3
    A4
    A5
    A6
    A7
    B0
    A8
    B1
    B2
    B3
    A9
    B4
    B5
    B6
    B7
    B8
    B9
     */
}

输出 1 中显示当 A 线程执行到 5 时让出了 CPU 使用权执行了 B 线程,而在输出 2 中显示执行完 A5 之后继续执行了 A6,由此说明 Thread.yield()函数只是暗示调度器愿意出让使用权,最终的决定权还是在调度器手中。

如何中断线程 {#如何中断线程}

已经被抛弃的方法:

  • 通过调用 stop()方法停止线程。

    可以由一个线程停止另外一个线程,这个方法太过暴力且不安全。

    比如线程 A 调用线程 B 的 stop()方法停止线程 B,而此时线程 A 并不知道线程 B 的执行情况,这种突然间的停止会导致线程 B 的清理工作无法完成。而且调用 stop()方法后线程 B 会马上释放锁,这有可能会引发数据不同步的问题。

    基于以上问题,stop()方法被抛弃了。类似的还有下面这些方法。

  • 通过调用 suspend()resume()方法。

目前使用的方法:调用 interrupt(),通知线程应该中断了。

  • 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,例如处于 sleepwaitjoin等状态,并抛出一个 InterruptedException异常;
  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响。

interrupt()方法需要被调用的线程配合中断:

  • 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程;
  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响。

interrupt()方法使用案例如下:

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable interruptTask = new Runnable() {
            @Override
            public void run() {
                int i = 0;
                try {
                    // 在正常运行任务时,经常检查本线程的中断标志位,如果设置了中断标志就自行停止线程
                    while (!Thread.currentThread().isInterrupted()) {
                        Thread.sleep(100);  // 休眠 100ms
                        i++;
                        System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread() + ") loop " + i);
                    }
                } catch (InterruptedException e) {
                    // 在调用阻塞方法时正确处理 InterruptedException 异常。(例如,catch 异常后就结束线程。)
                    System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread() + ") catch  InterruptedException.");
                }
            }
        };

        Thread t1 = new Thread(interruptTask, "t1");
        System.out.println(t1.getName() + " (" + t1.getState() + ") is new.");

        t1.start();  // 启动"线程t1"
        System.out.println(t1.getName() + " (" + t1.getState() + ") is started.");

        // 主线程休眠 300ms,然后主线程给 t1 线程发"中断"指令
        Thread.sleep(300);
        t1.interrupt();
        System.out.println(t1.getName() + " (" + t1.getState() + ") is interrupted.");

        // 主线程休眠 300ms,然后查看 t1 线程状态。
        Thread.sleep(300);
        t1.interrupt();
        System.out.println(t1.getName() + " (" + t1.getState() + ") is interrupted now.");
    }
    /* 输出:
    t1 (NEW) is new.
    t1 (RUNNABLE) is started.
    t1 (Thread[t1,5,main]) loop 1
    t1 (Thread[t1,5,main]) loop 2
    t1 (TIMED_WAITING) is interrupted.
    t1 (Thread[t1,5,main]) catch  InterruptedException.
    t1 (TERMINATED) is interrupted now.
     */
}

线程状态以及状态之间的转换 {#线程状态以及状态之间的转换}

image-20240302173115804

参考资料 {#参考资料}

赞(5)
未经允许不得转载:工具盒子 » Java 面试之多线程与并发