基础概念
什么是进程、线程?并发问题的产生?
- 进程是程序在内存的执行体(包含源代码_指令、相关数据_操作数、文件)
- 线程是进程执行过程中的某个独立的功能模块,是最小的执行单元
- 同一个进程的线程、不同进程的线程间在执行过程中涉及到对于计算机资源的占有冲突,并发编程需要解决的问题是保证在同一时间间隔内运行的若干线程能够正常执行
- 线程间同步/互斥
- 线程间通信
并发与并行
- 并发是指一个时间间隔内进行多个事件
- 并行是指一个时间间隔内
同时
进行多个事件 - 并发的应用场景更多
与IO相关
,使用多线程
实现并发 - 并行更多地是利用
多CPU、GPU
的性能
同步与异步
-
同步
调用一个功能时,在得到返回结果之前无法进行后续操作
。java中的所有方法调用都是同步的 -
异步
调用一个功能在得到返回结果之前,可以先进行后续操作
。调用完成后,一般通过通知、状态、回调
来通知调用者。
锁的分类
-
同步锁/异步锁
异步锁描述多个线程间在执行时间上的互斥关系
同步锁进一步描述多个线程间在同步资源(代码块/方法)竞争中的互斥关系 -
公平锁/非公平锁
描述多个线程在锁的竞争中遵守的原则,公平按照先来先得原则,非公平按照重要性/优先级获取锁
java.Concurrent包中的锁一般默认为非公平方式。 -
可重入锁/不可重入锁
描述进程是否每次获取同步锁都需要许可,隐式锁(synchronize)、显示锁(Lock)默认为可重入,可重入方式下线程可以递归调用同步资源(代码块/方法)。 -
乐观锁/悲观锁(共享锁/互斥锁)
乐观表明线程获取资源后不对资源加锁,默认该过程中无其它线程进行写入操作;悲观表明获取资源必须加锁,默认该过程中一定会有其它线程尝试写入修改数据。
线程的创建方式
Tread类
继承java.lang.Thread类,重写其run()方法
Runnable接口
实现java.lang.Runnable接口run()方法,将其作为构造参数创建Thread实例
FutureTask类、Callable接口
实现java.concurrent.Callable接口call()方法,将其实例作为构造参数创建java.concurrent.FutureTask实例,继续使用FutureTask实例作为构造参数创建Tread实例。该方式可实现有返回的线程执行体。
线程池(Excutors)
创建线程池(java.concurrent.Executors类提供的ExecutorService接口下)
线程的生命周期

新建(new)
new Tread()
,只建立了对象,在操作系统中未创建实际线程
就绪(start)
Thread.start()
,操作系统创建了线程,但还未开始执行
执行(run())
系统为线程分配时间片,开始执行run()
方法。可以通过yield()方法
返回就绪状态。
阻塞(sleep、wait)
线程执行过程中通过sleep()
、wait()
、等待获取锁进入阻塞状态。可以通过其他线程的notify()
、notifyAll()
唤醒阻塞中的线程进入就绪态。
终止(任务执行完毕、JVM终止)
线程正常执行完毕终止;线程执行/阻塞中异常终止;JVM终止
并发线程安全的三大特性
(变量)可见性
多个线程对变量进行操作,其中某一线程对变量的修改要能立刻被其它线程看见
(操作)原子性
某个线程对于变量操作的整个过程不能被中断,要么顺利执行、要么完全不执行
-
变量操作的原子性:
java中的基础类型变量(byte, short, int, long, float, double, char, boolean)、引用类型变量中除了long、double类型外都能保证操作(赋值)的原子性。而long、double可以通过CAS操作、synchronize关键字来保证原子性。 -
代码块/方法的原子性
只能使用锁机制来保证。
(执行)顺序性
线程执行过程要完全按照代码中的指令顺序,不能对指令进行重排序
保证线程安全的手段
Volatile关键字("变量"可见性、代码顺序性)
定义变量,保证其可见性;禁止指令重排,保证顺序性
- 保证可见性: 线程每次都从进程公共内存中读取变量值,操作执行结束后立刻将最新的变量值刷新到公共内存
- 保证顺序性(禁止指令重排)
Atomic变量("变量"操作原子性)
-
提供基本变量类型(long, double)的原子操作
- 自增/自减
- 加一个数/减一个数
-
CAS(Compare and Swap)操作
锁机制("变量"可见性、"变量/过程"操作原子性、代码顺序性)
隐式锁与显示锁的比较
类别 | synchronize(隐式锁/JVM内置锁) | Lock(显式锁/JDK锁接口) |
---|---|---|
层次 | java关键字,JVM层面 | concurrent包中的抽象类 |
获取锁 | 执行开始自动获取锁,未能获取锁的线程强制等待 | 线程可以多次尝试获取锁,不需要一直等待 |
释放锁 | 执行完毕自动释放; 发生异常JVM让线程释放锁 | 必须要在线程执行体中主动释放锁,如finally语句块中,否则会造成死锁 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 独占 非公平 可重入 | 独占/共享锁 公平/非公平(默认) 可重入(默认) |
性能 | 少量同步 | 大量同步 |
乐观/悲观 或 共享/独占 带来的非阻塞/阻塞操作
-
阻塞操作
悲观/独占方式使得同一时刻只能有一个线程获取锁,这导致其它尝试获取所得线程全部阻塞。在这个过程中大量线程被阻塞带来了庞大的上下文切换系统开销,降低了性能。 -
非阻塞操作
乐观/共享方式下可以有多个线程获取锁(共享锁),或者说有多个线程同时对变量进行操作,此时不会发生阻塞因此也避免了线程上下文切换。 -
synchronize关键字是一种悲观/独占锁,通过阻塞的方式保证操作的原子性
-
CAS(比较-交换操作,自旋锁)是一种乐观锁,能够使用非阻塞的方法保证变量(读、修改、增加、减少)操作的原子性。
synchronize底层实现原理及锁的升级(隐式锁/内置锁)
三种加锁类型
-
对象锁
- 代码块(对当前括号内指定对象加锁)
- 实例方法(对当前实例加锁,this)
-
类(类对象Class)锁
- 静态方法(对当前类加锁,static)
对象锁的实现
JVM (HotSpot) 对象内存模型 (JVM虚拟机)

