2.多线程-初阶(上)

简介: 2.多线程-初阶(上)

文章目录

大家好,我是晓星航。今天为大家带来的是 多线程-初阶 相关的讲解!😀

1. 认识线程(Thread)

1.1 概念

1) 线程是什么

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.

还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的业务。我们进一步设想如下场景:

一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。

如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。

此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

2) 为啥要有线程

首先, “并发编程” 成为 “刚需”.

单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.

有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.(线程之所以轻,是因为把申请资源/释放资源的操作给省下了)

创建线程比创建进程更快.

销毁线程比销毁进程更快.

调度线程比调度进程更快.

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)

关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.

3) 进程和线程的区别

进程是包含线程的. 每个进程至少有一个线程存在,即主线程。

进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.

比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。

  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

相当于我们只增加处理资源的线程,把申请资源和释放资源的操作省下来了!!!

如果我们进行多线程操作,相当于只有第一个线程启动的资源开销是比较大的,后续线程的加入就很简单了。同一个进程里的多个线程之间,共用了进程的同一份资源(主要指的是 内存 和 文件描述符表)。

注:一个线程只能在一个进程中,但是一个进程可以包含多个线程。

4) Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

如果一个线程抛异常,处理不好,很可能把其他线程都给带走了,导致所有线程都挂了。

1.2 第一个多线程程序

感受多线程程序和普通程序的区别:

每个线程都是一个独立的执行流

多个线程之间是 “并发” 执行的.

并行:微观上同一时刻,两个核心上的进程,就是同时执行的

并发:微观上,同一时刻,一个核心上只能运行一个进程。但是它能够对进程快速的进行切换,比如说 CPU 这个核心上,先运行一下 QQ音乐,再运行以下 cctalk ,再以下LOL,只要切换速度足够快(2.5GHz,每秒运行 25亿条指令),宏观上认识感知不到的

未来除非显式声明,否则谈到并发,就是指并行 +并发。

import java.util.Random;    
public class ThreadDemo {
        private static class MyThread extends Thread {
            @Override
            public void run() {
                Random random = new Random();
                while (true) {
                    // 打印线程名称
                    System.out.println(Thread.currentThread().getName());
                    try {
                        // 随机停止运行 0-9 秒
                        Thread.sleep(random.nextInt(10));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
            t1.start();
            t2.start();
            t3.start();
            Random random = new Random();
            while (true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    // 随机停止运行 0-9 秒
                    e.printStackTrace();
                }
            }
        }
    }
Thread-0
Thread-0
Thread-2
Thread-1
Thread-2
Thread-1
Thread-0
Thread-2
main
main
Thread-2
Thread-1
Thread-0
Thread-1
main
Thread-2
Thread-2
......

使用jconsole命令观察线程

1.3 创建线程

1.3.1方法1 继承 Thread 类

  1. 继承 Thread 来创建一个线程类.
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}
  1. 创建 MyThread 类的实例
MyThread t = new MyThread();
  1. 调用 start 方法启动线程
t.start(); // 线程开始运行

上述操作中有解耦合。

解耦合:目的就是为了让 线程 和 线程 要干的活之间分离开。未来如果要改代码,不用多线程,使用多进程,或者线程池,或者协程…此时代码改动比较小

1.3.2方法2 实现 Runnable 接口

  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
  1. 调用 start 方法
t.start(); // 线程开始运行

对比上面两种方法:

继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.

实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()

其他变形

匿名内部类创建 Thread 子类对象

// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};

  • 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});

  • lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
    System.out.println("使用匿名类创建 Thread 子类对象");
});

1.4 多线程的优势-增加运行速度

可以观察多线程在一些场合下是可以提高程序的整体运行效率的。

  • 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
  • serial 串行的完成一系列运算. concurrency 使用两个线程并行的完成同样的运算.
public class ThreadAdvantage {
    // 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
    private static final long count = 10_0000_0000;
    public static void main(String[] args) throws InterruptedException {
        // 使用并发方式
        concurrency();
        // 使用串行方式
        serial();
   }
    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();
        // 利用一个线程计算 a 的值
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a--;
               }
           }
       });
        thread.start();
        // 主线程内计算 b 的值
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
       }
        // 等待 thread 线程运行结束
        thread.join();
        // 统计耗时
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("并发: %f 毫秒%n", ms);
   }
    private static void serial() {
        // 全部在主线程内计算 a、b 的值
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a--;
       }
        int b = 0;
        for (long i = 0; i < count; i++) {
          b--;
       }
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("串行: %f 毫秒%n", ms);
   }
}
并发: 399.651856 毫秒
串行: 720.616911 毫秒

