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

并发编程之深入理解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的实现是基于等待通知机制的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL库操作

1、创建数据库 概念 创建数据库&#xff1a;根据项目需求创建一个存储数据的仓库 使用create database 数据库名字创建 数据库层面可以指定字符集:charset/character set 数据库层面可以指定校对集:collate 创建数据库会在磁盘指定存放处产生一个文件夹 创建语法 create …

用户体验设计师如何在 2024抢占先机?

01. 严峻的经济形势和就业市场 我们生活在一个通货膨胀的时代。就从超市抓几个苹果、卷心菜、鸡蛋&#xff0c;看看价格吧&#xff01;我不得不多次检查收据&#xff0c;因为我简直不敢相信。外出就餐费用上涨了 10-20%&#xff0c;现在 Spotify 和 YouTube 要求收取更高的订阅…

深入理解 v-for 中 key 的重要性

查看本专栏目录 关于作者 还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#x…

OR-806A固态继电器SSR光耦,可替代AQW212

OR-806A 固态继电器 VL60V输出端击穿电压光耦 高隔离电压 60 至 600V 输出耐受电压 工业温度范围&#xff1a;-40 to 85℃ 高灵敏度和高速响应 特征 输入和输出之间的高隔离电压 &#xff08;Viso&#xff1a;5000 V rms&#xff09;。 控制低电平模拟信号 高灵敏度和…

springboot网站开发02-接入持久层框架mybatisPlus

springboot网站开发02-接入持久层框架mybatisPlus&#xff01;经过上一小节内容分享&#xff0c;我们的项目嵌套模式框架搭建好了&#xff0c;下面就是开始编辑具体的业务代码了&#xff0c;我们使用到了持久层框架是mybatisPlus插件。下面是一些具体的植入框架的操作步骤。 第…

post请求向服务器发送JSON格式数据设置Content-Type

情景&#xff1a;与后台联调接口时&#xff0c;后台要求传递JSON格式的数据。 处理&#xff1a;我们进行XHR请求时需要设置请求头的Content-Type值为application/json&#xff0c;如下图所示。 浏览器查看具体请求与参数格式&#xff08;形式&#xff09;如下&#xff1a;

查找库文件中是否包含某个函数,库文件是否包含某个全局变量,库文件是否包含某个文件

strings 指令 释义&#xff1a;在对象文件或二进制文件中查找可打印的字符串&#xff1a; 用法&#xff1a;需要结合 grep 指令&#xff1b;可以把库文件直接文本打开看下&#xff0c;有字符串的内容&#xff0c;都是可以搜到的 库文件文本方式打开示例 使用示例 # 查找函…

切换分支时候IDEA提示:workspace associated with branch feature has been restored

切换分支时候IDEA提示&#xff1a;workspace associated with branch feature has been restored 这个消息是指与"feature"分支关联的工作区已经恢复。在Git中&#xff0c;工作区是指你当前正在进行修改和编辑的文件和目录。当你切换分支时&#xff0c;Git会自动将工…

编程学习线上提问现场解答流程,零基础学编程从入门到精通

编程学习线上提问现场解答流程 一、前言 之前给大家分享的一款中文编程工具&#xff0c;越来越多的学员使用这个工具学习编程。 在学习中有疑难问题寻求解答流程 1、可以在本平台留言或发私信联系老师 2、可以在群提问及时解答问题 3、通过线上会议的方式&#xff0c;电脑…

uniapp腾讯地图JavaScript Api,H5端和原生APP端可用

因项目需要&#xff0c;在uniapp中集成使用腾讯地图&#xff0c;为了方便维护&#xff0c;希望通过一套代码实现H5和APP同时可用。H5显示相对简单&#xff0c;APP端比较麻烦&#xff0c;记录下实现过程 一、集成步骤 1.使用 renderjs script标签使用renderjs&#xff0c;因为…

dell戴尔电脑灵越系列Inspiron 15 3520原厂Win11系统中文版/英文版

