一、多线程基本概念及基本函数介绍

进程和线程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

线程和进程两个名词不过是对应的CPU时间段的描述
进程就是包换上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文
CPU在执行的时候没有进行上下文切换的。—网上这句化有问题

  1. 什么是 CPU 上下文?

    CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
    程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。

  2. 什么是 CPU 上下文切换?

    就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
    而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
    根据任务的不同,可以分为以下三种类型 - 进程上下文切换 - 线程上下文切换 - 中断上下文切换

系统调用

从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。

在这个过程中就发生了 CPU 上下文切换,整个过程是这样的:

  1. 保存 CPU 寄存器里原来用户态的指令位
  2. 为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
  3. 跳转到内核态运行内核任务。
  4. 当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。

所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态-内核态-用户态)
不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。系统调用过程中一直是同一个进程在运行

进程上下文切换

进程是由内核来管理和调度的,进程的切换只能发生在内核态。
所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

线程上下文切换

线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。

所以,对于线程和进程,我们可以这么理解: - 当进程只有一个线程时,可以认为进程就等于线程。 - 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。当然,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
在java中,线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作这是很耗时的。

线程安全

所谓多线程安全问题,简单点说就是多个线程操作一个共享变量导致数据不一致的问题。为什么会产生这个问题?这就要说到java的内存模型。

  1. 内存可见性问题
    java内存模型规定,将所有变量都放到主内存中也就是堆,当线程使用变量时,它会把主内存中的变量复制到自己的工作内存(一般为cpu的一级缓存)。
  2. 原子性操作问题
    在一次操作中要么都执行,要么都不执行。
    i++问题
  3. java指令重排序
    java内存模型允许比阿尼器和处理器对指令重排序以提高运行性能,在单线程下没问题,但在多线程下会存在问题.
  4. 伪共享问题
    为了解决系统中主内存和cpu缓存中的速度差问题,会在CPU和主内存之间体检一级或多级缓存。而Cache内部时按行存储的,这就会产生一个问题:当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,其他只能等待。所以相比将每个变量放到一个缓存行里,性能会下降,这就是伪共享
    可见性问题

如上图,线程1 和线程2第一次读的时候会命中主内存,但在其他时候会命中线程中的变量副本v1或者v2,一旦发生修改,则可能会产生数据不一致问题。

线程状态

线程状态转移图
线程共包括以下5种状态:

  1. 初始状态,也就是刚创建new,但并没有启动
  2. 就绪状态,执行了start()方法之后,注意,这个时候并没有立即启动,它会在一个合适的时间拿到CPU的执行权,才开始执行
  3. 运行状态,线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态,图上说的阻塞状态是指已经获取到锁但没有继续运行
  5. run()方法结束

    基本函数介绍

    主要介绍一下函数:
  6. sleep()、yield()是Thread的静态方法 ,
  7. wait()、notify()、notifyAll() 是Object的方法
  8. join()
方法名 出处 是否阻塞 是否释放锁 作用
wait() Object 当前线程进入等待队列
notify() Object 不涉及 唤醒等待队列中的某一个线程,转为锁池
notifyAll() Object 不涉及 唤醒对象等待队列中的所有线程,转为锁池
sleep() Thread静态方法 休眠,让出cpu
yield() Thread静态方法 让出cpu时间片,重新进入锁池
join() Thread普通方法 等待其他线程执行完毕
interrupt() Thread普通方法 不涉及 不涉及 设置中断标志位true,并不实际中断,如果线程正调用wait()、join()、sleep()方法,那么会跑出InterruptException
isInterrupted() Thread普通方法 不涉及 不涉及 返回中断标志
interrupted() Thread静态方法 不涉及 不涉及 检测中断标志,但如果为true,还会清除中断标志,注意此方法是获取当前线程而不是调用对象的。

举例: wait()方法

  • 要点一:调用wait()方法之前,必须获取该对象的监视器锁
    如下会报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Thread t =new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("testWait begin!");
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    t.start();
    //java.lang.IllegalMonitorStateException
    // 此时调用wait()的对象是this,但我们并没有获取this的对象锁

    获取对象监视器锁

    1
    2
    3
    4
    // 代码块
    synchronized(this|object){

    }
    上述例子加上对象锁,再wait()则不会报错 输出 testWait begin!
    1
    2
    3
    4
    // 同步方法
    synchronized void add (){
    //...
    }
  • 要点二:wait() 会被阻塞,直到以下事件发生:

    • 其他线程调用了该线程的interrupt()
      1
      2
      3
      t.start();
      Thread.sleep(200);
      t.interrupt();
    • 其他线程调用了该共享对象的notify()或者notifyAll()方法

    注意: 同wait()方法一样,notify和notifyAll调用的前提条件也是获取对象锁。
    下面直接调用会报错:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Runnable runnable=new Runnable() {
    @Override
    public void run() {
    System.out.println("testWait begin!");
    synchronized (this){
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    System.out.println("testWait end!");
    }
    };
    Thread t =new Thread(runnable);
    t.start();
    Thread.sleep(200);
    runnable.notify();
    //Exception in thread "main" java.lang.IllegalMonitorStateException
    需要改成:
    1
    2
    3
    4
    5
    t.start();
    Thread.sleep(200);
    synchronized (runnable){
    runnable.notify();
    }