深入理解并发、线程与等待通知机制

发布时间:2024年01月07日

目录

进程和线程

延伸:进程间的通信有哪些?

并行和并发

线程的启动与中止

启动

面试题:新启线程有几种方式?

中止

深入理解 run()和 start()

线程的状态/生命周期

线程的优先级

线程的调度

线程间的通信和协调工作

join()

volatile轻量的通信/同步机制

等待/通知机制

等待/通知机制的标准范式


进程和线程

进程: 想象你在做一道菜,每道菜都是一个不同的任务。你需要烧个汤、煮个饭、炸个鸡。每道菜都需要使用一些厨房的资源,比如炉灶、锅碗瓢盆等。这里,整个做菜的过程就是一个进程,而每道菜是进程中的不同任务。每次你开始做新的一道菜,就好像启动了一个新的进程。

线程: 现在,想象你在同时处理一道菜的不同步骤,比如烧汤的同时准备配菜。这样你就可以更有效率地使用厨房的资源。这里,每个步骤就是一个线程。你可以同时进行多个步骤,每个步骤都共享同一个厨房的环境。

延伸:进程间的通信有哪些?

  1. 管道(Pipe):管道是一种半双工的通信方式,允许一个进程写入数据到管道,而另一个进程从管道读取数据。通常用于父子进程之间的通信。

  2. 命名管道(Named Pipe):与普通管道类似,但可以通过给定的名字在不同进程之间进行通信。适用于无亲缘关系的进程之间的通信。

  3. 消息队列(Message Queues):进程可以通过消息队列发送和接收消息。消息队列允许异步通信,发送方将消息放入队列,接收方从队列中取出消息。多个进程可以通过共享相同的队列进行通信。

  4. 信号(Signal):信号是一种轻量级的进程间通信方式,用于通知接收进程某个事件的发生。信号通常用于处理异步事件,如进程终止、错误等。

  5. 共享内存(Shared Memory):共享内存允许多个进程访问同一块内存区域。这样的共享内存区域被映射到每个进程的地址空间,进程可以直接读写这块内存,实现高效的数据共享。需要谨慎处理同步和互斥问题。

  6. 套接字(Socket):套接字是一种网络通信的方式,但也可以用于进程间通信,特别是在不同计算机之间。通过套接字,进程可以在网络上发送和接收数据。

  7. 文件映射(File Mapping):文件映射允许多个进程共享同一文件,通过将文件映射到内存中,多个进程可以直接访问这块内存。

  8. 远程过程调用(RPC):RPC 允许一个进程调用另一个进程的过程,就像调用本地过程一样。通过 RPC,进程可以在远程系统上执行代码。

并行和并发

并发: 想象你正在做饭,同时要煮面和炒菜。虽然你不能同时做两件事,但你可以在煮面的时候翻炒一下菜,然后再回去搭理面。这样感觉上好像是两个事情同时在进行,这就是并发。在计算机中,就像一个处理器在不同任务之间快速切换,让它们好像同时在进行一样。

并行: 现在,想象你有两个灶台,可以同时在两个锅里做不同的菜。这样你就能真正同时进行两个任务,一个人在处理多个任务,就像多个处理器在同一时刻执行不同的任务,这就是并行。

两者区别:一个是交替执行,一个是同时执行。


线程的启动与中止

启动

1、X extends Thread;,然后 X.start

2、X implements Runnable;然后交给 Thread 运行

Thread 和 Runnable 的区别:Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑) 的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。

面试题:新启线程有几种方式?

? ? ? ? Java 源码中 Thread 上的注释已经说明只有两种,官方说法是在 Java 中有两种方式创建一个线程用以执行,一种是派生自 Thread 类,另一种是实现 Runnable 接口。

? ? ? ? 本质上 Java 中实现线程只有一种方式,都是通过 new Thread()创建线程 对象,调用Thread.start 启动线程。

? ? ? ? 至于基于 callable 接口的方式,因为最终是要把实现了 callable 接口的对象 通过 FutureTask 包装成 Runnable,再交给 Thread 去执行,所以这个其实可以和 实现 Runnable 接口看成同一类。

? ? ? ?线程池的方式,本质上是池化技术,是资源的复用,和新启线程没什么关系。

中止

1. 线程自然终止

? ? ? ? 要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

2.?stop

? ? ? ? 暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。

3.?中断

? ? ??线程的中断是一种用于通知线程停止执行的机制。中断并不意味着强制终止线程,而是通过设置线程的中断标志来传达一个请求,告诉线程应该停止正在做的事情。

? ? Thread 类提供了 interrupt() 方法,用于设置线程的中断标志。

? ? Thread 类还提供了 isInterrupted() 方法,用于检查线程的中断状态。

