Skip to content

多线程 ​

关键字 ​

volatile ​

保证内存可见性 和 防止指令重排

内存屏障 2 个作用: 禁止重排 和 确保缓存与主存一致

  • Load 屏障 加载主存
  • Store 屏障 回刷主存
sh
LL | volatile读 | LS
SS | volatile写 | SL

JMM 内存模型 ​

线程可以将变量保存在本地内存(如 CPU 的寄存器), 而不是主存(共享内存). 产生不一致问题 volatile 防止 JVM 指令重排&&保证变量的可见性

并发 3 特征: 原子性 可见性 有序性 ​

原子性: 多个指令在监视器锁下原子执行

总线 ​

  • 总线嗅探: 每个处理器通过嗅探总线上传播的数据检查自己的缓存是否过期, 如果处理器发现自己缓存行对应的内存地址被修改, 会将处理器的缓存行设置为无效状态,并从主内存重新获取数据.
  • 总线风暴: 由于 volatile 的 mesi 缓存一致性协议需要不断的从主内存嗅探和 cas 不断循环无效交互导致总线带宽达到峰值 解决办法:部分 volatile 和 cas 使用 synchronized

synchronized ​

解决多个线程之间访问资源的同步性, 保证它所修饰的方法或者代码块在任意时刻只能有一个线程执行.

1.6 之前是重量级锁, 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态转换需要相对比较长的时间

