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

进程和线程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含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();
    }