? ? ? ?线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted() 会同时将中断标识位改写为 false。

? ? ? ?如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。

? ? ? 注意:处于死锁状态的线程无法被中断

深入理解 run()和 start()

? ? ? ? Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了 start()方法后,才实现了真正意义上的启动线程。

? ? ? ?从 Thread 的源码可以看到,Thread 的 start 方法中调用了 start0()方法,而 start0()是个 native 方法,这就说明 Thread.start 一定和操作系统是密切相关的。 start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现 的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常(注意:多次调用一个线程的 start 方法会抛出异常)。

? ? ? ?而 run 方法是业务逻辑实现的地方,本质上和普通方法并没有任何区别,可以重复执行,也可以被单独调用。

线程的状态/生命周期

Java 中线程的状态分为 6 种:

1. 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。

2. 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种 状态笼统的称为“运行”。 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start()方法。 该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权, 此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中 状态(running)。

3. 阻塞(BLOCKED):表示线程阻塞于锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作 (通知或中断)。

5. 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时 间后自行返回。

6. 终止(TERMINATED):表示该线程已经执行完毕。

线程的优先级

? ? ? ?在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

? ? ? ?设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较 高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定。

线程的调度

线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种:
? ? ? ? 协同式线程调度(Cooperative Threads-Scheduling)

? ? ? ? 抢占式线程调度(Preemptive Threads-Scheduling)

? ? ? ? 使用协同式线程调度的多线程系统,线程执行的时间由线程本身来控制,线 程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。使用协同 式线程调度的最大好处是实现简单,由于线程要把自己的事情做完后才会通知系 统进行线程切换,所以没有线程同步的问题,但是坏处也很明显,如果一个线程 出了问题,则程序就会一直阻塞。

? ? ? ?使用抢占式线程调度的多线程系统,每个线程执行的时间以及是否切换都由 系统决定。在这种情况下,线程的执行时间不可控,所以不会有「一个线程导致 整个进程阻塞」的问题出现。

? ? ? ?java中的线程就是抢占式的,原因如下:

  1. 多任务环境: Java通常在多任务环境中运行,即有多个线程同时执行。在这样的环境下,采用抢占式调度可以确保高优先级的任务能够及时获得执行权,提高系统的响应性。

  2. 优先级调度: 抢占式调度可以根据线程的优先级进行调度。线程的优先级通常与任务的紧急性和重要性相关,因此通过抢占式调度,可以更灵活地响应系统中不同优先级任务的需求。

  3. 防止饥饿: 抢占式调度有助于防止线程饥饿。如果采用协同式调度,一个线程可能由于某些原因(如无限循环、阻塞等)一直占用CPU,导致其他线程无法执行。而抢占式调度可以在一段时间后强制剥夺当前线程的执行权,确保其他线程有机会执行。

  4. 实时系统需求: 在实时系统中,对任务的响应时间有严格的要求。采用抢占式调度可以更精确地满足这些实时要求,确保高优先级任务能够及时执行。

? ? ? ?总体来说,抢占式调度提供了更灵活、更可控的线程调度机制,能够更好地适应不同类型任务和不同优先级的线程。这种机制有助于提高系统的效率、响应性和公平性。在Java中,抢占式调度是由操作系统或Java虚拟机的调度器负责的。

线程间的通信和协调工作

join()

? ? ? ?把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续 执行线程 B 剩下的代码。

volatile轻量的通信/同步机制

? ? ? ? volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某 个变量的值,这新值对其他线程来说是立即可见的。

等待/通知机制

? ? ? ?这种机制是指一个线程 A 调用了对象 O 的wait()方法进入等待状态,而另一个线程 B 调用了对象O的notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的 wait()和notify/notifyAll() 的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

notify()

? ? ? ?通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程 获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。

notifyAll()

? ? ? ?通知所有等待在该对象上的线程 wait() 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断 才会返回.需要注意,调用 wait()方法后,会释放对象的锁。

wait(long)

? ? ? ? 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达 n 毫秒,如果没有 通知就超时返回 wait (long,int) 对于超时时间更细粒度的控制,可以达到纳秒。

等待/通知机制的标准范式

等待方遵循如下原则

1. 获取对象的锁。

2. 如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。

3. 条件满足则执行对应的逻辑。

synchronize(对象){
       while(条件不满足){
            对象.wait();
       } 
}

通知方遵循如下原则

1. 获得对象的锁。

2. 改变条件。

3. 通知所有等待在对象上的线程。

synchronize(对象){
      改变条件
      对象.notifyAll();
}

notify和notifyAll选择

? ? ? ?尽可能用notifyall(),谨慎使用 notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

文章来源:https://blog.csdn.net/m0_61853556/article/details/135442579
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。