[TOC]
现代计算机的 CPU 大多是多核心,比如我们使用到的 Android 手机很多已经是 8 核处理器,核心数和线程数多为 1:1 的关系,英特尔处理器的超线程技术提高了线程数,成 1:2 的关系
多线程
Java 天生就是支持多线程的,并且提供了两种方式来实现多线程
- 继承 Thread
- 实现 Runnable 接口
网上还提到有其他创建线程方式,其实最终只有两种,为什么呢?在 Jdk Thread 源码里就说明了
Jdk Thread.class,比如 Callable 的方式实现多线程,实际上就是 Runnable,因为 Callable 的方式是使用 FutureTask ,而 FutureTask 最终实现了Runnable 接口
锁
- 内置锁 - sync
- 显示锁 - Lock
根据锁的分配方式分类
- 公平锁 - 谁先申请谁先使用,类似队列的先进先出
- 非公平锁-竞争使用
Synchornized 原理
ObjectMonitor 对象监视器,使用了 sync 关键字以后,编译器阶段会转换为相应的语句,里面就有监视器
作用于对象,也就是对象实例上
作用范围
分类 | 被锁的对象 | Code | 说明 |
---|---|---|---|
方法 | 实例对象 | public synchronized void fun() | 1. 锁住的是该实例对象 2. 同一个实例对象在不同线程调用会同步 3. 不同实例对象在不同线程调用不会调用 |
方法 | 静态方法 | public static synchronized void fun() | 1. 锁住的是该类的 Class 对象 2. 该类的实例对象在不同的线程调用都是同步 |
代码块 | 实例对象 | synchronized (this) | 1. 锁住的是实例对象 2. 该类的实例对象在不同的线程调用都是同步 3. 该类的不同实例对象在不同线程是不同步的 |
代码块 | 类对象 | synchronized (xx.class) | 1. 锁住的是该类的 Class 对象 2. 该类的实例对象在不同的线程调用都是同步 |
代码块 | 任意对象 | Object obj; synchronized(obj) | 锁住的是 obj 对象实例,具体根据 obj 的类型,如果是实例对象,则根据实例的规则,如果是类对象,则是类对象的规则 |
// 伪代码
// monitorenter
// 记录对应的线程 Id ,其他线程不许操作该对象的该方法
// monitorexit
- 内置锁和显示锁在 jdk 层哪里不同?
内置锁区别
锁升级
- 偏向锁 没有线程争夺资源,当执行同步代码时,会讲会将该对象头中偏向锁指向当前线程,避免 cas
- 轻量级锁
线程状态
- 初始
- 就绪
- 运行中
- 阻塞
- 等待
- 等待超时
- 终止
进入阻塞状态的唯一条件是通过 synchronized() 进入,阻塞是被动进入,等待则是主动进入,比如调用 sleep() 进入等待超时,调用 lock() 进入等待、等待超时状态
为什么会有就绪和运行中这两种状态切换呢?因为线程分配的时间片资源使用完后,就会进入xxx 状态,等待下一次时间片的分配
死锁
- 互斥
- 条件等待
- 不剥夺
打破死锁:使用 trylock()
ThreadLocal
线程本地变量,用来储存当前线程的副本
- 自己怎么实现
- Jdk 是怎么实现的
Thread 内部自己存储了副本,保存在静态内部类 ThreadLocalMap 中的 Entry 数组中
CAS
现代计算机提供了一条指令,简称 CAS
Compare And Swap,取出该值跟期望的旧值做比较,看是否发生变化,如果仍然是旧值,则将该值更新成新值,如果不是旧值则自旋
系统中有相关的 CAS 指令,jdk 基于 CAS 实现很多原子类
原子操作,指不可再分的操作,执行的结果是要么都做了,要么都没做,比如 sync 操作,内部的代码段要么获得锁执行,要么阻塞无法执行,但 sync 并不是 CAS
应用
Java API 中的原子类 AutomaticInteger 等,该类中 increaseAndGet() 方法,内部是由 Unsafe 类调用 native 方法,HotSpot 对 x86 平台的实现,是由汇编指令 lock cmpxchg 指令实现ABA 问题
假设某个值为 A 的变量,中间变成了 B,然后又重新变成了 A,此时执行 CAS 指令时,A 虽然经过了变化,但仍为期待的旧值,就对它更新成新值
怎么解决 ABA 问题呢?
设置一个版本戳,比如 A 发生一次改变,标志位 +1,jdk 中的 AtomicMarkableReference(记录是否发生过改变) 和 AtomicStampedReference (记录了发生改变的次数)
- 开销问题
大量的自旋消耗 cpu
- 只能保证一个变量的原子操作
怎么解决呢?把几个要同时更新值的变量封装成一个对象,此时更新该对象就是一个原子操作了
阻塞队列
解决生产者与消费者模型中两方处理速率不同步的问题
线程池
由于线程的不仅在运行时需要耗时,在创建、销毁线程的过程中也是需要时间的,如果在我们的程序中频繁地创建、销毁线程,会造成很多不必要的消耗,那么就需要使用到容器,使得线程能够复用,它就是线程池
- 线程池的创建
- 核心线程数
- 最大线程数
- 阻塞队列
- 拒绝策略
- 线程存活时间
假设创建线程池的核心线程数 3,最大线程数为15,此时我们在执行三个任务,核心线程刚好跑满,继续提交 1 个任务,不会马上启动新线程,而是放到阻塞队列中,直到阻塞队列填满后才会创建新线程,如果不断地创建线程,直到达到最大线程数时,触发拒绝策略
- 拒绝策略
- DiscardOldestPolicy 抛弃最老的任务
- AbortPolicy 直接抛出异常,默认执行该策略
- CallerRunsPolicy 调用者执行该任务
- DiscardPolicy 抛弃最新提交的任务
- 任务提交
- Submit 有返回值
- Excute 入参为 runnale,有无返回值
- 销毁线程池
- shutdown
- shutdownNow 立即发送中断指令
当调用线程池中断操作时,会对正在运行的线程设置中断指令,也就是执行 interrupt() 方法,最终线程是否停止,取决于线程内部是否有对中断信号进行处理
AQS
全称为 AbstractQueuedSynchronier ,抽象的队列式同步器,是一个由 Jdk 提供的底层同步器,,显示锁是基于它来实现的,具体内容可以参考这篇博客( 从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队 )
- 使用 AQS 有什么作用呢?
AQS 内部是怎样实现的呢?
CLH
基于 AQS 实现独占锁
基于 AQS 实现可重入锁
Java 内存模型 [JMM]
现在计算机处理器由于 cpu 与内存的速度差距太大,为了提高运算效率,引入了多级缓存的概念(L1/L2/L3等)。每次 CPU 运算前将数据从内存复制到缓存中,运算时直接从缓存中获取,运算结束后又将结果写回内存
Java 虚拟机定义了一套 Java 内存模型(JMM),分为线程工作内存和主内存,类似缓存和内存的对应关系。比如多个线程执行 count = count+1 操作时(count 为 0),首先线程会从主内存中读取 count 的值到工作内存
- volatile 关键字是什么
能保证操作的可见性,但不能保证操作的原子性,是什么意思呢?比如上面提到多线程自加,当工作线程中 +1 后,会马上将主内存对应的值也进行 +1
举个栗子:当一个线程从主内存读取时 count == 1,它工作内存中 count 的值也为 1,执行 count + 1 后,由于 count 使用 volatile 修饰,主内存的值也会立刻更新,如果此时该线程的时间片用完了(上下文切换),但 count 赋值的操作还并未完成,当线程重新恢复执行时,从主内存读取 count 的值此时已经为 2 了
当只有一个线程写,多个线程读的时候,volatile 是够用的
- volatile 的实现原理
使用 volatile 关键字的共享变量,在写操作前会使用 cpu 提供的 lock 前缀指令
- 保证了工作内存中的值实时刷新到主内存中
- 强制使得其他工作内存中的值失效,重新从主内存中获取最新的值
volatile int count = 0;
- 使用双检锁实现单例模式,为什么使用了 volatile 关键字 ,仍然需要加锁呢?
在双检锁的实现中,对象实例使用 volatile 修饰,但由于 volatile 关键字的只能保证可见性,而创建对象,并且给 INSTANCE 实例的操作在 JVM 中 并不是原子操作,所以需要用到 synchornied()来保证原子性。
这么说的话,那么单纯使用锁的话就好了,那为什么又要用到 volatile 呢?
- volatile 和 synchornized 有什么区别呢?
volatile 和 synchornized 都是解决多线程数据同步问题的,但是它俩的作用范围不一样, volatile 是轻量级的(非锁),只能保证及时将工作内存中的值刷新到主内存中。而 synchornized 是重量级锁,既能保证可见性又能保证原子性
大厂面试题
- sychronied修饰普通方法和静态方法的区别?什么是可见性?
- 锁分哪几类?
- CAS无锁编程的原理。
- ReentrantLock 的实现原理。
- AQS 原理(小米、京东)
- Synchronized 的原理以及与 ReentrantLock 的区别。(360)
- Synchronized 做了哪些优化(京东)
- Synchronized static 与非 static 锁的区别和范围(小米)
- volatile 能否保证线程安全?在 DCL 上的作用是什么?
- volatile 和synchronize 有什么区别?(B站小米京东)
- 什么是守护线程?你是如何退出一个线程的?
- sleep、wait、yield 的区别,wait 的线程如何唤醒它?(京东、头条)
- sleep 是可中断的么?(小米)
- 线程生命周期。
- ThreadLocal 是什么?
- 线程池基本原理
- 三个线程 T1、T2、T3, 怎样确保按顺序执行