1.5 PCB、PID、进程和线程之间的关系

PCB 对应的是线程。

一个线程对应一个PCB。

一个进程对应多个PCB。

如果一个进程只有一个线程,就是一个进程对一个PCB了。

同一个进程里的若干PCB、PID相同,不同进程的 PID 是不同的。

2. Thread(/θred/) 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象 就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

注:我们使用Thread类时不必要import一个包,因为我们的Thread就再java.lang下面。

2.1 Thread 的常见构造方法

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

注:这里的3和4方法多出来的String name的作用是给我们的线程起名字。

2.2 Thread 的几个常见属性

ID 是线程的唯一标识,不同线程不会重复

名称是各种调试工具用到

状态表示线程当前所处的一个情况,下面我们会进一步说明

优先级高的线程理论上来说更容易被调度到

关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。

前台线程:会阻止进程结束,前台线程的工作没做完,进程是不可结束的。

后台线程:不会阻止进程结束,后台线程工作没做完,进程也是可以结束的。

代码里手动创建的线程,默认都是前台的。包括 main 默认也是前台的。其他 jvm 自带的线程都是后台的,也可以手动的使用 setDaemon 设置成后台线程。是后台线程就是守护线程。

即isDaemon()返回为true 那么该线程就是后台线程。

  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • isAlive() 是在判断,当前系统里面的这个 线程 是不是真的有了。


另外,如果内核里线程把 run 干完了,此时线程销毁,pcb随之释放。但是 Thread t 这个对象还不一定被释放的。此时isAlive() 也是 false。(这个函数只关注内核里的线程是否在工作,不关注Thread所创建的对象是否还存在)

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                    for (int i = 0;i < 3;i++) {
                        System.out.println("hello");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
            }
        },"mythread");
        t.start();
        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(t.isAlive());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在执行完for的三次后,t被销毁,因此后续的isAlive()返回的都是false。

  • 线程的中断问题,下面我们进一步说明
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还
活着");
                    Thread.sleep(1 * 1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
       });
        System.out.println(Thread.currentThread().getName() 
                           + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() 
                           + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() 
                           + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() 
                           + ": 优先级: " + thread.getPriority());
                           System.out.println(Thread.currentThread().getName() 
                           + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() 
                           + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() 
                           + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName() 
                           + ": 状态: " + thread.getState());
   }
}

2.3 启动一个线程-start()

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程 就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

调用 start 方法, 才真的在操作系统的底层创建出一个线程.

通过循环打印"hello world" 和 "hello thread"来观察两个线程是怎么工作的。

由上图可知"hello world" 和 "hello thread"这两个字符串循环打印。

注:这里的"hello world" 和 "hello thread"他们的打印顺序是随机的,内核里本身并非是随机的,但是干扰因素太多,并且应用程序这一层也无法感知到细节,就只能认为是随机的了!

如果把上述代码的t.start改成t.run那么会在run中出不来,相当于只有一个线程在干活!!!

 

C:\Program Files\Java\jdk1.8.0_192\bin在这里我们可以找到jconsole这个查看进程的工具

找到我们idea中运行的这个进程

由于是我们自己的电脑,所以很安全不会存在不安全一说。

连接完选择线程这一类,我们就可以很清楚的看到我们thread中的所有线程。

被我们红方框圈出来的就是我们的调用栈,描述了当前方法之间的调用关系。

2.4 中断一个线程

我们线程中的中断不是让线程立即就停止,而是通知线程你应该要停止了。是否真的停止,取决于线程这里具体的代码写法。此时线程有三个选择:

1.立即中断

2.稍后中断

3.不中断

李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们 需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如 何通知李四停止呢?这就涉及到我们的停止线程的方式了。

目前常见的有以下两种方式:

1.通过共享的标记来进行沟通

2.调用 interrupt() 方法来通知

示例-1: 使用自定义的变量来作为标志位.

我们自定义falg为标志位,并在一开始设置为true

package thread;
public class ThreadDemo8 {
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           while (flag) {
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
        Thread.sleep(3000);
        //在主线程里就可以随时通过 flag 变量的取值,来操作 t 线程是否结束。
        flag = false;
    }
}

