并发编程之深入理解Java线程

news2024/11/24 3:49:00

并发编程之深入理解Java线程

线程基础知识

线程和进程

进程

  • 程序由指令和数据组成、但这些指令要运行,数据要读写,就必须要将指令加载至CPU、数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程
  • 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是系统资源分配的最小单位。

线程

  • 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须由一个父进程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
  • 线程,有时也被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位
进程和线程的区别
  • 进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算的进程通信称为IPC(Inter-processcommunication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信相对简单,因为他们共享进程内的内存,一个例子是多个进程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般要比进程上下文切换低
进程间通信的方式
  • 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它允许无亲缘关系线程间进行通信
  • 信号(signal):信号是软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号于处理器收到一个中断请求效果上是一致的。
  • 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量优先的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取消息。
  • 共享内存(shared memory):最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中的数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  • 信号量(semaphore):主要作为进程之间及同一种进程之间得同步和互斥手段
  • 套接字(socket):这是一种更为一般得进程间得通信机制,它可用于网络中不同机器之间的进程间通信
线程的同步互斥
  • 线程同步是指线程之间具有一种制约关系,一个线程的执行依赖于另一个线程的消息,当它没有得到另一个线程的消息时应等待,知道消息到达时才会被唤醒。
  • 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排他性。当有若干个线程都要使用某一共享资源是,任何时刻最多只允许一个线程去使用,其他要使用该资源的线程必须要等待,直到占用资源者释放该资源。线程互斥可以看成一种特殊的线程同步。
四种线程同步互斥的控制方法
  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问(在一段时间内只允许一个线程访问的资源就是临界资源)
  • 互斥量:为协调共同对一个共享资源的单独访问而设计的
  • 信号量:为控制一个具有有限数量用户资源而设计。
  • 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始
上下文切换(Context switch)

上下文切换是指CPU(中央处理单元)从一个线程或进程到另一个线程或进程的切换。

进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源

上下文是CPU寄存器和程序计时器在任何时间点的内容

寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。

程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统

image-20240214215412696

上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动

  • 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
  • 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它。
  • 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。

image-20240214215657229

上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但他们可以通过系统调用运行部分内核代码。

内核模式(Kernal Mode) vs 用户模式(User Mode)

Kernal Mode

  • 在内核模式中,执行代码可以完全且不受现在地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪

User Mode

  • 在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。

image-20240214220302903

CPU保护模式

image-20240214220336301

应用程序以下几种情况会切换到内核模式:

  • 系统调用:当应用程序需要执行诸如读写文件、创建进程、建立网络连接等操作时,需要通过系统调用请求操作系统提供服务。
  • 异常事件:当程序执行出错,如除零错误、访问非法内存等,会触发异常,操作系统需要切换到内核态来处理这些异常。
  • 设备中断:当外部设备(如键盘、鼠标、网络接口卡等)发出中断信号时,操作系统需要切换到内核态来处理这些中断

上下文切换是多任务操作系统的一个基本特性。在多任务操作系统中,多个进程似乎同时在一个CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃他们在CPU中的事件,或者是调度器在进程耗尽其CPU时间片时进行切换的结果。

上下文切换通常是计算机密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本,时间上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一就是它的上下文切换和模式切换成本极低。

通过命令查看CPU上下文切换情况

Linux可以通过命令统计CPU上下文切换数据

# 可以看到整个操作系统每1秒CPU上下文切换的统计
vmstat 1

image-20240214221800673

其中cs列就是CPU上下文切换的统计。CPU上下文切换不等价于线程切换,很多操作会造成CPU上下文切换。

  • 线程、进程切换
  • 系统调用
  • 中断
查看某一个线程\进程的上下文切换

使用pidstat命令

  • 常用的参数:

    -u 默认参数,显示各个进程的 CPU 统计信息

    -r 显示各个进程的内存使用情况

    -d 显示各个进程的 IO 使用

    -w 显示各个进程的上下文切换

    -p PID 指定 PID

# 显示进程5598每一秒的切换情况
pidstat -w -p 5598 1

image-20240214222213640

其中cswch表示主动切换,nvcswsh表示被动切换。

  • 从进程的状态信息中查看

    通过命令 cat /proc/5598/status 查看进程的状态信息

    voluntary_ctxt_switches: 40469351

    nonvoluntary_ctxt_switches: 2268

    这2项就是该进程从启动到当前总的上下文切换情况。

操作系统层面线程生命周期

操作系统层面的线程的生命周期基本上可以用下图“五台模型”来描述,这五态分别是:初始状态、可运行状态、运行状态、休眠状态和中止状态

image-20240214222636764

  • 初始状态:指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
  • 可运行状态:指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
  • 当有空闲CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态
  • 运行状态的线程如果调用一个阻塞的API(例如以阻塞方式读文件)或者等待某个时间(例如条件变量),那么线程的状态就会切换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  • 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

Linux

  • ps -ef 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

Java线程详解

Java线程的实现方式

方式1:使用Thread类或继承Thread类

// 创建线程对象
Thread t = new Thread() {
    public void run() {
    // 要执行的任务
    }
};
// 启动线程

方式2:实现 Runnable 接口配合Thread

Runnable runnable = new Runnable() {
    public void run(){
    // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程

方式3:使用有返回值的Callable

class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());

方式4:使用lambda

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

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终会调用Thread#run方法

Java线程实现的原理

Jva线程执行为什么不能直接调用run()方法,而是要调用start()方法

run()方法是普通方法调用,start()会创建线程进程调用

https://www.processon.com/view/link/5f02ed9e6376891e81fec8d5

Java线程属于内核级线程

JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。

内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。

用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。

image-20240214230110067

协程

协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

image-20240214230154684

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。

  • 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。

  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。

Java线程的调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要的调度方式分为两种,分别是协同式线程调度和抢占式线程调度。

协同式线程调度

  • 线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

抢占式线程调度

  • 每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程调度就是抢占式调度

希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

public class SellTicketDemo implements Runnable {
    /**
     * 车票
     */
    private int ticket;

    public SellTicketDemo() {
        this.ticket = 1000;
    }

    @Override
    public void run() {
        while (ticket > 0) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        // 线程进入暂时的休眠
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 获取到当前正在执行的程序的名称,打印余票
                    System.out.println(Thread.currentThread().getName()
                            + ":正在执行操作,余票:" + ticket--);
                }
            }
            Thread.yield();
        }
    }

    public static void main(String[] args) {
        SellTicketDemo demo = new SellTicketDemo();

        Thread thread1 = new Thread(demo, "thread1");
        Thread thread2 = new Thread(demo, "thread2");
        Thread thread3 = new Thread(demo, "thread3");
        Thread thread4 = new Thread(demo, "thread4");
        //priority优先级默认是5,最低1,最高10
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        thread3.setPriority(Thread.MIN_PRIORITY);
        thread4.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

上述程序并不一定是thread1和thread2抢到的票最多

Thread.yield()的使用

用于暂停当前正在执行的线程,让出CPU的使用权,以允许其他线程执行,但是Thread.yield()并不能保证当前线程立即停止执行,具体的行为取决于线程调度器

Java线程的生命周期

Java语言中线程共有六种状态,分别是:

  • NEW(初始化状态)
  • RUNNABLE(可运行状态+运行状态)
  • BLOCKED(阻塞状态)
  • WAITING(无时限等待)
  • TIMED_WAITING(有时限等待)
  • TERMINATED(终止状态)

在操作系统层面,Java线程中的BLOCKED、WAITING、TIMED_WAITING 是一种状态,即休眠状态,也就是只要Java线程处于这三个状态,那么这个线程永远没有CPU的使用权

image-20240220204626915

从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)