三种使用方式:

  1. 修饰方法: 1.1 实例方法: 相当于对当前对象加锁, 需要获得对象锁 1.2 静态方法: 相当于给当前类对象加锁, 需要获得类锁 同步方法使用 ACC_SYNCHRONIZED 标识, JVM 检测到之后进行同步调用.

  2. 修饰代码块: 指定加锁对象, 需要获得相对应的对象锁或者类锁

    通过查看字节码信息 javap -c -s -v -l SynchronizedDemo.class

    可以看出同步代码块使用 monitorenter(指向开始) 和 monitorexit(指向结束) 指令, 执行 enter 时尝试持有对象监视器 monitor

    (HotSpot 中每个对象都内置一个 ObjectMonitor 对象//HotSpot 中,Monitor 是基于 C++ 实现的)

    执行 enter 时尝试获取对象锁, 锁计数器为 0 表示可获取, 获取后锁计数器+1
    执行 exit 后将锁计数器设为 0, 表示锁已释放.

本质都是对对象监视器 monitor 的获取.

synchronized 锁升级 ​

无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态 锁升级过程:无锁状态,持有偏向锁的线程二次进入也不需要重新获取锁,等出现锁竞争的时候会变成轻量级锁,轻量级锁通过CAS 自旋处理锁竞争,自旋次数到达一定程度以后变成重量级锁,使用操作系统的监视器锁 1.6 后 synchronized 优化 https://www.cnblogs.com/wuqinglong/p/9945618.html

volatile 和 synchronized 的区别 ​

修饰对象不同, 修饰变量 和 修饰方法&代码块

解决变量在多个线程之间的可见性 解决多个线程之间访问共享资源的同步性

线程 ​

线程状态 ​

状态说明
NEW线程构建后未调用 start()
RUNNABLE就绪&运行 统称 runnable
BLOCKED阻塞于锁
WAITING等待,等待其他线程通知或中断
TIME_WAITING超时等待
TERMINATED终止,线程执行完毕

状态转移 ​

状态都是跟 Runnable 做转移 Object::wait Thread::join 主线程等待 thread1 join LockSupport.park()

wait() sleep() ​

  • this.wait() 释放对象锁,并进入等待
  • Thread.sleep() 不释放锁, 进入等待 wait() notify()只有 syncronized 内部可调用, 只能由 monitor 锁当前持有的对象调用, 否则 IllegalMonitorStateException

线程中断 ​

interrupt 优雅地中断线程是一种艺术。类似一种标志位,让线程自己中断。

  1. interrupt 中断操作时,非自身打断需要先检测是否有中断权限,这由 jvm 的安全机制配置;
  2. 如果线程处于 sleep wait join 等状态,那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常;
  3. 如果线程处于 I/O 阻塞状态,将会抛出 ClosedByInterruptException(IOException 的子类)异常;
  4. 如果线程在 Selector 上被阻塞,select 方法将立即返回;
  5. 如果非以上情况,将直接标记 interrupt 状态;

Future::cancel ExecutorService::shutdown

死锁 ​

一组线程被阻塞了,等待一个永远不会为真的条件。每个线程都在等待其他线程执行一个不可能执行的操作。 如何避免:给定所有互斥操作的一个全序,如果每个线程都以一种顺序获得互斥锁并且以相反的顺序释放,就不会死锁。(CSAPP)

产生死锁必须具备以下四个条件:

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

线程池 ​

  • 构成: 核心线程数 最大线程数 阻塞队列 饱和策略
  • 构造函数
java
  public ThreadPoolExecutor(int corePoolSize, //核心线程数
                            int maximumPoolSize, //最大线程数
                            long keepAliveTime, //存活时间
                            TimeUnit unit, //时间单位
                            BlockingQueue<Runnable> workQueue, //阻塞队列
                            ThreadFactory threadFactory, //工厂
                            RejectedExecutionHandler handler //拒绝策略
                            );
  • 运行机制: 核心线程满-> 阻塞队列满-> 线程池满-> 拒绝策略

    另外, 如果核心线程数没满, 新任务会直接创建新的核心线程, 而不是复用核心线程

  • 阻塞队列

    • 无界队列
      • DelayQueue 元素有有效期
      • LinkedBlockingQueue
      • LinkedTransferQueue 对于每个生产者是 FIFO
      • PriorityBlockingQueue 优先队列 指定初始容量也会扩容
    • 有界队列
      • ArrayBlockingQueue
      • LinkedBlockingQueue
    • 同步移交(相当于没有队列) 条件是有线程在等待接收队列中的元素; 如果没有线程等待接收, 那么会创建新的线程处理; 然而又超过了最大的线程数限制那会根据饱和策略丢弃;
      • SynchronousQueue
  • 拒绝策略:

    • CallerRunsPolicy: 交由调用方执行
    • AbortPolicy: 抛出 RejectedExecutionException 异常
    • DiscardPolicy: 丢弃
    • DiscardOldestPolicy: 丢弃最早未处理的任务
  • 线程超时会被标记为可回收, 超过核心线程数, 被标记的会被中止.

    注意这里的核心线程池里的线程不会被标记, 可以通过设置核心线程可超时处理

  • 线程数设置:
    计算密集型的 CPU 核心数+1;

    +1 为了防止缺页中断等造成任务暂停

    线程数=目标利用率∗CPU核心数∗(1+等待时间计算时间)

线程池运行状态 5 种 ​

状态描述
running可接任务,也可处理队列
shutdown不接任务,仍可处理队列
stop中断正在处理的线程
tidying所有任务终止,有效线程数 0
terminated结束

shutdown(): status -> shutdown shutdownNow(): shutdown 并且返回等待队列中的任务列表, 但是对执行中的任务不能保证立刻停止, 例如使用 Thread.interrupt 但是无响应 awaitTermination(): shutdown() 之后判断是否结束

isTerminated() shutdown 后 任务完成返回 true; 必须先调用 shutdown

Runnable Callable 区别 ​

需要返回结果或抛出异常,用 callable.
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。

适配器模式 Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object resule)

execute()方法和 submit()方法的区别 ​

Executor::execute 方法用于提交不需要返回值的任务,所以无法判断任务是否执行成功 ExecutorService::submit 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功

与上面的 Callable 配合使用

ThreadLocal ​

copy value into every thread 设计的初衷是跨线程传递数据; 常用方式:通过这玩意儿透传数据,避免线程间对参数的修改相互影响。

java
class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

class ThreadLocal {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            // Entry本身持有ThreadLocal对象(弱Weak引用)和Object对象(强引用)
            Object value;
        }
    }
}

一个 Thread 独享一个 ThreadLocal.ThreadLocalMap, 一个 map 有多个 entry, Entry 里面的"key"属性是 threadLocal 的弱引用, Entry 的"value"是 Object. ThreadLocal 只是作为一个 key, 它只被 Entry 持有; 操作都以 key 为入口

ThreadLocal 是个泛型类, 虽然它本身作为 key

