https://www.bilibili.com/video/BV16J411h7Rd 小林code八股
多线程
内存模型JMM - 介绍
-
解决多线程并发的一套规则,规定了在多线程环境下,线程怎么访问共享变量才能不出错,核心处理是可见性、原子性、有序性这三个问题。核心思路:规定变量必须从主内存加载到工作内存才能进行操作,改完再写回主内存。
-
可见性:(e.g. 遇到线程读取变量时并未更新的情况,因为CPU有缓存,线程在工作时,会把主内存的变量读到自己的工作内存中,但是改完之后还没有即时更新值。解决办法:
volatile关键字:改完变量后会立刻刷回主内存,同时使其他线程的缓存失效) -
原子性:
synchronizedorLock锁 -
有序性:
多线程是什么?需要注意的点
-
多线程主要是在一个Java程序中运行多个线程,这些线程共享程序的内存空间,但有各自的栈和程序计数器。能同时执行不同的任务,提高了程序的效率。
-
需要注意的点:
-
线程安全问题:
-
线程间的通信:
wait(),notify()等等 -
线程的创建和销毁成本(线程池)
-
Java中的线程和OS中的线程差异?
- Java中创建线程的方式:
pthread_create,所以本质上Java程序创建的线程和OS是一致的,都是1对1的线程模型
使用多线程要注意什么
-
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic包和关键字
synchronized -
可见性:一个线程对主内存的修改可以即使被其他线程看到
-
有序性:happens-before原则来确保有序性
保证数据一致性的方案
-
事务管理:通过数据库的ACID的属性来保证数据的一致性
-
锁机制:实现对共享资源的互斥访问。可以使用
synchronized或者`ReentrantLock
线程的创建、启动、停止方式
-
线程创建
- 继承
Thread类 - 实现
Runnable接口 - 实现
Callable接口与FutrureTask - 使用线程池
Executor框架
- 继承
-
线程启动
Thread.start()
-
线程停止
- 异常法停止:
interrupt() - 沉睡中停止:
sleep() -> interrupt() - 暴力停止:`stop()
return停止
- 异常法停止:
调用interrupt让线程抛出异常的原理
-
一个
boolean表示中断状态- 低级别中断可用方法 ->
InterruptedException异常 - 否则
Thread.interrupt()仅设置线程的中断状态,轮询中断判断是否要定值执行任务
- 低级别中断可用方法 ->
Java的线程状态有哪些
| 线程状态 | 说明 |
|---|---|
NEW |
线程创建,还未调用start() |
RUNNABLE |
就绪状态start()+正在运行 |
BLOCKED |
阻塞状态,等待监视器锁 |
WAITING |
等待状态,正在等另一线程执行特定给的操作(notify等) |
TIME_WAITING |
等待状态:特定的等待时间 |
TERMINATED |
终止状态,线程完成了执行 |
sleep和wait的区别
| 特性 | sleep() |
wait() |
|---|---|---|
| 所属类 | Thread类(静态方法) |
Object类(实例方法) |
| 锁释放 | no | yes |
| 使用前提 | 任意位置调用 | 同步块或同步方法内 |
| 唤醒机制 | 超时自动恢复 | 需要notify()/notifyAll()或超时 |
| 设计用途 | 暂停线程执行,不涉及锁写作 | 线程间协调,释放锁让其他线程工作 |
sleep会释放cpu吗
-
会释放
CPU,但不会释放持有的锁 -
线程在调用
sleep()后,会主动让出CPU时间片,进入TIMED_WAITING状态,此时会触发OS的进程调度,将CPU分配给其他处于就绪状态的线程 -
不会释放持有的锁,因此,如果有其他线程试图获取同一把锁,他们让仍会被阻塞,直到原线程退出同步代码块
blocked和waitting的区别
-
触发条件:
-
BLOCKED状态通常是因为试图获取一个对象的锁,但是该锁已经被另一个线程持有。 -
WAITING状态通常是因为它正在等待另一个线程执行某些操作,这个时候线程将不会消耗CPU资源,不会参与锁的竞争
-
-
唤醒机制:
-
阻塞状态的线程在等待锁的时候,一旦锁被释放,线程将有机会重新尝试获取锁。如果成功,就会
BLOCKED -> RUNNABLE -
线程在
WAITING状态时采用的是显式唤醒,如果线程调用了wait()方法,那么它必须等待另一个线程调用同一对象的notify()或者notifyAll()唤醒才行
-
线程如何从wait状态 -> running状态
WAITING -> RUNING的核心机制是通过外部事件的触发或资源可用性变化,比如等待的线程被其他线程对象唤醒,如notify()或者notifyAll()
notify和notifyAll区别
-
notify: 唤醒某一个线程,其他线程都处于WAITING的等待唤醒的状态,如果被唤醒的线程结束时没调用notify,其他线程就没人去唤醒,只能等待超时,或者被中断 -
notifyAll():所有的线程退出waiting状态,开始竞争锁
notify选择哪个线程
notify()在源码注释中说到notify选择唤醒的线程是任意的,依赖于具体的jvmJVM有很多的实现,比较流行的是hotspot,主要是按照“先进先出”的顺序唤醒
不同线程之间是如何通信的
-
共享变量:为了实现多线程的可见性,保证线程的安全,所以通常需要使用
synchronized关键字或volatile关键字 -
线程间协作:
wait(),notify(),notifyAll()方法用于线程间的协作Lock和Condition接口提供了更加灵活的线程间的通信方式。对应的是:await(),signal(),signalall()方法
线程间的通信方式有哪些
-
最基础的线程通信方式,基于对象的监视器(锁)机制:
wait(),notify(),notify()方法
-
Lock和Condition接口Lock优于synchronized,Condition接口则配合- 更好的
await(),signal(),signalall()方法
-
volatile:保证变量的可见性,他会保证对该变量的写操作立即刷新到主内存中,而读内存会从主内存读取最新的值 -
CountDownLatch是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作 -
CyclicBarrier是一个同步辅助类,它允许一组线程相互等待,直到所有线程都达到某个公共屏障点 -
Semaphore是一个计数信号量,它可以控制同时访问特定资源的线程数量
如何停止一个线程
-
共享标志位主动终止:定义一个可见的状态变量,由主线程控制其值
-
中断机制:
Thread.interrupt() -
return取消任务:使用线程池提交任务,并通过Future.cancel()停止线程,依赖中断机制 -
处理不可中断的阻塞操作。
Go的协程和Java的线程有什么区别
-
调度模型不一样:
- Java线程是OS级别的线程,也就是内核线程,他的创建、销毁、调度都是由操作系统内核来管理的
- Go的协程则是用户态的轻量级线程,它是由Go运行时自己调度的,不需要经过系统内核
-
消耗资源差别明显:
- Java创建一个线程的开销是比较大的,默认下一个线程的栈空间大概是1MB左右,而且线程的创建和销毁都需要系统调用,开销不笑
- 而协程就轻量多了,一个协程初始栈空间只有2KB,而且栈空间是可以伸缩的
-
调度方式不一样:
- Java是抢占式的,由OS的调度器决定说明时候切换线程,线程切换需要保存和恢复上下文,还涉及到用户态和内核态的切换,这个开销相对比较大
- Go的切换成本比较低
-
使用方式更简单
-
通信机制差异很大
-
高并发场景下go的表现会更好
并发安全
JUC包下常用的类有哪些
-
线程池
ThreadPoolExecutor用于创建和管理线程池Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池
-
并发集合类:
ConcurrentHashMap:线程安全的哈希映射表,用于在多线程的环境下高效的存储和访问键值对CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行
-
同步工具类:
ConutDownLatch:通过一个计数器来实现,允许一个或多个线程完成操作后再继续执行CycliBarrier: 让一组线程相互等待Semaphore:信号量,用于控制同时访问某个资源的线程数量
-
原子类:
AtomicInteger:原子整数类,提供了对整数类型的原子操作AtomicReference: 原子引用类,用于对对象引用进行原子操作
怎么保证多线程的安全
synchronizedvolatileLock接口和ReentrantLock- 原子类
- 线程局部变量:
ThreadLocal - 并发集合:
ConcurrentHashMap,ConcurrentLinkedQueue - JUC工具类:
Semaphore,CyclicBarrier
Java中常用的锁和使用的场景
- 内置锁
synchronized ReentrantLock- 读写锁
ReadWriteLock: - 乐观锁和悲观锁
- 自旋锁
实践开发时如何使用锁的
synchronizedLock接口:如ReentrantLock- 读写锁
ReadWriteLock
Java并发工具有哪些
ConutDownLatch:通过一个计数器来实现,允许一个或多个线程完成操作后再继续执行CycliBarrier: 让一组线程相互等待Semaphore:信号量,用于控制同时访问某个资源的线程数量Future和CallableConcurrentHashMap:线程安全的哈希映射表,用于在多线程的环境下高效的存储和访问键值对CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行
CountDownLatch有什么用
-
作用:用于让一个或多个线程等待其他线程完成操作后再继续执行
-
一个计数器实现线程间的协调:
- 初始化计数器:
- 等待线程阻塞
- 任务完成通知
- 唤醒等待进程