image-20240220204709578

从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)

image-20240220204805548

Thread常用的方法

sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入的参数为0时,和yield相同

yield方法

  • yield会释放CPU资源,让当前线程从Running进入Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
  • 具体的实现依赖于系统的任务调度器

join方法

  • 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t begin");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t finished");
            }
        });
        long start = System.currentTimeMillis();
        t.start();
        t.join();
        System.out.println("执行时间:" + (System.currentTimeMillis() - start));
        System.out.println("Main finish");
    }
}

stop方法

stop()方法已经被JDK废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。

stop会释放锁,可能造成数据不一致

Java线程的中断机制
  • Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协同机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程有用完全的自主权,它即可以选择理解停止,也可以选择一段时间后停止,也可以选择压根不停止。

API的使用

  • interrupt():将线程的中断标志设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为false。
public class ThreadInterruptTest {

    static int i = 0;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    i++;
                    System.out.println(i);
                    // Thread.interrupted() 清除中断标志位   --> 只会输出一个 ==========
                    // Thread.currentThread().isInterrupted() 不会清除中断标志位   -> 会输出十个==========
                    if (Thread.interrupted()) {
                        System.out.println("==========");
                    }
                    if (i == 10) {
                        break;
                    }
                }
            }
        });
        t1.start();
        t1.interrupt();
    }
}

利用中断机制优雅的停止线程

while (!Thread.currentThread.isInterrupted() && more work to do) {
	do more work
}
public class StopThread implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
        }
        System.out.println("线程停止 : stop thread");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}

使用中断机制时要注意是否中断标志位被清除的情况