内存泄漏和线程复用下脏数据情况, ThreadLocal 用完后要调用 remove()

  • 内存泄漏: ThreadLocal 对象(key)通常是静态的,即便是弱引用也不会在下一次 YGC 时回收,那就无法使 Value 跟着回收

    当 ThreadLocal 被回收后, value 需要在 get/set 方法中, 遍历 slot, key==null 的置为 null

  • 脏数据:线程复用情况下,没有调用 remove(),导致 Thread 共享到脏数据

源码 ​

map 结构: 只有数组, 没有链表 扩容: 2 倍 冲突: 线性向后查找空位 清理: 探测式 expungeStaleEntry 和启发式 cleanSomeSlots

InheritableThreadLocal ​

java
public class InheritableThreadLocal<T> extends ThreadLocal<T>

子线程创建的时候就会拷贝一份 parent 的 threadLocals, 但是线程池场景不合适

TransmittableThreadLocal 阿里巴巴 ​

ThreadLocal 应用 ​

org.slf4j.MDC, UUID 全链路追踪


ReentrantLock ​

ReentrantLock 原理 ​

state 资源状态计数。state 初始化为 0,表示未锁定状态,A 线程 lock()时,会调用 tryAcquire() 独占锁并将 state+1。 其他线程 tryAcquire() 就会失败,直到 A 线程 unlock() 到 state=0 为止。 在释放锁之前 A 线程可以重复获取这个锁,state 状态会累加,这就是可重入。

java
abstract static class Sync extends AbstractQueuedSynchronizer {
  abstract boolean initialTryLock();// 有公平和非公平2个实现
}

synchronized 和 ReentrantLock 的区别 ​

二者都是可重入锁: 自己可以再次获得自己内部锁, 每次锁计数器会自增 1

如果不是可重入的, 重入的时候会死锁

synchronized 依赖jvm, ReentrantLock 依赖API ReentrantLock 需要手动加锁、释放;

通过循环调用 CAS 操作实现加锁。

ReentrantLock 高级功能: 中断等待 可公平 可以选择性唤醒

  1. 等待时可中断: 可以中断等待锁的线程. lock.lockInterruptibly()

    ReentrantLock.lockInterruptibly() throws InterruptedException

    如果锁被其他线程持有, 线程休眠, 直到:

    • 锁可获取
    • 当前线程被中断

    如果当前线程进入这个方法时是中断状态或者获取锁的时候中断, 抛出异常; 相比于 lock(), 对中断的处理更优先

  2. 可实现公平锁: 可指定是公平锁还是非公平锁(通过构造方法)

  3. 可实现选择性通知: 在一个 Lock 对象中可以创建多个 Condition 实例, 线程可以注册到指定的 Condition 中, 可以有选择性地进行线程通知.


CompletableFuture ​

功能强大的 Future, 异步计算, 合并结果, 并行运行统一结束等等

java
class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}

AQS ​

java
java.util.concurrent.locks.AbstractQueuedSynchronizer;

