线程与等待通知机制

141 阅读6分钟

1 操作系统中的线程与进程

1.1 什么是进程

进程是程序运行分配资源(内存为主)的最小单位

1.2 什么是线程

线程是CPU调度的最小单位

1.3 线程与进程的关系

线程依赖于进程,一个进程下可以拥有多个线程

image.png

1.4 java进程

java环境下,线程必须有一个父进程。由于jvm本身是一个进程,如果需要在jvm中去运行程序,必须是要去使用线程去运行我们编程的程序。

image.png

1.5 进程之间的通信

进程的几种通信方式:

1.管道: pipe-父子进程通信, named pipe-允许非父子进程间通信

2.信号:sign - 软件层面对中断的模拟

3.消息队列: 内存的消息队列

4.共享内存:数据同步

5.信号量:同步和互斥的手段

6.套接字:socket 除了用于进程通信,还能用于网络通信;mysql通过socket连接,所以可以在本机连,也可以远端连,使用同一套代码

1.6 CPU

1个CPU核心数同时可以执行一个线程,逻辑处理器,intel引入的超线程技术,1个物理核心视为2个逻辑核心

Runtime#availableProcessors 可以获取逻辑核心数,一般来说设置线程数和逻辑核心数相关

1.7 上下文切换

一个线程在CPU上运行时,cpu的内部存储会存放这个线程的运行相关的数据;当这个线程A被调度出去,线程B被调度进来的时候,操作系统还需要把A线程的运行时相关数据的数据保存,将B线程的运行数据重新载入。

中断,内核态用户态切换等操作会引发上下文切换,一般来说一次上下文切换会耗费5k到2w个时钟周期,执行指令只需要耗费几到几十个时钟周期,vmstat 可以查询上下文切换

parallel 并行-同时工作;concurrent 并发-交替执行,一段时间内做多件事情,一定会引起上下文切换

系统调用会产生系统调用的上下文切换,会调用操作系统的api

2 认识java线程

java线程的使用方法:

官方只有2种: 1创建一个线程 2派生自runnable

继承Thread,执行start方法



public class MyThread extends Thread{
    
    @Override
    public void run(){
        System.out.println("hello");
        
    }
}

实现Runnable方法,交给Thread执行

实现Callable 通过FutureTask封装成一个Runnable交给Thread执行,在通过get获取结果

private static class UserCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        
        return 22;
        
    }
}

public static void main(String[] args) {
    UserCallable userCallable = new UserCallable();
    FutureTask<Integer> futureTask = new FutureTask<Integer>(userCallable);
    new Thread(futureTask).start();
    
    //xxxx
    Integer integer = futureTask.get();
    
}

线程池

复用创建的线程

2.1 线程的使用

不建议使用的方法:

suspend() 挂起,资源不释放

stop() 直接停止,没有执行完操作,资源没有正确的释放

interrupt()-线程的中断

优雅的中断线程的执行

告诉运行的线程你要中断了,线程可通过isInterrupted()来自己实现中断;不建议自定义取消标记来自己实现,当线程内部存在阻塞方法时,例如sleep,take等方法,线程不知道这个标志位变化了,反应没有这么快速。jdk凡是阻塞类的方法都会有InterruptedException,会重新把true改为false,可以捕获这个异常处理中断的情况。

处于死锁的线程无法被中断

public class MyThread extends Thread{
  @Override
    public void run(){


        while (!isInterrupted()){
            System.out.println("hahaha");
        }

    }




    public static void main(String[] args) throws InterruptedException {


        MyThread thread = new MyThread();
        thread.start();
        Thread.sleep(10);

        thread.interrupt();
        System.out.println("interrupt");
        Thread.sleep(10);


    }
  }

start()-线程的启动

start为什么不允许执行2次?

因为start方法是真正启动一个线程,一个Thread对象只能映射一个线程,所以不允许执行2次; start方法就会将thread对象和真正的线程进行挂钩

2.2 线程的状态

image.png

yield()-让出cpu调度,concurrentHashMap 初始化过程使用了yield

2.3 线程的调度

协同式线程调度 用完cpu主动通知其他线程

抢占式线程调度 每个线程的执行的时间以及切换都由操作系统决定。java线程调度就是抢占式调度

2.4 线程的实现

语言层面的线程的实现方式: 用户线程 1:n 内核线程 1:1 混合 m:n

内核线程实现

内核线程(Kernel-level Thread KLT)就是只有由操作系统内核支持的线程,

用户线程实现

语言层面的线程操作系统是无法感知到的。用户线程的创建,销毁切换和调度都是必须考虑的问题。例如、阻塞,多处理器都需要考虑

(Go)语言的实现方式就是这种方式

混合实现: 内核线程+用户线程

hotspot中,每一个Java线程都是直接映射到内核线程,也就是通过内核线程进行实现的

2.5 协程

用户线程的实现

出现的原因

互联网架构在处理一次对外部业务的请求的响应,往往需要不同服务器的大量服务共同协作来实现的,也就是微服务架构。由于服务数的增加,导致每个服务的处理的时间需要更短的时间

协程的使用 适用于IO密集型,高并发业务场景

内存占用: 线程 >1M/个 协程 >100kb个

2.6 纤程-Java中的协程

Loom项目

推荐使用Quasar java中较为出名的协程库Quasar 运行时需要加 -javaagent: quasar-core-xxx.jar

jdk19

不推荐使用

jdk19(非LTS)引入协程,并称为轻量级虚拟线程但是只是预览版,不推荐使用,如果要使用需要 javac --release 19 --enable-preview XXX.java编译 并使用java --enable-preview XXX运行改程序

2.7 守护线程

当jvm中运行的都是守护线程时,jvm就会退出 守护的是资源(内存)调度

useThread.setDeamon(true);

3 多线程

3.1 线程间的通信以及协调

java中的管道的输入输出:

PipedInputStream,PipedReader

join()方法,可以控制线程顺序

3.2 synchronized内置锁

对象锁和类锁

锁在静态方法是使用的类锁,加在xx.class这个对象上的锁

synchronized需要锁住同一个对象,不然锁是无用的

3.3 volatile轻量级同步机制

线程a可以感知到线程b对同一对象中volatile变量的修改

不能保证线程安全,适用于一个线程写,多个线程读

3.4 等待/通知机制

wait()/notify()

notifyAll()通知所有线程,推荐使用

notify() 随机唤醒线程

等待和通知的标准范式

wait方法会去释放锁

//等待方:
   lock(obj){
      while(条件不满足){0
          obj.wait();
      }
      dothings();
   }
   
//通知方:
    lock(obj){
      dothings();
      //改变条件
      obj.notify();
   }

方法和锁

yield(),sleep()都不会释放锁;wait()会释放持有锁,而且当前被唤醒后,需要重新去获取锁;notify不会对锁有影响,notify一般在同步块代码最后一行

为什么wait和notify需要在同步块里使用(不然java会抛异常)?


//生产者
count + 1;
notify();

//消费者
while(count < 0)
wait();
count--;

生产者和消费者都是2个步骤,消费者准备休眠时,生产者通知,将会被消费者给丢弃掉,然后就无法被唤醒;