public class StopThread implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 重新设置线程中断状态位true
                Thread.currentThread().interrupt();
            }
        }
        System.out.println("线程停止 : stop thread");

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}

处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置为false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。

sleep可以被中断 抛出中断异常 : sleep interrupted,清除中断标志位

wait可以被中断 抛出中断异常 InterruptedException 清除中断标志位

Java线程间通信
  • volatile

volatile有两大特性,一个是可见性,二是有序性,禁止指令重排序,其中可见性就是让线程之间通信

public class VolatileDemo {

    private static volatile boolean flag = true;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (flag){
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (!flag){
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}


### 结果

trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
trun on
trun off
  • 等待唤醒(等待通知)机制

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用改线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒

public class WaitDemo {

    private static Object lock = new Object();
    private static boolean flag = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    while (flag) {
                        try {
                            System.out.println("wait start .....");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("wait end..........");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag) {
                    synchronized (lock) {
                        if (flag) {
                            try {
                                Thread.sleep(2000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            lock.notifyAll();
                            System.out.println("notify ......");
                            flag = false;
                        }
                    }
                }
            }
        }).start();
    }
}

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

public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();

        System.out.println("唤醒parkThread");
        // 为指定线程parkThread提供许可
        LockSupport.unpark(parkThread);
    }

    static class ParkThread implements Runnable {
        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            // 等待许可
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}
  • 管道输入输出流

管道输入/输出流和普通文件输入/输出流或网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输媒介为内存。管道输入/输出主要包括了以下四种实现:

PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符

