目录

Java并发编程概览

/images/current/concurrentOverview.png
Java并发编程概览

主要知识

  • 多线程基础核心 → Synchronized实现原理 → Volatile实现原理 → JMM和指令重排
  • JUC → 原子类与CAS实现原理 → 锁与AQS实现原理→ 并发工具类→ 并发容器→ 阻塞队列→ 线程池

多线程相关概念

线程和进程

  • 进程:是指内存中运行的一个应用程序,每个进程都有自己独立的内存空间;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过 程。
  • 线程:是进程中的一个执行单元,负责当前进程中任务的执行。一个进程在其执行过程中,会产生很多个线程。

进程与线程区别

  • 进程:有独立内存空间,每个进程中的数据空间都是独立的。
  • 线程:多线程之间堆空间与方法区是共享的,但每个线程的栈空间、程序计数器是独立的,线程消耗的资源比进程小的多。

并发与并行

  • 并发(Concurrent):同一时间段,多个任务都在执行 ,单位时间内不⼀定同时执行。
  • 并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行。并行上限取决于CPU核数(CPU时间片内50ms)

注意:并发是一种能力,而并行是一种手段。当我们的系统拥有了并发的能力后,代码如果跑在多核CPU上就可以并行运行。所以咱们会说高并发处理,而不会说高并行处理。并行处理是基于硬件CPU的是固定的,而并发处理的能力是可以通过设计编码进行提高的。

线程上下文切换

一个CPU内核,同一时刻只能被一个线程使用。为了提升CPU利用率,CPU采用了时间片算法将CPU时间片轮流分配给多个线程,每个线程分配了一个时间片(几十毫秒/线程),线程在时间片内,使用CPU执行任务。当时间片用完后,线程会被挂起,然后把 CPU 让给其它线程。

  • CPU切换线程,会把当前线程的执行位置记录下来,用于下次执行时找到准确位置。
  • 线程执行位置的记录与加载过程就叫做上下文切换
  • 线程执行位置记录在程序计数器

上下文切换过程

  1. 挂起当前任务任务,将这个任务在 CPU 中的状态(上下文)存储于内存中的某处。
  2. 恢复一个任务,在内存中检索下一个任务的上下文并将在 CPU 的寄存器中恢复。
  3. 跳转到程序计数器所指定的位置(即跳转到任务被中断时的代码行)。

线程上下文切换会有什么问题呢?

过多的线程并行执行会导致CPU资源的争抢,产生频繁的上下文切换,常常表现为高并发执行时,RT延长。因此,合理控制上下文切换次数,可以提高多线程应用的运行效率。(也就是说线程并不是越多越好,要合理的控制线程的数量。)

  • 直接消耗:指的是CPU寄存器需要保存和加载,系统调度器的代码需要执行
  • 间接消耗:指的是多核的cache之间得共享数据,间接消耗对于程序的影响要看线程工作区操作数据的大小

线程安全问题

什么是线程安全问题

  • 多个线程同时执行,可能会运行同一行代码,如果程序每次运行结果与单线程执行结果一致,且变量的预期值也一样,就是线程安全的,反之则是线程不安全。

引发线程安全问题的根本原因:多个线程共享变量

  • 如果多个线程对共享变量只有读操作,无写操作,那么此操作是线程安全的
  • 如果多个线程同时执行共享变量的写和读操作,则操作不是线程安全的

解决线程安全问题,Java给出了各种办法

  • 同步机制Synchronized
  • Volatile关键字:内存屏障
  • 原子类:CAS
  • 锁:AQS
  • 并发容器

线程并发三大特性

原子性

  • 分时复用引起:操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异导致。
  • 原子性:一个系列指令代码,要么全执行,要么都不执行,执行过程不能被打断
1
2
3
4
    x = 10;        //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
    y = x;         //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
    x++;           //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
    x = x + 1;     //语句4: 同语句3

可见性

  • 为什么会出现不可见问题呢?因为Java内存模型(JMM), CPU缓存引起:CPU增加了缓存,以均衡与内存的速度差异导致。
  • 当多个线程访问同一个变量时,一个线程修改了共享变量的值,其他线程能够立即看到

有序性

  • 重排序引起:由于编译程序指令重排序优化指令执行次序,使得缓存能够得到更加合理地利用导致。
  • 有序性:程序代码按照先后顺序执行

要想多线程程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确

线程的一生

常用属性

  • 线程名称
  • 线程ID:ThreadID = tid
  • 线程优先级:Priority

常用方法

  • 线程让步:yield()
  • 让线程休眠的方法:sleep()
  • 等待线程执行终止的方法: join()
  • 线程中断interrupt()
  • 等待与通知系列函数wait()、notify()、notifyAll()

线程从出生到死亡会出现六种状态

  • ①New(新建)、②Runnable(可运行) 、③Terminated(终止)
  • ④Blocked(锁阻塞)、⑤Waiting(无限等待)、⑥Timed_Waiting(超时等待)

/images/current/JCP-threadLeft.png
线程的一生

wait与sleep()的区别

  • 主要区别:sleep()方法没有释放锁,wait()方法释放了锁
  • 两者都可以暂停线程执行:wait()常用于线程间交互/通信,sleep()用于暂停线程执行
  • wait()方法被调用后,需要别的线程调用同一个对象的notify和notifyAll。超时苏醒使用wait(long)方法
  • sleep()方法执行完成后,线程会自动苏醒。

多线程源码剖析

Java线程是通过start()方法启动,启动后会执行run()方法Thread究竟是如何执行run()方法呢?

/images/current/JCP-souceCodeFlow.png
多线程源码剖析

  1. 线程类被JVM加载时会绑定native方法与对应的C++方法
  2. Java线程调用start方法:start方法 ==> native state0方法 ==> JVM_StartThread ==> 创建JavaThread::JavaThread线程
  3. 创建OS线程,并指定OS线程的运行入口:创建JavaThread::JavaThread线程 ==> 创建OS线程os::create_thread ==> 指定OS线程执行入口Java线程的run方法
  4. 启动OS线程,运行时会调用指定的运行入口run()方法。至此,实现一个的线程运行
  5. 创建线程的时候使用的是互斥锁MutexLocker操作系统(互斥量),所以说创建线程性能很差