是一个构建锁和同步器的框架,能构造出大量同步器 (ReentrantLock 就是用了这个

内部维护了 state 代表共享资源,一个 FIFO 双向等待队列封装请求资源的线程

java
private volatile int state;

AQS 原理 ​

如果被请求的资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源锁定。 如果被请求的资源被占用,那么需要一套阻塞、等待、以及唤醒时锁分配机制。AQS 通过 CLH 队列锁实现,将暂时获取不到锁的线程加入队列。

CLH(Craig、Landin and Hagersten)队列是单向链表, AQS 中的队列是 CLH 变体的虚拟双向队列(FIFO), AQS 是通过将每条请求资源的线程封装成一个节点来实现锁的分配。

AQS 共享资源的方式 ​

  • 独占 Exclusive:只有一个线程能执行,比如 ReetrantLock,又可分为公平锁和非公平锁
    • 公平锁:按照线程在队列中的顺序,先到者拿到锁
    • 非公平锁:无视队列顺序,两次 CAS 争抢锁
  • 共享 Share:多个线程可同时执行 如 CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock

AQS 公平锁非公平锁 ​

非公平锁在调用 lock 后,首先就会 CAS(compareAndSetState) 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了; 公平锁不抢 非公平锁在第 1 次抢锁失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁 但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

AQS 等待队列 ​

怎么实现的 都有哪几个状态。各个状态什么含义 有什么用 通过内部类 Node 封装线程,并且维护 pre、next 和 waitStatus 信息实现双向队列

java8: waitStatus 状态:

  1. CANCELLED=1 取消状态,表示线程获取锁的请求已经取消了,如节点获取锁超时。
  2. SIGNAL=-1 表示线程已经准备好了,等到资源释放
  3. CONDITION=-2 表示节点在等待队列中,节点线程等待唤醒
  4. PROPAGATE=-3 当前线程处于共享情况下才会使用
  5. 0 初始化

java17: status 状态:

  • WAITING:1
  • CANCELLED:0x80000000
  • COND:2

AQS 条件队列 ​

是啥 有什么用 单向链表,阻塞队列的暂存

AQS 响应中断 ? ​

本质是阻塞的线程能够唤醒并执行完毕。 1 线程能从阻塞中唤醒,AQS 使用 LockSupport.park(this)阻塞线程。而此方法是支持中断。 2 线程能正常执行完毕,只有获取同步状态才能正常退出自旋循环。需要退出就需要在中断时抛出异常。

AQS 实现类 ​

Semaphore ​

允许 x 个线程同时访问, 相当于颁发 x 个许可证

CyclicBarrier ​

基于 ReentrantLock 和 Condition

跟倒计时器类似,它是让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时被拦截的线程才能继续工作

CountDownLatch ​

协调多个线程的同步,可以让某线程等待倒计时结束再开始执行 典型用法:

  • 等待多个线程结束
  • 让多个线程同时开始

CountDownLatch ​

任务分为 N 个子线程执行,state 初始化为 N,子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS,减一。等到 state=0,会 unpark()主线程,主线程会从 await()返回,继续后续操作。

countDownLatch 使用 ​

预估多个任务费用的时候, 有 10 条线程并行执行, 每条线程执行完任务后 countDown, 最后 await 等待所有线程执行完毕后统一返回结果 可以用 CompletableFuture 改进


原子类 ​

Atomic 类 ​

4 类: 基本类型, Array, Reference, Updater

基本类型 ​

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型 ​

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型 ​

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
    java
    final T reference;
    final int stamp;
  • AtomicMarkableReference:原子更新带有标记位的引用类型
    java
    final T reference;
    final boolean mark;

对象的属性修改类型 ​

更新对象的属性必须 public volatile 修饰

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
    java
    public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
                                                                    Class<W> vclass,
                                                                    String fieldName){}

AtomicInteger 原理 ​

利用 CAS, volatile 和 native 方法来保证原子操作 拿期望的值和原本的一个值作比较,如果相同则更新成新的值。 UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法用来拿到“原来的值”的内存地址,返回值是(long) valueOffset。 另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。


并发容器 ​

  • ConcurrentHashMap
  • CopyOnWriteArrayList 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
  • ConcurrentLinkedQueue 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  • ConcurrentSkipListMap 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找

ConcurrentHashMap ​

1.7 底层是分片数组
有 Segment 分段锁, 继承于 ReentrantLock, 每次只给一段加锁保证并发度, 显然冲突的时候使用 CAS

1.8 底层跟 HashMap 一致, 使用 synchronized, 放弃使用 ReentrantLock; CAS 操作在 桶位创建第一个元素的时候 casTabAt put 方法中有一个循环, 建表/放置头节点之后会进入下一次循环; 会用 sychronized 锁定桶位

其实是减小了锁的粒度, 在哈希冲突不多的情况下, 本也不需要加锁操作, HashMap 的线程不安全主要还是在哈希冲突的时候

CopyOnWriteArrayList ​

复制与替换, get() 完全不加锁 set() 加锁 copy array

非阻塞队列 ConcurrentLinkedQueue ​

典型代表 ConcurrentLinkedQueue CAS 实现并发读写, 性能不错

ConcurrentSkipListMap ​

跳表 实现有序 map 平衡树的锁粒度大, 跳表的锁粒度小

CAS ​

轻量级 不加互斥锁

  • 并发量大时对 CPU 消耗大, 有可能产生忙循环
  • ABA 问题, 可以通过添加版本号解决

乐观锁 悲观锁 ​

乐观默认别人不会修改,更新的时候会判断是否被其他操作修改过,如果修改过会重试。版本号、CAS 等 悲观锁默认别人会修改,会先对资源进行加锁处理