Java多线程学习 (吐血超详细总结)
进程
是程序的一次 执行过程,是 程序执行 和 资源分配 的基本单位。线程
是一个比 进程 更小的 执行单位,多个 线程 共享 进程 的 堆、方法区 资源,线程切换 效率更高。
【每个线程 有自己的 程序计数器、虚拟机栈 和 本地方法栈】
在 Java 中,启动 main 函数 就相当于启动了一个 JVM 的进程,而 main 函数 所在的 线程 就是这个进程中的 一个线程,也称 主线程。
什么是守护线程?
用户线程
: main线程 和 用户创建的线程 是 用户线程。守护线程
: 程序运行时,在后台提供 通用服务 的 线程。JVM 中的 垃圾回收线程 就是 守护线程。
Java多线程学习 (吐血超详细总结)
Java 线程的几种状态
新建状态
可运行状态
:包括 就绪 和 运行 两个状态。阻塞状态
:阻塞 是因为 获取不到锁。【同步阻塞】等待状态
:调用了 无参wait() 方法,线程要等待 notify() 方法 唤醒,并 没有在竞争锁。【等待阻塞】定时等待状态
:调用了 sleep()方法 或 有参wait()方法。【定时阻塞】终止状态
异步处理
:发短信、发邮件 等。批量处理
定时任务
- 继承
Thread类
: 重写run()
方法。- 实现
Runnable接口
: 重写run()
方法。- 实现
Callabe接口
: 重写call()
方法, 该接口有 返回值。
Java多线程学习 (吐血超详细总结)
- 可以避免 Java 中的 单继承限制
- 线程池 只能放入实现 Runable接口 或 Callable接口 的线程,不能直接放入继承 Thread类 的线程。
run()
: 线程体。它包含了线程要执行的内容。start()
: 创建 新的线程,并且执行在 run()方法 里的代码。
直接调用 run()方法,只会把 run()方法 当作 普通方法 去执行。
Java线程间通信有多少种方法
锁与同步
(ReentrantLock + Condition)等待/通知机制
(synchronized + wait() + notify())信号量
: 点击查看共享内存
管道
信号量
: 点击查看共享内存
线程优先级
:通过 线程优先级 来决定线程 分配CPU的顺序,高优先级 的线程 优先分配。- Java 中的 线程优先级 默认为 5。优先级范围是 1~10。
死锁
是指 两个以上的进程 在运行时,由于 竞争资源 而造成的一种 互相等待 的现象,若无外力干预,它们都将无法推进下去。
预防死锁,只需要破坏 死锁 的产生的 必要条件 即可:
破坏请求与保持条件
:一次性申请 所有的资源。破坏不可剥夺条件
:如果申请不到 所有资源,就要 主动释放 已占有的资源。破坏循环等待条件
:按序申请资源。
手动锁 + 同步
(ReentrantLock + Condition)自动锁 + 同步
(synchronized + wait() + notify())线程安全类
:如 Vector、Hashtable、ConcurrentHashMap等。
说说sleep和 wait有什么区别
sleep()
是 Thread类 的方法;wait()
是 Object类 的方法。sleep()
后线程进入 定时阻塞 状态,wait()
后线程进入 等待阻塞 状态。sleep()
是使线程 休眠,不会释放对象的锁。wait()
是使线程 等待, 释放对象的锁。
作用:都是 暂停线程,释放 CPU,使得其他线程能够访问。
sleep()
等到 睡眠时间结束 可进入 就绪状态。yield()
释放 CPU 后立即进入 就绪状态。sleep()
给其他线程运行机会时 不考虑线程的优先级;yield()
只会给 相同优先级 或 更高优先级 的线程以运行的机会;
Java基础——Synchronized用法
synchronized
是 Java 中的一个 关键字,主是一种 自动锁,保证资源可以 互斥访问。synchronized
可以保证被它修饰的 方法 或 代码块 在任意时刻只能有 一个线程执行。
synchronized
的内部是基于JVM内置锁
实现的。- 当一个线程进入一个 synchronized 区域时,需要先获取 JVM内置锁,并在结束时 自动释放锁。
同步块
是更好的选择,因为它 不会锁住整个对象。(也可以让它锁对象)同步方法
会锁住 整个对象 (锁住对象中全部 同步块 和 同步方法)。
- 构造方法 本身就是于 线程安全 的。
- 涉及 共享资源 时,可以使用
synchronized
确保 线程安全。
volatile
关键字 主要用于保证 变量 的 可见性 和 有序性。
可见性
:变量 被 volatile 修饰时,它会保证 修改的值 会立即更新到 主存,其他线程 可以立即读取到 最新的值。有序性
:被 volatile 修饰的 变量 不会被 重排序。
多个线程读,一个线程写的场合
:一个线程在修改 volatile 变量 时,其他线程 能够立即看到 最新结果。作为标记使用
位置不同
:synchronized
可以修饰 方法、代码块;volatile
只能给修饰 变量。效率不同
:synchronized
可能导致 线程阻塞;volatile
不会造成 线程阻塞。
synchronized
可以给 方法、代码块 加锁;Lock
只能给 代码块 加锁。synchronized
不需要手动 管理锁,不会造成死锁;Lock
需要 手动管理锁,使用不当会 造成死锁。
注:
synchronized
是 Java 内置 同步机制关键字。Lock
是个 Java接口;
ThreadLocal使用与原理
访问
ThreadLocal变量
的 每个线程 都会有一个这个变量的 本地拷贝,不会影响其他线程的 局部变量,实现了线程的 数据隔离。
原理
:ThreadLocal 在内部使用一个类似于 Map 的 ThreadLocalMap 来存储 每个线程 的 变量副本。注意
:ThreadLocal 局部变量 使用结束后,需要手动 remove 删除,否则会造成内存泄漏
。
JUC
指的是 Java 的 并发工具包 (java.util.Concurrent),提供了多种 线程同步 和 线程通信 的工具类
。JUC 提供了一系列 线程安全工具:
手动锁 + 同步
(ReentrantLock + Condition)自动锁 + 同步
(synchronized + wait() + notify())线程安全类
:如 Vector、Hashtable、ConcurrentHashMap等。线程池工具类
Java并发–可重入锁、读写锁、(非)公平锁都怎么用
公平锁
: 线程 按照申请锁的顺序 来 获取锁。非公平锁
: 线程 不按照申请锁的顺序 来 获取锁。
`
注:
sychronized
只支持 非公平锁,ReentrantLock
默认 是 非公平锁,但是支持 公平锁。
究竟什么是可重入锁?
可重入锁
也叫递归锁
: 可递归调用 的 锁,在 外层 获取 对象的锁 之后,内层 仍然可以 再次获得该对象的锁,并且 不发生死锁。
可重入锁
:Lock
、ReentrantLock
和synchronized
都是 可重入锁。
public class MyReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner(); // inner() 函数中也有加锁释放锁的行为
lock.unlock(); // 调用结束后仍可以继续执行本函数
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
Java–CAS的原理和优缺点
点击查看
CAS
(Compare And Swap):是一种乐观锁
机制,通过比较 内存中的值 与 期望值 是否相等来确定 是否执行交换操作。- 优点:性能很高。缺点:如果 冲突频繁,则 CPU 占用很高。
ReentrantLock 是如何实现锁公平和非公平性的?
ReentrantLock
是 Lock 接口 的一个 具体实现类,是一个 可重入锁,同时也实现了 公平锁 机制。ReentrantLock
内部使用了 AQS 来实现 锁资源的竞争,从而保证了 公平性。
Java–AQS的原理
Java–原子类(atomic)的用法(有实例)
基本类型:
AtomicInteger
AtomicLong
数组类型:
AtomicIntegerArray
AtomicLongArray
手动实现简单线程池
Java线程池总结
什么是线程池:
线程池
:管理 多个 线程 的池子, 线程池 里的 线程 不断地从任务队列
中取出任务
执行。- 高并发场景下 大量 创建线程 很费时, 使用 线程池 可以 提高响应速度。
线程池的优点:
提高响应速度
:高并发场景下 大量 创建线程 很费时, 使用 线程池 可以 提高响应速度。统一的连接管理
: 提高 线程 的 可管理性。
面试必备:Java线程池解析
线程池可以通过
ThreadPoolExecutor
来创建,核心参数有:
corePoolSize
:核心线程数。maximumPoolSize
:最大线程数。workQueue
: 存放任务的 阻塞队列。handler
: 线程池的 饱和策略。
不重要的参数:
- keepAliveTime:非核心线程的 存活时间。
- unit: 指定 keepAliveTime 的 时间单位。
Java线程池系列–线程池的种类
FixedThreadPool
:固定大小 的线程池。SingleThreadExecutor
:单线程 的线程池。CachedThreadPool
:可缓存 线程池。ScheduleThreadPool
:可定时 的线程池。
种类 | 核心线程数 | 最大线程数 | 队列长度 | 描述 |
---|---|---|---|---|
FixedThreadPool | n | n | 无限 | / |
SingleThreadExecutor | 1 | 1 | 无限 | / |
CachedThreadPool | 0 | 无限 | 0 | 动态增删线程 |
ScheduleThreadPool | n | 无限 | 无限 | 周期性执行任务 |
FixedThreadPool
:适用于 负载稳定,任务数量可预知 的场景。CachedThreadPool
:适用于 负载不稳定,可能会有 负载高峰 的场景。SingleThreadExecutor
:适用于 按顺序执行任务 的场景,任务会按照 提交顺序 执行。
面试必备:Java线程池解析
阻塞队列 (BlockingQueue)
:在 普通队列 上增加了 两个操作:
- 1.在队列为空时: 获取元素的线程会 等待 队列变为非空。
- 2.当队列满时: 存储元素的线程会 等待 队列可用。
【阻塞队列
中的 线程任务 都是Runnable接口
类型的,因为 executor.execute() 的参数类型只能是 Runnable接口】
常见的 阻塞队列:
LinkedBlockingQueue
: 基于 链表 的 无界阻塞队列,容量为 Integer.MAX_VALUE。
【FixedThreadPool、SingleThreadExecutor 使用】SynchronousQueue
: 无缓冲 的 阻塞队列 (同步队列)。
【CachedThreadPool 使用】DelayedWorkQueue
: 支持 延时获取数据 的 阻塞队列。
【ScheduledThreadPool 使用】
饱和策略 | 说明 |
---|---|
AbortPolicy |
(默认策略) 丢弃此任务,抛出异常 |
DiscardPolicy |
丢弃此任务, 不抛出异常 |
CallerRunsPolicy |
让 提交任务的线程 执行此任务 |
DiscardOldestPolicy |
从队列里 删除最老的任务 |
核心线程
: 线程池中 始终存在 的线程,非核心线程
:当 任务数量 超过了 核心线程 数量,并且 阻塞队列 满时,才会创建的线程就是 非核心线程。
CPU密集型
:核心线程数 = CPU核数 + 1IO密集型
:核心线程数 = CPU核数 * 2 + 1- +1的原因:一个 额外 的线程,可以确保 CPU 不会 中断工作。
如果任务 被阻塞的时间 大于 执行时间,即 IO密集型 任务,就需要创建 比处理器核心数大几倍数量的线程。
- 在 子线程 中进行
try...catch
捕捉异常。- 通过 submit() 提交线程,在外部可以通过 Future 中的 future.get() 获取到异常。
线程池ThreadPoolExecutor和Executors详解
ThreadPoolExecutor
是创建 自定义线程池。Executors
是用于创建 常见线程池 的工具类,不能实例化。
点击查看
execute(Runnable x)
:没有返回值。无法判断 任务 是否成功完成。submit(Callable x)
:可以提供Future
类型的 返回值。可以用这个 future 来判断任务是否成功完成。
Java异步–CompletableFuture–实例
CompletableFuture
:实现了 Future, CompletionStage 两个接口。
CompletableFuture 相比于 Future 有以下优点:
- 支持手动结束
- 支持回调函数
- 支持多个Future合并
runAsync
:不支持返回值。因为它接收 Runnable接口。Runnable是没有返回值的。supplyAsync
:支持返回值。因为它接收 Supplier接口。
Lambda
表达式 是一种 轻量级 的 匿名函数。它的本质只是一个 “语法糖”,由 编译器 转换为 常规的代码,可以用 更少的代码 来实现 同样的功能。Lambda
表达式 的形式为:(参数) -> Java表达式
。
Lambda表达式 使用案例
- 如果 接口 内部只有 一个方法,则可以用 Lambda表达式 来 实例化接口,其中,Lambda表达式 的作用是 重写接口的方法。
- 如果 方法 的 参数 是 只包含一个方法的接口,则可以直接用 Lambda表达式 作为该方法的入参,再简单一点,可以使用 方法引用。
Java8 中的
方法引用
是一种快速简便地将 现有方法 转换为Lambda
表达式 的方式。它可以使代码更加 简洁易懂,减少重复性的代码,提高代码的 可读性 和 可维护性。
方法引用语法如下:
对象::实例方法名
类::静态方法名
类::实例方法名
其中,第一个参数 表示方法的接收者(如果是实例方法)。例如,对于
System.out::println
,System.out
是接收者,println
是方法名。如果方法是静态的,则第一个参数表示方法所在类的名字,例如Math::sqrt
。
方法引用 使用案例
Java–Stream流详解
Stream
是一个 高效 且 易于使用 的 集合处理数据方式。
- 可以进行复杂的 集合数据操作,比如 查找、过滤 和 映射 等。
- 不直接修改 源数据,而是生成 新的 数据集合。
List<Student> students = new ArrayList<>();
// ...
// 获取所有学生的姓名
List<String> tmpList1 = students.stream().map(s -> s.getName()).collect(Collectors.toList());
// 获取所有学生的姓名和年龄
List<People> tmpList2 = students.stream().map(s -> new People(s.getName(), s.getAge)).collect(Collectors.toList());
// 筛选年龄 大于16岁 的学生
List<Student> tmpList3 = students.stream().filter(s -> s.getAge() > 16).collect(Collectors.toList());