并发基础

142 阅读7分钟

概念性内容

线程和进程区别?

进程线程
进程是程序的一次运行过程,从创建,运行到销毁,是操作系统运行程序的基本单位,像我们运行一个.exe文件就是启动了一个进程,有的能开启多个进程,比如qq登录多个账号。有的仅开启一个进程,比如360安全卫士一个进程内可以包含1到多个线程,可以认为进程是线程的容器。在Java中,进程作为最小的资源分配单位,而线程是最小的调度单位

二者不同点:

  1. 进程的资源都是独立的,并不共享。
  2. 线程共享进程内的堆和方法区空间(jdk1.8是元空间,直接使用本地物理内存),每个线程有自己独立的程序计数器、虚拟机栈、本地方法栈。 线程之间切换耗费时间也更少,因此线程也被称为轻量级的进程。
  3. 进程通信,实现较为复杂,因为进程之间不共享资源,需要额外的协议来支持。比如同一台PC进程通信使用IPC(Inter-process communication),远程PC进程通信依靠Http协议。而线程通信较简单,因为可以访问共享变量。

所以可以看出进程资源不共享,有利于管理,不会造成相互影响,但是上下文切换成本较大。而线程相反,因为有共享资源,所以可能会造成相互影响,不利于管理,但上下文切换成本比进程小得多。

为什么程序计数器、虚拟机栈、本地方法栈是线程私有?

先贴一张G哥的图

图片.png

程序计数器作用:

  1. 帮助字节码解释器读取下一条字节码。
  2. 在多线程环境下,程序计数器用于被记录当前执行的位置,当线程被切换回来时能知道上次执行到哪了。

所以程序计数器私有是为了保存程序执行到的位置

虚拟机栈作用:Java在方法调用时候,会创建对应的栈帧,保存方法内的局部变量表、操作数栈、常量池引用等信息。方法的调用和返回对应着虚拟机栈的入栈和出栈。所以需要为每一个调用者(线程)提供单独的虚拟机栈不让别的调用者访问到。

本地方法栈:本地方法栈跟虚拟机栈作用差不多,只不过调用的是native本地方法。

在HotSpot虚拟机中,已经将两个虚拟机栈合二为一了,

聊聊堆和方法区?

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

多线程

并行和并发的区别

并行是同一时间执行多个线程的任务,能够同时得到执行。parallel 并发也是同一时间执行多个线程任务,但是不一定得到执行,可能需要排队。concurrent

注意:并行需要在多核心CPU中才能有体现,单核CPU体现不出来,会变成串行执行。java8中的stream流配合多核CPU可以提高性能。

为什么要使用多线程?

  1. 线程更加轻量,切换和调度资源耗费的更少
  2. 当今互联网的并发量大,需要使用到多线程
  3. 单核CPU下,能够提供CPU利用率
  4. 多核CPU下,可以提高任务执行速度(类似steam流)

使用多线程带来的问题

内存泄漏、线程不安全、以及线程死锁(操作系统中是使用银行家算法解决,即哲学家就餐)等问题

线程的生命周期

Java中的生命周期

图片.png

操作系统中的生命周期

图片.png

注:上述图片来自《Java并发编程的艺术》

通过对比,我们发现Java中只有6种状态,而操作系统中有7种状态,少的这一种Ready是被Java合并到了Runnable中。至于为什么合并,原因是因为现在大部分都使用的是分时操作系统,利用时间片轮转来决定线程的执行时间,时间一到就得回到调度队列中等待下一次操作系统的调度。通常这一时间非常短,大概为0.01秒。所以Java认为Ready这一状态并没太大意义,所以合并了。

在Java程序执行时,并不是固定在某一声明周期的,而是会随着代码执行的不同而到不同的声明周期:

  1. 创建了线程还没有start的时候,为New
  2. 开始了start为Runable
  3. 获取不到锁为Blocked
  4. 调用wait或者sleep方法Waitting
  5. 调用wait(long timeout),sleep(long timeout)时为Timed_Waitting
  6. 最后在执行完了run方法中的内容后为Terminated

什么是上下文切换?

线程有自己执行条件,当发生如下情况,会从CPU中退出

  1. CPU时间片到期。
  2. 发生阻塞类型事件,调用了系统中断,比如IO事件。
  3. 调用sleep或者wait方法放弃CPU执行时间。
  4. 线程执行完毕。

上下文切换时会保存当前线程运行的状态,保存现场,以便下次切换回来的时候能够还原现场。而保存和还原都需要耗费一定的资源,比如内存消耗,CPU消耗等等

线程死锁,以及避免死锁

操作系统中死锁四个必要条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件

可以使用银行家算法寻找出安全序列。

sleep和wait方法的区别和相同点

相同:

  1. 都可以让线程暂停

不同:

  1. wait会释放锁,而sleep不会释放锁
  2. wait用于线程间的通信,而sleep用于线程的暂停
  3. wait不会自动苏醒,而sleep可以。(wait(long timeout)可以自动苏醒)。

调用start方法和run方法区别?

我们调用start方法,会创建一个线程并进入就绪状态,放入就绪队列,当分配到了时间片后,就可以自动执行run方法中的内容。而调用run方法并不会有上面的流程,他只会被当成main线程中的一个普通方法调用。

创建线程有几种方式?

  1. Thread类
  2. Runnable接口
  3. Callable接口(具有返回值的任务,可以搭配FutureTask获取返回值)
  4. 线程池创建

什么是守护线程

守护线程也是线程的一种,但是以一种守护模式进行的,即别的线程运行完了,即使他的代码还没有运行完也会立即终止。

图片.png