并发编程前言

并发和并行

并发和并行目标都是最大化CPU的使用率

并行:在同一时刻,多条指令在多个处理器上同时执行,无论是从微观还是宏观的角度来看二者都是一起执行

并发:在同一时刻只能有一条指令执行,但多个进程指令快速的轮换执行,使得在宏观上具有多个进程时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干份,使多个进程快速交替执行

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
  • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

多线程的作用

对于多线程的作用有这三个:同步、互斥、分工

同步:指的是A线程的结果需要依赖B线程的结果。比如说用户访问tomcat开启一个tomcat的线程。而tomcat线程会去访问应用程序线程,应用程序线程又会调用jdbc的线程访问数据。拿到一系列结果后放回给tomcat 再放回给用户。这就是线程之间的同步协作。

互斥:指的是A线程在使用这个资源,其他线程无法使用,必须等到A线程释放后才能访问该资源。比如在数据库中对某个数据加了写锁,在一个线程进行写操作的时候另一个线程无法访问到该数据。

分工:每个线程分配不同的任务,最后结果汇总起来。比如说在计算很大数据时,开启多个线程,每个线程计算一部分,最后把所有线程计算的结果汇总

并发编程出现bug的根本原因

根本原因在于:可见性原子性和有序性问题。这也是并发的三大特性

并发编程三特性

可见性

当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介 的方法来实现可见性的。 如何保证可见性:

  • 通过 volatile 关键字保证可见性。
  • 通过 内存屏障保证可见性。
  • 通过 synchronized 关键字保证可见性。
  • 通过 Lock锁机制保证可见性。
  • 通过 final 关键字保证可见性

    有序性

    即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。 如何保证有序性:
  • 通过 volatile 关键字保证有序性。
  • 通过 内存屏障保证有序性。
  • 通过 synchronized关键字保证有序性。 通过 Lock锁机制保证有序性。

    原子性

    一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。如何保证原子性:
  • 通过 synchronized 关键字保证原子性。
  • 通过 Lock锁机制保证原子性。
  • 通过 CAS保证原子性。