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

70 阅读5分钟

基本概念

进程

相当于一个app的实例,站在系统角度,进程是程序运行资源分配(以内存为主)的最小单位,可以没有线程。一次只能做一件事,要想做多件事必须启动线程

线程

线程是CPU调度的最小单位,线程依赖于进程,必须有进程。linux早期把线程称之为轻量级进程

cpu核心数和线程数关系

image.png

上下文切换

代价较大5000-20000时钟周期,调优时需要重点考虑

image.png

并行和并发

并行,同时进行;并发:交替执行

实现

  1. 继承Thread类,重写run方法,创建MyThread实例,执行start方法
  2. 实现Runable接口,重写run方法,创建MyRunable实例给到Thread中,执行start方法
  3. 实现Callable接口,重写call方法,将MyCallable实例给到FutureTask进行包装,再将futureTask实例给到Thread中,执行start方法,通过futureTask.get()获取线程返回结果

终止

自然终止

  • run方法结束
  • run方法抛出异常

方法终止

  • stop/suspend/resume已弃用,即使终止也会占用资源
  • 中断机制:interrupt方法执行后将中断标志位设置为true(相当于给对应线程打个招呼,提示你要中断了),这个时候就需要通过isInterrupted或interrupted方法判断是否需要中断,区别是interrupted执行后会将标志位设置为false,而isInterrupted不变。不建议自定义标志位,原因是阻塞方法不会被中断了。 标准代码示范:
    @Override
    public void run() {
        while (!this.isInterrupted()) {
            try {
                
            } catch (InterruptedException e) {
                this.interrupt();
            }
        }
    }

注意:抛出interrupted异常后,要interrupt()一下,因为异常后会将标志位重新改为false

线程状态/生命周期

1700124641987.png 1700124557535.png

线程的调度

  • 协同式线程调度
  • 抢占式线程调度:提高利用率(java使用)

线程和协程

任何语言实现线程有三种方式:使用内核线程实现(1:1);使用用户线程实现(1:N);使用用户线程和轻量级进程混合实现(N:M)

协程

用户线程实现,适合IO密集型场景,在计算密集型场景下不适用。 目前java比较出名的协程库是Quasar,它的实现原理是字节码注入,在字节码层面对当前被调用函数中的所有局部变量进行保存和恢复

守护线程

image.png

线程间的通信和协调、协作

管道输入输出流

image.png 示例代码:

public class Test {
    public static void main(String[] args) throws Exception {
        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream();
        pipedOutputStream.connect(pipedInputStream);
        Thread thread = new Thread(new MyThread(pipedInputStream),"inputStream");
        thread.start();
        int write = 0;
        while ((write = System.in.read()) != 0){
            pipedOutputStream.write(write);
        }
    }
}
class MyThread implements Runnable{
    private PipedInputStream inputStream;
    public MyThread(PipedInputStream inputStream){
        this.inputStream = inputStream;
    }
    @Override
    public void run() {
        int receive = 0;
        while (true){
            try {
                if (((receive = inputStream.read()) != 0)){
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

join()

image.png

synchronized内置锁

image.png

方式

方法上
同步代码块

可以是对象实例(this)、单独声明对象作为锁(obj)、类

对象锁和类锁

对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的

正确使用

保证锁的是同一对象,否则加锁毫无作用。(如:Integer做累加,对象发生了变化,即使锁了Integer实例,但结果仍然不正确)

volatile,最轻量的通信/同步机制

image.png 注意:它不是轻量级锁。

等待/通知机制

wait():等待

notify():通知某一个等待线程,不能指定线程唤醒(要指定唤醒考虑显示锁)

notifyAll():通知所有等待的线程

标准范式

等待方:
    加锁(对象){
        while(条件不满足){
            // 调用wait方法锁会释放
            对象.wait()
        }
        进入后面的业务逻辑
    }
通知方:
    加锁(对象){
        业务逻辑使得条件满足
        // 往往放在同步代码块最后一句,只有执行完同步代码块才会释放锁
        对象.notify()
    }

CompleteableFuture

实现任务编排的能力,可以将任务按照不同规则顺序组合起来。在传统Future上做了加强。

面试题

进程间通信(IPC)--大厂高频

  1. 管道:分为匿名管道及命名管道。匿名管道可用于有亲缘关系的父子进程间通信,命名管道除了具有管道所具有功能外,还允许无亲缘的进程间通信(注:子进程完全独立于父进程)
  2. 信号
  3. 信号量
  4. 消息队列
  5. 共享内存:需要依靠同步操作,如互斥锁和信号量
  6. 套接字:Mysql控制台

synchronized一定比cas要慢吗

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

创建线程有几种方式

官方说法,两种,解释Callable接口的方式本质上看只不过是包装成了FutureTask,但它仍然是实现了Runable接口。

为什么不建议在finalize方法中做资源回收呢?

执行该方法的Finalizer为守护线程。

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行。

通过join方法把指定的线程加入到当前线程中。

调用yield()/sleep()/wait()/notify()等方法对锁有何影响?

image.png

为什么wait()和notify()方法要在同步代码块中调用?

image.png 其实这就是所谓的lost wake up问题。

为什么应该在循环中检查等待条件

image.png

其他

线程优先级(了解,很少使用)

image.png