因为这里休眠3000毫秒后,flag变为false,因此我们的线程循环while中变为false而终止线程。

示例-2: 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定 义标志位.

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.

  • 使用 thread 对象的 interrupted() 方法通知线程结束.
  • thread 收到通知的方式有两种:

1.如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志

1.当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择 忽略这个异常, 也可以跳出循环结束线程.

2.否则,只是内部的一个中断标志被设置,thread 可以通过

1.Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志 false 变 true

2.Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志 中断标志为false

这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

package thread;
public class ThreadDemo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }
}

这里interrupt在将线程内部的标志位(boolean)给设置为true,如果线程在进行sleep,就会触发异常,把sleep唤醒。

但是sleep在唤醒时,还会做一件事,把刚才设置的这个标志位,再设置回false。(清空了标志位)

这就导致了sleep的异常被catch完了之后,循环还要继续执行。

我们这里为大家提供了解决这个方法的三个情况:

1.

线程t忽略了你的终止请求。

2.

线程t立即响应你的终止请求

3.

稍后进行终止

唤醒之后线程到底要终止,还是要执行,到底是立即终止还是稍后,就把选择权交给程序猿自己了。

示例-3 观察标志位是否清除

标志位是否清除, 就类似于一个开关.

Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”

Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为 “不清除标志位”.

  • 使用 Thread.isInterrupted() , 线程中断会清除标志位.
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
           }
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
  • 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
 System.out.println(Thread.currentThread().isInterrupted());
           }
           }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true

2.5 等待一个线程-join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转 账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() 
                                       + ": 我还在工作!");
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
       };
        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
   }
}

大家可以试试如果把两个 join 注释掉,现象会是怎么样的呢?

附录

 

package thread;
public class ThreadDemo9 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("join 之前");
        //此处的 join 就是让当前的 main 线程来等到 t 线程执行结束 (等待 t 的 run 执行完)
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("join 之后");
    }
}

本身执行完start之后,t线程和main线程就并发执行,分头行动。

main继续往下执行,t也会继续往下执行。

遇到t.join()

就会发生阻塞

一直阻塞到,t线程结束,main线程才会从join中恢复过来,才能继续往下执行。(t线程肯定比main线程先结束)

 

如果开始执行join的时候已经结束了,join就不会阻塞,就会立即返回。

2.6 获取当前线程引用

这个方法我们以及非常熟悉了

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

在哪个线程中调用,就能获取到哪个线程的实例。

2.7 休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实 际休眠时间是大于等于参数设置的休眠时间的。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }
}

被sleep的PCB(线程)就相当于放到了阻塞队列中,我们程序继续运行非阻塞队列,当sleep的时间耗完时,我们的PCB就回到非阻塞队列中继续运行。

感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘

目录
相关文章
|
4月前
|
调度 C语言
深入浅出:C语言线程以及线程锁
线程锁的基本思想是,只有一个线程能持有锁,其他试图获取锁的线程将被阻塞,直到锁被释放。这样,锁就确保了在任何时刻,只有一个线程能够访问临界区(即需要保护的代码段或数据),从而保证了数据的完整性和一致性。 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含一个或多个线程,而每个线程都有自己的指令指针和寄存器状态,它们共享进程的资源,如内存空间、文件句柄和网络连接等。 线程锁的概念
184 1
|
5月前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
6月前
|
安全 算法 Java
多线程知识点总结
多线程知识点总结
66 3
|
6月前
|
设计模式 安全 编译器
多线程(初阶六:单例模式)
多线程(初阶六:单例模式)
54 0
|
6月前
|
Java 测试技术 程序员
多线程(初阶九:线程池)
多线程(初阶九:线程池)
69 0
|
安全 Linux 程序员
「技术干货」一文搞懂C语言内存模型与栈
「技术干货」一文搞懂C语言内存模型与栈
|
11月前
|
Java Linux API
【JavaEE初阶】一文带你了解线程、多线程基础
【JavaEE初阶】一文带你了解线程、多线程基础
52 0
|
算法 Ubuntu C++
[总结] C++ 知识点 《四》多线程相关
[总结] C++ 知识点 《四》多线程相关
|
Java 程序员 调度
多线程(初阶)——多线程基础
多线程(初阶)——多线程基础
88 0
|
安全 Java 程序员
多线程初阶——线程安全
多线程初阶——线程安全
75 0