对象锁
存放在对象头的Mark Word中,java中所有对象自带隐藏的监视器Monitor
配合Object.wait()、Object.notify()、Object.notifyAll()方法使用
Object.wait()与Object.notify()原理
对象锁的四种状态
随着竞争激烈程度加深锁的状态会升级,但是不会降级
+ 无锁
线程间竞争,采用公平 or 非公平方式
+ 偏向锁
记忆偏向线程,下一次竞争偏向线程优先
+ 轻量级锁
???
+ 重量级锁
???
对象锁的升级过程: 加锁、撤销、升级
-
无锁->偏向锁
多个线程使用CAS竞争处于无锁状态的对象,成功获取锁的线程ID将被记录到偏向锁记录中 -
偏向锁->轻量级锁
处于偏向锁状态的对象被线程尝试获取时,若线程ID等于偏向的ID则直接获取
反之检查锁状态,若处于无锁状态则撤销偏向锁
让多个线程竞争
若仍处于偏向锁状态则尝试撤销偏向锁
,若偏向线程已不存在则直接撤销,反之若偏向线程想要继续持有则升级到轻量级锁
-
轻量级锁->重量级锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中
,即Displaced Mark Word。
然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁
。
如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,锁就会升级为重量级锁
。
轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回到对象头
,如果成功,表示没有竞争发生。如果失败,表示当前锁存在竞争,锁已经被升级为重量级锁,则会释放锁并唤醒等待的线程
java.concurrent并发包锁原理分析
LockSupport工具类: 阻塞与唤醒线程
作用类似对象锁机制中的wait()、notify()
-
LockSupport.park()
消费许可证,调用线程若没有与LockSupport类相关联的许可证则会被阻塞;反之会直接返回 -
LockSupport.unpark(Thread thread)
发放许可证,线程调用该方法将获得与LockSupport类相关联的许可证; 调用该方法的线程会将作为参数的线程从park()调用导致的阻塞状态唤醒并返回
抽象同步队列(AQS)
显示锁/Lock接口 的底层支持
组成
- 双向链表, 节点为Node内部类
- 状态变量
int state
内部类Node
相关联线程对象thread
- 竞争的资源类型(SHARED、EXCLUSIVE)
- 当前等待状态waitStatus(CANCELLED、SIGNAL、CONDITION、PROPAGATE)
状态变量
对于不同的Lock类具有不同的含义,线程同步的关键在于对state变量的操作
- 独占、共享两种方式
锁类型对应的状态变量含义
锁类型 | 含义 |
---|---|
ReetrantLock | 重入(递归调用)次数 |
ReetrantReadWriteLock | 高16位表示写锁(独占)重入次数 |
对状态变量的两种操作类型 (尝试获取 -> 获取 -> 释放)
独占方式:
-
tryAcquire() -> acquire() -> tryRelease()
-
获取锁/进入临界区的线程修改state值以指示锁已被获取/资源已被占有,失败(tryAcquire()失败)的线程被加入到阻塞队列
共享方式:
-
tryAcquireShared() -> acquireShared() -> tryRelease()
-
失败的线程以自旋CAS的方式不断继续尝试获取,直到成功 or 被打断,处于等待的线程依然会被加入阻塞队列