进程 线程 协程

进程和线程

进程与线程的区别

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

  1. 进程是资源分配的最小单位,线程是程序执行的最小单位。
  2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  4. 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

同一进程中的线程

线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。
在一个进程的线程共享堆区,而进程中的线程各自维持自己堆栈。
堆:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

多进程

怎么创建新进程,内核做了啥?

fork, vfork,然后具体do_fork过程,copy on write等
进程 : 代码 + 数据 + 堆栈 + PCB
PCB (进程控制块):pid 进程标识符 + pwd 进程标识 + ppid 父进程进程号

  1. 分配一个 PID 从小到大找一个未被使用的进程号
    0号进程是内核进程,它创建1号进程、还将物理内存搬到磁盘和磁盘搬到物理内存
  2. 分配PCB,拷贝父进程的 PCB的绝大部分数据
  3. 给子进程分配资源
  4. 复制父进程地址空间
  5. 将子进程置成就绪状态,放入就绪队列

在unix中系统调用的是:fork,fork会创建一个与父进程一摸一样的副本
在windows中该系统调用是:cresteprocess,既负责处理进程的创建,也负责把正确的程序装入新进程
无论是unix还是windows,进程只有一个父进程,不同的是:
在unix中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组
在windows中,没有进程层次的概念,所有的进程地位都相同

fork()作用

Linux系统函数fork()可以在父进程中创建一个子进程,这样的话,在一个进程接到来自客户端新的请求时就可以复制出一个子进程让其来处理,父进程只需负责监控请求的到来,然后创建子进程让其去处理,这样就能做到并发处理。fork函数会返回两次结果,因为操作系统会把当前进程的数据复制一遍,然后程序就分两个进程继续运行后面的代码,fork分别在父进程和子进程中返回,在子进程返回的值pid永远是0,在父进程返回的是子进程的进程id。打印的顺序与系统的进程调度有关。

  • 父子进程交替进行
  • 父进程死亡,子进程将变成孤儿进程,由 1号 进程领养
  • 子进程死亡,成为僵尸进程

多进程相关函数

multiprocessing模块就是跨平台版本的多进程模块。创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。
join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
小结:

  • 在Unix/Linux下,可以使用fork()调用实现多进程。
  • 要实现跨平台的多进程,可以使用multiprocessing模块。
  • 进程间通信是通过Queue、Pipes等实现的。

进程间通信方式

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
    消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  3. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  4. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段
  5. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  6. 信号signal: 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

多线程

多线程的实现

  1. 继承Thread类,重写run方法
  2. 创建MyRunnable类实现runnable接口,重写run方法,创建MyRunnable类的对象,创建Thread类的对象,并把MyRunnable类对象作为参数传递
  3. 使用ExecutorService、Callable、Future实现有返回结果的多线程
  4. 通过线程池创建

优劣分析

  • 继承Thread
    优势:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
    劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。
  • 使用runnable或者callable
    优势是:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
    劣势是:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
  • runnable和callable区别
    Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    Call方法可以抛出异常,run方法不可以。
    运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

多线程优点

  • 在一个程序中,有很多的操作是非常耗时的,如数据库读写操作,IO操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程,可以在将耗时任务放在后台继续执行的同时,同时执行其他操作。
  • 可以提高程序的效率。
  • 在一些等待的任务上,如用户输入,文件读取等,多线程就非常有用了。

多线程缺点

  • 使用太多线程,是很耗系统资源,因为线程需要开辟内存。更多线程需要更多内存。
  • 影响系统性能,因为操作系统需要在线程之间来回切换。
  • 需要考虑线程操作对程序的影响,如线程挂起,中止等操作对程序的影响。
  • 线程使用不当会发生很多问题。

什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

  • 线程体中调用了yield方法让出了对cpu的占用权利
  • 线程体中调用了sleep方法使线程进入睡眠状态
  • 线程由于IO操作受到阻塞
  • 另外一个更高优先级线程出现
  • 在支持时间片的系统中,该线程的时间片用完

线程的状态

  1. 新建状态(New):新建一个线程对象。
  2. 就绪/可运行状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获得CPU并执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep的状态超时、join等待线程终止或者超时、以及I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程执行完成或者因异常退出run方法,该线程结束生命周期。

线程的相关问题

ThreadLocal是什么?有什么用?

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

为什么sleep是Thread的方法而不是Object的

  • 在java的内置锁机制中,每个对象都可以成为锁,也就是说每个对象都可以去调用wait,notify方法,而Object类是所有类的一个父类,把这些方法放在Object中,则java中的所有对象都可以去调用这些方法了。
  • 一个线程可以拥有多个对象锁,wait,notify,notifyAll跟对象锁之间是有一个绑定关系的,比如你用对象锁aObject调用的wait()方法,那么你只能通过aObject.notify()或者aObject.notifyAll()来唤醒这个线程,这样jvm很容易就知道应该从哪个对象锁的等待池中去唤醒线程,假如用Thread.wait(),Thread.notify(),Thread.notifyAll()来调用,虚拟机根本就不知道需要操作的对象锁是哪一个。
  • sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定.sleep()可以将一个线程睡眠,参数可以指定一个时间。
  • wait(),是由某个确定的对象来调用的,wait()可以将一个线程挂起,直到超时或者该线程被唤。
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。

