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

目录

进程和线程

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

并行和并发

线程的启动与中止

启动

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

中止

深入理解 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()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值