• 多线程理论基础


    1. 线程基础知识

    1.1 线程和进程的区别

    1. 所属:进程是资源分配的最小单位,线程是程序执行的最小单位,即资源调度的最小单位;
    2. 地址空间:进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵,而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多;
    3. 通信便利程度:线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行,不过如何处理好同步与互斥是编写多线程程序的难点;
    4. 影响程度:多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

    1.2 线程的同步互斥

    线程同步是指线程之间采用某种方式相互制约建立预期的执行顺序或联系的一种方式。
    线程互斥 是指对于共享的进程系统资源,在各单个线程访问时的排它性。

    1.3 上下文切换(Context switch)

    上下文是CPU寄存器和程序计数器在任何时间点的内容。上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
    上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活
    动:
    1. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
    2. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
    3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
    上下文切换是多任务操作系统的一个基本特性。 在多任务操作系统中,多个进程似乎同时在一个
    CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次
    或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃它们在CPU中的时间,或者是调
    度器在进程耗尽其CPU时间片时进行切换的结果。
    上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本,
    实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是 尽可能
    地避免不必要的上下文切换 。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势
    之一是它的上下文切换和模式切换成本极低。

    2. Java线程详解

    2.1 Java线程的实现方式

    1. 通过继承Thread类实现(Thread类实现了Runnable接口)
    2. 实现Runnable接口(Runnable接口里就一个run方法)。
    3. 实现Callable接口(Callable接口里就一个call方法)。

           1>、用FutureTask执行Callable接口的实现类,实现精准接收返回值。 

      2>、 用线程池的submit方法执行Callable接口的实现类,可以实现线程池并发的处理结果,方便对Callable实现类的执行方式做统一的管理。

          4. 使用lambda表达式

    new Thread(() ‐ > System.out.println(Thread.currentThread().getName())).start();

    2.2 run方法和start方法的区别

         Run方式是Runnale接口中定义的抽象方法经实现类实现的。主要用来处理要实现的逻辑,可以说就是一个普通方法。而Start方法是Thread类的中的方法,它里面调用了一个叫start0 的native方法。start0里面调用jvm的startThread的方法来创建一个子线程,并调用线程的run方法开始线程逻辑的处理。

    2.3 Java线程的调度机制

    在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。

    分时调度模型:指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。
    抢占式调度模型:让可运行迟中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其它线程获取CPU的使用权。

     java虚拟机默认采用抢占式调度模型,但在某些特定的需求下需要改变这种模型,由线程自己来控制CPU的调度。

    2.4 多线程的六种状态

    1. 新建状态(New)

    2. 运行状态(Runnable):包含Running和Ready。调用start方法后进入的状态。

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

       进入此状态方式:

    •  无参的Object.wait();
    •  无参的Thread.join();

    4.  限期等待(Timed  Waiting):在一定时间后系统自动唤醒。

    • 入参的Object.wait();
    • 入参的Thread.join();
    • 入参的Thrad.sleep();

    5. 阻塞状态(Blocked):等待获取排它锁。

    6. 结束状态(Terminated   /'tɝmə,net/):线程执行完毕。

    2.5 Thread常用方法

    • sleep和wait的区别
    1. sleep是Thread类中的方法,而wait是Object的方法
    2. sleep可以在任何地方使用,而wait只能在synchronized方法或synchronized代码块中使用(lock.wait())。
    3. sleep的主要作用是使当前线程让出cpu资源,不会导致锁行为的改变;而wait不仅会让出cpu,而且还会释放已占有的同步锁资源。线程调用的wait方法后就进入了线程等待池中,直到线程完成了指定的等待时间或者锁对象调用了notify/notifyall方法后,线程才重新回到锁池中等待锁资源的获取。
    • notify和notifyall的区别

         Notify只会随机让一个处于等待池中的线程进入锁池中去竞争锁的机会,而notifyall会让所有处在等待池中的线程进入锁池去竞争锁资源,没有获取锁资源的线程也只能继续待在锁池中等待其他机会获取锁资源,而不能回到等待池中。

    • yield方法

           当调用Thread.yield函数时,会给线程调度器一个当前线程愿意让出cpu使用的暗示,但线程调度器可能会忽略这个暗示。yield()不会释放当前线程占用的锁资源。线程调用yield方法后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

    • join方法
          join方法用于阻塞当前线程,等到调用join方法的线程执行完毕后再来执行当前线程。一般用于等待异步线程执行完结果之后才能继续运行的场景。
    1. Thread t = new Thread(new Runnable() {
    2. @Override
    3. public void run() {
    4. System.out.println("t begin");
    5. try {
    6. Thread.sleep(5000);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. System.out.println("t finished");
    11. }
    12. });
    13. long start = System.currentTimeMillis();
    14. t.start();
    15. //主线程等待线程t执行完成
    16. t.join();
    17. System.out.println("执行时间:" + (System.currentTimeMillis() ‐ start));

    2.6 Thread中断方法

    • stop方法

        Thread类中方法,暴力中断线程,不论线程此时处于什么状态。

    •  interrupt实例方法
    1. 1.当线程状态在正常运行状态时,改变线程的中断标识为true(即isInterrupted()返回值为true),但不会对线程的运行产生任何影响。
    2. 2.当线程为阻塞状态时,抛出InterruptedException异常,将线程从阻塞状态中唤醒,线程恢复运行状态。不会改变线程的中断标识,
    • interrupted静态方法

          Interrupted是静态方法,它里面调用了currentThread.isInterrupted(true),它会返回当前线程的中断标识,并重置为false。

    • isInterrupted实例方法

           判断当前线程的中断标识,如果是true则返回true,是Flase则返回flase,不会线程的中断标识做处理。

    2.7 线程通信方法

    • volatile
         volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
    • 等待唤醒(等待通知)机制
           等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒。
    1. public class WaitDemo {
    2. private static Object lock = new Object();
    3. private static boolean flag = true;
    4. public static void main(String[] args) {
    5. new Thread(new Runnable() {
    6. @Override
    7. public void run() {
    8. synchronized (lock) {
    9. while (flag) {
    10. try {
    11. System.out.println("wait start .......");
    12. lock.wait();
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. }
    17. System.out.println("wait end ....... ");
    18. }
    19. }
    20. }).start();
    21. new Thread(new Runnable() {
    22. @Override
    23. public void run() {
    24. if (flag) {
    25. synchronized (lock) {
    26. if (flag) {
    27. lock.notify();
    28. System.out.println("notify .......");
    29. flag = false;
    30. }
    31. }
    32. }
    33. }
    34. }).start();
    35. }
    36. }
    • LockSupport()

        LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用 unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

    1. public class LockSupportTest {
    2. public static void main(String[] args) {
    3. Thread parkThread = new Thread(new ParkThread());
    4. parkThread.start();
    5. System.out.println("唤醒parkThread");
    6. LockSupport.unpark(parkThread);
    7. }
    8. static class ParkThread implements Runnable {
    9. @Override
    10. public void run() {
    11. System.out.println("ParkThread开始执行");
    12. LockSupport.park();
    13. System.out.println("ParkThread执行完成");
    14. }
    15. }
    16. }
    • 管道输入输出流

      管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程 之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符
    1. public class Piped {
    2. public static void main(String[] args) throws Exception {
    3. PipedWriter out = new PipedWriter();
    4. PipedReader in = new PipedReader();
    5. // 将输出流和输入流进行连接,否则在使用时会抛出IOException
    6. out.connect(in);
    7. Thread printThread = new Thread(new Print(in), "PrintThread");
    8. printThread.start();
    9. int receive = 0;
    10. try {
    11. while ((receive = System.in.read()) != ‐1){
    12. out.write(receive);
    13. }
    14. } finally {
    15. out.close();
    16. }
    17. }
    18. static class Print implements Runnable {
    19. private PipedReader in;
    20. public Print(PipedReader in) {
    21. this.in = in;
    22. }
    23. @Override
    24. public void run() {
    25. int receive = 0;
    26. try {
    27. while ((receive = in.read()) != ‐1){
    28. System.out.print((char) receive);
    29. }
    30. } catch (IOException ex) {
    31. }
    32. }
    33. }
    34. }
    • Thread.join
           join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。
  • 相关阅读:
    西瓜书-2习题
    Vue开发实战二十二:列表中表格动态合并的实现
    华为被迫开源,从认知到落地SpringBoot企业级实战手册(完整版)
    SSM之Spring注解式缓存Redis
    前端好用API之Fullscreen
    有了这45个小技巧,再也不怕女朋友代码写得烂了!!
    Prometheus 采集Mysql监控数据
    Monorepo[单一代码库] 与MicroService[微服务] 架构
    区块链DAPP系统开发方案丨区块链dapp详细系统开发逻辑
    面向OLAP的列式存储DBMS-13-[ClickHouse]的MergeTree表引擎原理解析
  • 原文地址:https://blog.csdn.net/lmj3732018/article/details/125881761