线程B怎么知道线程A修改了变量

  • volatile修饰变量
    Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程/进程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。每次读取volatile的变量时,都要从它的内存地址中读取,并没有说每次修改完volatile的变量后,都要立刻将它的值写回内存。volatile只提供了内存可见性,而没有提供原子性。
    可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
    有序性:程序执行的顺序按照代码的先后顺序执行。
  • synchronized修饰修改变量的方法
  • wait/notify
    wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
    wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
    wait(long,int):对于超时时间更细力度的控制,单位为纳秒。
    notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
    notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
  • while轮询

保证线程安全怎么做?

当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
最常见的一种不安全的情况,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来,还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

  1. synchronized关键字
    用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。
  2. Lock
    Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性。从使用上说Lock明显没有synchronized使用起来方便快捷。进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。
    Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁。
    tryLock有一个Boolean的返回值,如果没有拿到锁,直接返回false,停止等待。

怎么等一个线程做完

join :主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)
p.join只能join住start开启的进程,而不能join住run开启的进程 (即task)

线程池

什么是线程池

线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。

为什么要线程池

由于创建和销毁线程都是消耗系统资源的,所以当需要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

四种线程池的创建:

  1. newCachedThreadPool创建一个可缓存线程池
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

线程池的优点?

  1. 重用存在的线程,减少对象创建销毁的开销。
  2. 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  3. 提供定时执行、定期执行、单线程、并发数控制等功能。

线程池的中的线程数量能不能无限增加,为什么?

不能,线程需要一定开销,太多线程会耗尽计算机资源,(而且过多线程也无法发挥多线程的优势,毕竟cpu核就那么多)

java 里任务提交给线程池后,那些任务是存储在哪的?

把任务用 Runable(其实也可以是Callable) 表示,放在一个阻塞队列里。

阻塞队列怎么实现?

创建一个数组或者链表,每次取元素或者放元素就对数组操作。没有元素而要取元素时,阻塞,满了而要放元素时,阻塞。

队列满了,那个线程池的 submit 方法会阻塞在那里?

  1. 如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,可以继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
  2. 如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

协程

线程和协程区别

协程(Coroutines)是一种比线程更加轻量级的存在,一个线程可以拥有多个协程。协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。

  • 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户是无感知的。进程的切换内容包括页全局目录、内核栈、硬件上下文,切换内容保存在内存中。进程切换过程是由“用户态到内核态到用户态”的方式,切换效率低。
  • 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户无感知。线程的切换内容包括内核栈和硬件上下文。线程切换内容保存在内核栈中。线程切换过程是由“用户态到内核态到用户态”, 切换效率中等。
  • 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序所决定的。协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。

协程Coroutine用法

  • 将复杂操作分帧计算。
  • 异步下载。
  • 使用yield return coroutine等待协程,将多个异步逻辑串联。如先进行异步下载,完成下载任务后再接着运算。
  • 创建互斥区。如某个下载函数同一时刻只能有一个协程进入。

yield

协程的完成主要靠yield关键字,带有yield关键字的函数自动变成生成器,根据yield函数的记录标签位置继续执行协程函数的后面部分代码。

  • yield return new WaitForSeconds(3.0f); // 等待3秒,然后继续从此处开始,常用于做定时器
  • yield return null; // 这一帧到此暂停,下一帧再从暂停处继续,常用于循环中
  • yield return new WaitForEndOfFrame(); // 等到这一帧的cameras和GUI渲染结束后再从此处继续,即等到这帧的末尾再往下运行。这行之后的代码还是在当前帧运行,是在下一帧开始前执行,跟return null很相似
  • yield return new WaitForFixedUpdate(); // 在下一次执行FixedUpdate的时候继续执行这段代码,即等一次物理引擎的更新
  • yield return www; // 等待直至异步下载完成
  • yield break; // 直接跳出协程,对某些判定失败必须跳出的时候,比如加载AssetBundle的时候,WWW失败了,后边加载bundle没有必要了,这时候可以yield break跳出。
  • yield return StartCoroutine(methodName); // 等待另一个协程执行完。这是把协程串联起来的关键,常用于让多个协程按顺序逐个运行

https://www.cnblogs.com/zhehan54/p/6130030.html
https://blog.csdn.net/zhaohong_bo/java/article/details/89552188
https://blog.csdn.net/shuilan0066/article/details/7683315
https://www.cnblogs.com/YeLing0119/p/9754090.html
https://blog.csdn.net/boteman123/article/details/81000631
https://blog.csdn.net/beidaol/article/details/89135277
https://blog.csdn.net/tanmomo/article/details/99671622
https://www.jianshu.com/p/bb07a4d047eb
https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064
https://www.cnblogs.com/linuxAndMcu/p/11064916.html
https://www.cnblogs.com/CharlesGrant/p/8125841.html
https://blog.csdn.net/y277an/java/article/details/98697454
https://www.jianshu.com/p/6dde7f92951e
https://blog.csdn.net/qq_18995513/article/details/51944602?utm_source=itdadao&utm_medium=referral