Dell戴尔笔记本灵越3520原装出厂Windows11系统包&#xff0c;恢复出厂开箱预装OEM系统 链接&#xff1a;https://pan.baidu.com/s/1mMOAnvXz5NCDO_KImHR5gQ?pwd3nvw 提取码&#xff1a;3nvw 原厂系统自带所有驱动、出厂主题壁纸、系统属性联机支持标志、Office办公软件、MyD…

DWG控件Web CAD SDK v15发布,支持注释 2D 绘图

Web CAD SDK为ASP.NET控件&#xff0c;可用于通过Internet、Intranet、Sharepoint、Office 365 及其他在线 HTML5 启用技术查看DWG和其他CAD文件。该产品不要求安装 AutoCAD 或其他第三方应用程序或组件&#xff0c;提供该产品时附带 C# 示例。 我们发布了Web CAD SDK 15 &…

【Unity】如何使用Spine动画

1.下载&#xff0c;选择自己需要的版本下载 下载链接&#xff1a;http://zh.esotericsoftware.com/spine-unity-download 2.下载完&#xff0c;导入Unity里 3.把美术文件拖入Unity里&#xff0c;会自动生成Spine数据 ①_Atlas 文件是texture atlas文件 (.atlas.txt). 它包含对…

国内Twitter账号注册要注意什么?注册多个Twitter账号如何防止被封?

在这个数字化快速发展的时代&#xff0c;Twitter作为一个全球性的社交媒体平台&#xff0c;对于个人品牌塑造乃至跨境电商均有着不可忽视的影响力。然而&#xff0c;在中国大陆地区&#xff0c;对于Twitter账号注册以及如何安全地注册多个Twitter账号这样的话题&#xff0c;往往…

k8s-heml管理 17

Helm是Kubernetes 应用的包管理工具&#xff0c;主要用来管理 Charts&#xff0c;类似Linux系统的 yum。Helm Chart 是用来封装 Kubernetes 原生应用程序的一系列 YAML 文件。可以在你部署应用的时候自定义应用程序的一些 Metadata&#xff0c;以便于应用程序的分发。 对于应用…

同一个包下 golang run时报undefined

问题描述 今天在运行一个项目&#xff0c;一个包下有两个文件&#xff0c;分别是main.go和route&#xff0c;main函数在main.go文件中&#xff0c;main引用了route.go中的两个函数&#xff0c;SetupRoutes和SetupAdminRoutes go build 编译后&#xff0c;直接运行&#xff0c…

【DDD】学习笔记-发布者—订阅者模式

在领域设计模型中引入了领域事件&#xff0c;并不意味着就采用了领域事件建模范式&#xff0c;此时的领域事件仅仅作为一种架构或设计模式而已&#xff0c;属于领域设计模型的设计要素。在领域设计建模阶段&#xff0c;如何选择和设计领域事件&#xff0c;存在不同的模式&#…

连接查询(学习笔记)

通过对DQL的学习&#xff0c;我们可以很轻松的从一张数据表中查询出需要的数据&#xff1b;在企业的应用开发中&#xff0c;我们经常需要从多张表中查询数据&#xff08;例如&#xff1a;我们查询学生信息的时候需要同时查询学生的班级信息&#xff09;&#xff0c;可以通过连接…

伦茨lenze触摸屏维修p500系列P50GAP60300M5H0XXX-02S14315000

Lenze伦茨显示屏维修系列有&#xff1a;EL5800&#xff1b;EL2800&#xff1b;EL9800&#xff1b;EL2500&#xff1b;EL600&#xff1b;P300&#xff1b;P500. 伦茨触摸屏不能触摸维修&#xff1a;触摸屏幕时鼠标箭头无动作&#xff0c;没有发生位置改变。 原因&#xff1a;造…

多维时序 | Matlab实现LSTM-Mutilhead-Attention长短期记忆神经网络融合多头注意力机制多变量时间序列预测模型

多维时序 | Matlab实现LSTM-Mutilhead-Attention长短期记忆神经网络融合多头注意力机制多变量时间序列预测模型 目录 多维时序 | Matlab实现LSTM-Mutilhead-Attention长短期记忆神经网络融合多头注意力机制多变量时间序列预测模型预测效果基本介绍程序设计参考资料 预测效果 基…