public class PipedTest {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输入流和输出流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();

        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }


    static class Print implements Runnable {

        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.println((char) receive);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  • Thread.join

join可以理解成线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实是已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现是基于等待通知机制的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1464225.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目解决方案:校园云视频平台方案(视频接入、汇聚、联网、分享)

目 录 一、项目需求 二、系统设计方案 三、平台功能 四、案例展示 本方案分四个部分&#xff1a;项目需求、系统设计方案、平台基础功能、案例展示&#xff0c;如下&#xff1a; 一、项目需求 二、系统设计方案 通过AS-V1000视频资源综合管理平台实现监控视频的接入、…

成年人学英语其实有个捷径,但你们都不信

上班了…… 我不想上班&#xff0c;只想躺平&#xff0c;同时银行卡上的余额还能够不断的增加。 当然现阶段肯定是不行的&#xff0c;我仍要靠打工养活自己&#xff0c;而且先要获得第一桶金。 第一桶金在何方&#xff1f;我还不知道&#xff0c;人在迷茫时&#xff0c;就来学英…

docker:Haoop集群

系列文章目录 docker&#xff1a;环境安装 docker:Web迁移 docker:Haoop集群 文章目录 系列文章目录前言一、宿主机选择二、环境准备1.前置技术2.网络环境1. docker网卡2. 分配IP 三、容器互联三、Jdk和Hadoop安装四、分发脚本五、启动Hadoop总结 前言 年前学习了docker的相关…

新疆营盘古城及古墓群安防舱体实施方案

3 总体布局 3.1设计原则 3.1.1执行有效的国家标准、国家军用标准和行业标准&#xff1b; 3.1.2满足指标要求&#xff1b; 3.1.3采用通用化、模块化设计&#xff0c;提高设备可维修性&#xff1b; 3.1.4采用人机工程学知识进行设计&#xff0c;充分考虑安全性。 3.2 总体…

Sora的第一波受害者出现了。

不知道大家最近除了被Sora刷屏之外&#xff0c;有没有被这张图刷屏 我只能说网友太强大了 说实话&#xff0c;我进入舟老师的直播间&#xff0c;每次都是还有3分钟下播&#xff0c;还有6单就拍完 但是10分钟后还在激情逼单&#xff0c;6单之后还有6单 也许在营销学上&#x…

亚马逊工程师严选,超 40 篇 LLM 论文汇总

2023 年&#xff0c;大语言模型依旧是「话题制造机」&#xff0c;不管是 OpenAI 的「宫斗剧」&#xff0c;还是各个大厂的新模型、新产品「神仙打架」&#xff0c;亦或是行业大模型发展的风生水起&#xff0c;都昭示着大语言模型具备巨大的发展空间。花香自引蝶&#xff0c;其实…

LeetCode 算法题 (数组)存在连续3个奇数的数组

问题&#xff1a; 输入一个数组&#xff0c;并输入长度&#xff0c;判断数组中是否存在连续3个元素都是奇数的情况&#xff0c;如果存在返回存在连续3个元素都是奇数的情况&#xff0c;不存在返回不存在连续3个元素都是奇数的情况 例一&#xff1a; 输入&#xff1a;a[1,2,3…

探索Django路由规则(路由匹配、路由命名空间、HTML中的跳转与Django集成、路由传参以及后端重定向)

路由管理&#xff1a; ​ 在实际开发过程中&#xff0c;⼀个Django 项⽬会包含很多的 app &#xff0c;这时候如果我们只在主路由⾥进⾏配置就会显得杂乱⽆章&#xff0c;所以通常会在每个 app ⾥&#xff0c;创建各⾃的 urls.py 路由模块&#xff0c;然后从根路由出发&#x…

leetcode日记(32)字符串相乘

做了很久很久……真的太繁琐了&#xff01;&#xff01; class Solution { public:string multiply(string num1, string num2) {string s;string str;if (num1 "0" || num2 "0") return "0";for(int inum2.size()-1;i>0;i--){int c2num2[…

代码随想录算法训练营第三十八天|509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯。

509. 斐波那契数 题目链接&#xff1a;斐波那契数 题目描述&#xff1a; 斐波那契数 &#xff08;通常用 F(n) 表示&#xff09;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c…

2024 高级前端面试题之 计算机通识(基础) 「精选篇」

该内容主要整理关于 计算机通识&#xff08;基础&#xff09; 的相关面试题&#xff0c;其他内容面试题请移步至 「最新最全的前端面试题集锦」 查看。 计算机基础精选篇 一、网络1.1 UDP1.2 TCP1.3 HTTP1.4 DNS 二、数据结构2.1 栈2.2 队列2.3 链表2.4 树2.5 堆 三、算法3.1 时…

阿里云配置服务器详细指南_2024年CPU内存带宽配置选择

阿里云服务器配置怎么选择&#xff1f;根据实际使用场景选择&#xff0c;个人搭建网站可选2核2G配置&#xff0c;访问量大的话可以选择2核4G配置&#xff0c;企业部署Java、Python等开发环境可以选择2核8G配置&#xff0c;企业数据库、Web应用或APP可以选择4核8G配置或4核16G配…

Sora热潮 | 暴雨AI服务器助推大模型向前发展

Sora全球爆火这事还有谁不知道吗&#xff1f; 2月16日&#xff0c; OpenAI发布了一条由视频大模型Sora所自动生成的视频&#xff0c;逼真的视觉效果让其在一夜之间“刷屏”。 一石激起千层浪&#xff0c;Sora的发布让科技从业者&#xff0c;投资圈、影视行业纷纷“炸锅“&…

【双指针】:LCR179.查找总价值为目标值的两个商品

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本专栏是关于各种算法的解析&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结构专栏&…

数据结构D3作业

1. 2. 按位插入 void insert_pos(seq_p L,datatype num,int pos) { if(LNULL) { printf("入参为空&#xff0c;请检查\n"); return; } if(seq_full(L)1) { printf("表已满&#xff0c;不能插入\n"); …

Spring 类型转换、数值绑定与验证(二)—PropertyEditor与Conversion

Spring 中&#xff0c;属性类型转换是在将数值绑定到目标对象时完成的。例如在创建ApplicationContext 容器时&#xff0c;将XML配置的bean 转换成Java类型对象&#xff0c;主要是借助了PropertyEditor类&#xff0c;而在Spring MVC 的Controller的请求参数转化为特定类型时&am…

为M系Mac安装Centos

下载镜像 需要使用特殊镜像&#xff0c;官网或国内的arch 镜像源不可安装 https://share.weiyun.com/2qc0S2VV CentOS-7-aarch64-08191738.mpg https://www.aliyundrive.com/s/1DCW2E5EySR 原文链接&#xff1a;https://blog.csdn.net/acdemic964850/article/details/1290565…

ROS问题记录

目前遇到的问题&#xff1a; 1 、包名大写会警告 包名不要出现大写 2、catkin_make前配置环境变量 尤其在更换终端时&#xff0c;一定要再配置一遍环境变量&#xff0c;常见的错误如下 基本上这个错误都是因为没有执行以下命令 source ~/catkinws/devel/setup.bash3、调用…

ABAP 某些列无法正常筛选

某些列无法正常筛选 如果无法筛选&#xff0c;要注意前导0是否添加&#xff0c;数据库是没有前导0&#xff0c;但是一旦筛选&#xff0c;就是自动加上前导0去筛选&#xff0c;所以筛选不到数据

【JVM】五种对象引用

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;JVM ⛺️稳中求进&#xff0c;晒太阳 几种常见的对象引用 可达性算法中描述的对象引用&#xff0c;一般指的是强引用&#xff0c;即是GCRoot对象对普通对象有引用关系&#xff0c;只要这层…