从JVM的角度看 :一个进程有多个线程,每个线程共享 栈和 方法区(1.8后为 元空间) ,
每个线程都要自己的 程序计数器 虚拟机栈 本地方法栈。
总结: 线程 是进程 划分的 最小的 运行单位。 线程和 进程 最大的不同在于 基本上
各进程是独立的 而各线程不一定, 因为同一个进程中 的线程 极有可能 相互影响。
而 线程执行开销小 但不利于资源的管理 , 而 线程 则相反。
为什么程序计数器、虚拟机栈和本地⽅法栈是线程私有的呢?为什么堆和⽅
法区是线程共享的呢?
程序计数器 为什么是私有的?
主要有2个原因:
- 在多线程的情况下,程序计数器能够记录当前线程的执行位置,从而当线程被切换回来的时候 能够 知道线程 上次的执行位置。
- 字节解释器 通过改变 程序计数器的 来依次读取指令 ,从而实现 代码的流程控制。
例如 : 顺序执行 循环 异常的处理 选择。
需要 注意的是 当执行 的是 native 方法 ,那么程序计数器 记录的 是 undefined 地址, 只有执行代码时 程序计数器记录的才是 下一条 指令的地址。
所以, 程序计数器私有 是为了 线程 切换后能恢复到 正确的 执行位置。
虚拟机栈 和 本地方法栈 为什么 是 私有的?
虚拟机栈 : 每个 java 方法 在执行的同时 会 创建 一个 栈帧 用于 存储 局部变量表 操作数栈 常量池引用等信息, 从 方法调用 直至 执行 完成 的过程 ,就对应 一个 栈帧 在 java 虚拟机栈中 入栈 和 出栈 的过程。
本地方法栈 : 和虚拟机栈的作用非常相似。 区别就是 : 虚拟机栈 是为 虚拟机 执行 java代码(也就是字节码)服务, 而 本地方法栈 则为虚拟机 使用到的 navite 方法 服务 , 在 HotSpoot 虚拟机 中 和 java 虚拟机栈 合二为一。
所以: 为了保证线程中 的 局部变量 不被 别的线程 访问到 ,虚拟机栈 和 本地方法栈 是私有的。
** 一句话简单了解 堆 和 方法区 **
堆 和 方法区 都是 所有 线程 共享 的资源 , 其中 堆 是进程中最大的内存,主要用于存储 新创建的对象(所有对象都在这里分配内存) , 方法区 主要用于 存放 已被加载的 类信息 常量 静态变量 即时编译器 编译后的代码等数据。
** 说说 并发 和 并行 的区别? **
并发 : 同一时间段, 多个任务都执行。
并行: 单位时间内, 多个任务同时执行。
** 使用 多线程 可能 带来 什么问题? **
并发编程的目的 是为了 提高 程序的执行效率 和 提高 程序的 运行 速度,但是 并发编程 并不是 总能提高 程序 运行速度的 , 而且并发编程 可能会遇到很多问题。
比如: 内存泄露 上下文切换 死锁还有受限于硬件 和 软件 的资源闲置 问题。
** 线程 的生命周期**
Java 线程在运⾏的⽣命周期中的指定时刻只可能处于下⾯ 6 种不同状态的其中⼀个状态
线程在⽣命周期中并不是固定处于某⼀个状态⽽是随着代码的执⾏在不同状态之间切换。Java 线程状
态变迁如下图所示
线程创建之后进入 NEW(初始)状 态 ,调用 start() 方法 后 开始运行 线程就处于 READY(可运行) 状态 。 可运行状态 的线程 获得了 CPU 时间片 后就 处于 RUNNING(运行)状态 。
当线程执行 wait() 方法后 线程 就进入 WAITING(等待) 状态 。 进入等待状态的线程 需要其他 线程的通知 才能 返回到运行状态 , 而 TIME_WAITING (超时等待) 状态 相当于 在等待状态的 基础 上 增加了 超时限制 , 比如通过 sleep(long millis) 方法 或 wait(long millis) 方法 可以将 java 线程 置于 TIME_WAITING状态, 当超时时间到达后 java线程将返回 RUNNABLE 状态。 当线程 调用 同步方法时, 在没有 获取到 锁的情况下 线程进入 BLOCKED (阻塞) 状态。 线程在执行
RUNNABLE 的 run() 方法之后就会 进入 到 TERMINATED (终止) 状态。
** 什么 是线程 死锁? 如何避免 死锁?
线程 死锁 : 多个线程 同时被阻塞,他们中的一个 或者 全部 都在等待 某个资源的释放 ,由于线程 被 无期限 阻塞 ,因此程序 不可能 正常终止。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
Output
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
/*
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过
Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 得到执⾏然后获取到 resource2 的监视 器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对⽅的资源,然后这两个线程就会陷⼊互相等
待的状态,这也就产⽣了死锁。上⾯的例⼦符合产⽣死锁的四个必要条件。
*/
** 产生 死锁的 四个条件 **
- 互斥条件: 该资源任意一个时刻 只由 一个 线程 占用。
- 请求保持条件: 一个进程 因请求资源而堵塞时,对已获得的资源保持不放。
3.不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程 强行剥夺, 只有自己使用完毕后才释放资源。
4.循环等待条件: 若干线程之间形成了一个 头尾相接 循环 等待资源关系。
** 如何避免 死锁**
1.破坏 互斥条件 : 这个条件 无法破坏, 因为我们用 锁本来 就是想让他们 互斥 (临近资源 需要 互斥访问)
- 破坏 请求与保存条件 : 一次性申请所有的资源
3.破坏 不剥夺条件 : 占用部分资源的线程 进一步访问其他资源 时, 如果申请不到,可以主动放弃它占用的资源。 - 破坏 循环等待条件 : 靠 按序申请资源来预防。 按某一条件 顺序 申请资源, 释放资源时 则 反序 释放 ,破坏 循环等待条件。
我们对线程 2 的代码修改成下⾯这样就不会产⽣死锁了。
new Thread(() i> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get
resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get
resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get
resource2");
}
}
}, "线程 2").start();
Output
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
/*
线程 1 ⾸先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取
resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占
⽤,线程 2 获取到就可以执⾏了。这样就破坏了破坏循环等待条件,因此避免了死锁。
*/
说说 sleep() 方法 和 wait () 方法 区别 和 共同点?
共同点: 两者 既可以 暂停 线程的执行。
区别: sleep()方法 没有 释放锁 wait() 方法 释放了锁
sleep 通常 用于 暂停执行 wait 通常 用于 线程间 的 通信 / 交互。
sleep() 方法 执行完成后 ,线程会 自动 苏醒;
wait() 方法 调用 后 , 线程 不会 自动 苏醒 ,需要 别的 线程 调用 同一 对象 的 notify () 或者 notnotify() ,也可以 使用 wait(long timeout) 超时后线程自动苏醒。
** ☆ 为什么我们在调用 start()方法 时 会 执行 run()方法 ,为什么 我们不直接调用 run() 方法?☆ ** (重点面试题)
new 一个 Thread, 线程就进入了 就绪状态 ; 调用 start() 方法 ,会启动一个线程并使 线程 进入 就绪状态 ,当分配到 时间片 后就可以开始执行了。 start () 会执行 线程 的相应工作 ,然后 自动 执行 run()方法 的内容, 这就是真正的 多线程工作。 然而 直接 调用 run() 方法 ,会把 run()方法 当做 main 线程 下的 普通 方法去执行 并不会在 某个 线程中取执行它 , 所以 这不是 多线程 工作。
** synchronized 关键字 **
synchronized 关键字 是 解决 多个线程 之间 访问 资源的同步性, synchronized 关键字 可以保证 被他修饰 的 方法 或者 代码块 在任意 时刻 只能有一个线程被执行。
synchronized 关键字 最主要 的 三种 使用方式
修饰实例方法 :作用于当前对象实例加锁,进入同步代码块前 要获得 当前 对象实例 的锁。
synchronized void method(){
}
修饰代码块 : 指定 加锁对象 ,对 给定 对象 加锁 , 进入同步代码库前要获得给定对象的锁。
synchronized(this){
}
修饰 静态方法 : 就是给 当前类加锁,会作用于 类的 所有对象 实例, 因为静态成员 不属于任何 一个 实例对象 ,是类成员(static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有⼀份) 。
如果一个线程 A 调用 一个 实例 对象 的非静态 synchronized 方法, 而线程B 调用 这个 实例对象 所属类的 静态 synchronized 方法 不会出现 互斥现象。
因为 访问 静态 synchronized 方法占用的 锁 是当前类的 锁 ,而 访问 非静态 synchronized 方法 占用 的锁 是 当前实例 对象 的锁。
synchronized static void method(){
}
** 总结 :
synchronized 关键字 加到 static 静态 方法 和 synchronized(class) 代码块 都是 给 class 类(当前类) 上锁。 synchronized 关键字 加到 实例方法 上 是给 实例对象上锁。 尽量不要使用 synchronized(String a) 因为在jvm 中 ,字符串具有缓存功能!
package pers.zzk.jianzhi_offer;
public class DeadLockDemo {
private static String resource1="aaa";
private static String resource2="aaa";
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
//保持resource1不释放
while(true) {
System.out.println(Thread.currentThread() + "get resource1");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程 2").start();
}
}
以上代码中我们创建了两个资源resource1和resource2,String类型且指向同一个对象。
线程1中我们占用resource1,并一直占用不释放;线程2中我们占用resource2
1、使用Object类型创建数据,避免缓存
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
使用new 字符创建一个新的对象
private static String resource2=new String("aaa");
**
⾯试中⾯试官经常会说:“单例模式了解吗?来给我⼿写⼀下!给我解释⼀下双重检验锁⽅式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全)
package test;
/**
* 双重校验锁实现对象单例(线程安全)
*/
public class Singleton {
private volatile static Singleton uniqueInstance;
public Singleton() {
}
public synchronized static Singleton getUniqueInstance(){
//先判断对象是否已经实例过,没有实例化过才能进入加锁代码
if (uniqueInstance ==null){
//给类对象加锁
synchronized (Singleton.class){
if (uniqueInstance ==null){
uniqueInstance=new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执⾏:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和3,此时 T2 调⽤ getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回uniqueInstance,但此时 uniqueInstance 还未被初始化。
使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏.
讲⼀下 synchronized 关键字的底层原理
synchronized同步语块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏
monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象
头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏
monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞
等待,直到锁被另外⼀个线程释放为⽌
synchronized 修饰方法的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized ⽅法");
}
}
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该 ACC_SYNCHRONIZED 访问
标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤
说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍⼀下这些优化吗?
JDK1.6 对锁的实现引⼊了⼤量的优化,如偏向锁、轻量级锁、⾃旋锁、适应性⾃旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:⽆锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈⽽逐渐升级。注意锁可以升级不可降级,这种策略是为了提⾼获得锁和释放锁的效率。
** 谈谈 synchronized 和 Reentrantlock 的区别 **
都是 可重入锁
可重入锁 : 自己可以 获取 自己的 内部锁。
比如 一个线程 获得了 某一个 对象的 锁 ,此时这个 对象锁 还没有 释放 ,当其 还想要再次 获取 这个对象 的 锁 还是可以的 ,如果 不可锁重入的 话, 就会造成 死锁。
同一个线程 每次 获取 锁的时候,锁的计数器 都会 自增 1 所以要等到 锁的计数器 下降为0 的时候 才能释放锁。synchronize的 依赖于 虚拟机 reentrantlock 依赖于 API
synchronize的 是依赖于jvm 实现的 ,虚拟机团队 在对 synchronized 关键字进行优化都是在 虚拟机层面的 并没有直接暴露给我们。
reentrantlock 是 JDK 层面(也就是 API 层面 需要 lock() 和 unlock() 方法配合 try/finally 语句块 来完成) 实现的 ,可以通过 查看 它的 源代码 查看它如何实现。
- reentrantlock 比 synchronize的 增加了 一些 高级 特性
reentrantlock 增加了 一些 高级特性 :
1.等待可中断 :ReentrantLock提供了⼀种能够中断等待锁的线程的机制。
可实现公平锁 : ReentrantLock可以指定是公平锁还是⾮公平锁。
⽽synchronized只能是⾮公平锁。所谓的公平锁就是先等待的线程先获得锁。可实现选择性通知(锁可以绑定多个条件)
线程对象可以注册在指定的
Condition中,从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活。 在使⽤
notify()/notifyAll()⽅法进⾏通知时,被通知的线程是由 JVM 选择的,⽤ReentrantLock类结合Condition实例可以实现“选择性通知”.
- 性能 已经不是 选择的 标准
** volatile 关键字 **
volatile: 主要作用 是 保证 变量的 可见性 和 防止 JVM 指令重复排序。
**并发编程 的 三 个 特性 **
原子性 : 一个的操作 或者 多次操作 , 要么 所有操作都能得到执行并且不会收到任何 因素 的干扰 而 中断 ,要么 所有操作 都执行 要么 都不执行 synchronized 能够保持 代码片段 的原子性。
可见性 : 当一个变量 对 共享变量 进行了修改 ,那么 另外 的线程 也能够立即 看到 修改后的 最新值。 volatile 能够保持 共享 变量 的可见性。
有序性: 代码在执行过程中的 先后顺序 , java在编译器以及 运行期间的 优化, 代码的执行顺序 未必就是 编写 时候 代码的顺序, volatile 关键字 能够禁止 指令 进行 重排序优化。
** 说说 synchronized 和 volatile 关键字的区别
volatile 关键字 是 线程同步 的 轻量级实现 ,volatile 性能 肯定 比 synchronized 性能好。 但是 volatile 关键字 只能作用于 变量 而 synchronized 能够 修饰 方法 以及 代码块。
synchronized 关键字 的 使用场景多一些。
多线程访问 volatile 关键字 的时候 不会发生 堵塞 ,而 synchronized 可能会 发生 阻塞。
volatile 关键字 只能 保证 数据的 可见性 ,但不能 保证 数据的 原子性。而 synchronized 两者都能够 保证。
volatile 关键字 主要 用于 解决 变量 在 多个 线程 之间 的可见性 ; 而 synchronized 关键字 解决 的 是 多个线程 之间 访问 的 同步性。
** ThreadLocal **
ThreadLocal 类 主要是 让 每个 线程 绑定 自己的值。 可以将 ThreadLocal 形象 比喻成 一个 存放 数据的 盒子 ,盒子 中 存储了 每个线程 的 私有数据。
如果 你 创建 了 一个 ThreadLoacl 变量 ,那么 访问 这个变量 的 每个线程 都会有这个线程 的 本地副本, 这个也是ThreadLoacl 的由来。 可以使用 get() 和 set() 方法 去获取 默认值 或将其 值 更改 为 当前线程 所存 副本 的值,从而 避免了 线程 安全问题。
最终的变量是放在了当前线程的 ThreadLocalMap中,并不是存在 ThreadLocal 上, ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。
ThreadLocal 内部维护的是⼀个类似 Map 的 ThreadLocalMap 数据结构, key 为当前对象的Thread 对象,值为 Object 对象。
ThreadLocalMap 是 ThreadLocal 的静态内部类。
** ThreadLoacl 内存泄露问题 **
ThreadLocalMap 中使用的 key 为 ThreadLoacl 的弱引用,而 value 是强引用。
所以,在 ThreadLoacl 在没有被 外部 强引用 的 时候,在垃圾回收的时候, key会被清理掉 ,而 value 不会被 清理掉。 这样 一来 就会出现 ThreadLocalMap key为null 的Entry。 假如 我们不做任何措施 , value 永远 不会被 GC 回收,这 时候 就会出现 内存泄露问题。
ThreadLocalMap实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null 的记录。使⽤完ThreadLocal ⽅法后 ,最好⼿动调⽤ remove() ⽅法。
(弱引⽤介绍:
如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。
弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。
不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。
弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,
Java虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。)
** 线程池 **
线程池 提供了 一种 限制 和 管理资源 (包括执行一个任务) 。每个 线程池 还 维护一些 基本 统计 信息 ,例如 已经完成任务 的数量。
使用线程池 的好处:
1.降低资源消耗。 通过重复利用已创建的线程降低 线程创建 和销毁 造成的消耗。
2.提高响应速度。 当 任务达到时, 任务可以不通过 线程的创建 就能立即执行。
3.提高线程的可管理性。 线程是稀缺资源 , 如果无限制的创建 ,不仅会消耗系统资源 , 还会降低 系统的稳定性 ,使用线程池 可以 进行 统一分配 调优和监控。
** 实现 Runnable 接口 和 Callable 接口 的区别 **
Runnable 不会返回结果 或 抛出异常 ,但 Callable 可以。
如果任务 不需要 返回值 或 抛出 异常 推荐 使用 Runnable ,可以 使 代码简洁。
工具类 Executos 可以 实现 Runnable 与 Callable 之间 的转换。
Runnable.java
@FunctionInterface
public interface Runnable(){
//线程被执行,没有返回值也不会抛出异常
public abstract run();
}
Callable.java
public interface Callable(){
/*
计算结果 或无法这样做的时候 抛出异常
@return 计算得出的结果
@throws 如果 无法 计算结果 则抛出异常
*/
V call throw Exception();
}
** 执行 execute() 和 sumbit() 的区别 是什么 ? **
execute() 方法 用于 提交 不需要 返回值 的任务 ,所以无法判断 线程池 执行成功与否。
sumbit () 方法 用于 提交 需要 返回值 的任务。 线程池 会 返回 一个 Future 类型对象 , 通过 Future 对象 可以 判断 这个 任务 是否 执行成功; 并且可以通过 get() 方法 来 获取 返回值。 get()方法 会 阻塞 当前 线程 直至任务 完成。 而 使用 get(long timeout,TimeUnit unit) 方法 会 阻塞 当前线程 一段时间后立即返回, 这可能 会使 任务 还没执行完成 就返回。
我们以 AbstractExecutorService 接⼝中的⼀个 submit ⽅法为例⼦来看看源代码:
上⾯⽅法调⽤的 newTaskFor ⽅法返回了⼀个 FutureTask 对象。
我们再来看看 execute() ⽅法:
public Future<?> submit(Runnable task) {
if (task WX null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T
value) {
return new FutureTask<T>(runnable, value);
}
public void execute(Runnable command) {
...
}
** 如何 创建 线程 池 **
《阿⾥巴巴Java开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过
ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的⻛险。
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列⻓度为Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。
⽅式⼀:通过构造⽅法实现
⽅式⼆:通过Executor 框架的⼯具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor:
FixedThreadPool : 该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。
ThreadPoolExecutor 构造函数重要参数分析:
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize : 核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。
workQueue : 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor 其他常⻅参数:
1. keepAliveTime :当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任务提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
2. unit : keepAliveTime 参数的时间单位。
3. threadFactory :executor 创建新线程的时候会⽤到。
4. handler :饱和策略。关于饱和策略下⾯单独介绍⼀下。
ThreadPoolExecutor 饱和策略定义:
如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任
时, ThreadPoolTaskExecutor 定义⼀些策略:
1.ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。
2.ThreadPoolExecutor.CallerRunsPolicy :调⽤执⾏⾃⼰的线程运⾏任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话,你可以选择这个策略。
3.ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
4.ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
举个例⼦: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使⽤的是 ThreadPoolExecutor.AbortPolicy 。在默认情况下, ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代
表你将丢失对这个任务的处理。 对于可伸缩的应⽤程序,建议使⽤
ThreadPoolExecutor.CallerRunsPolicy 。当最⼤池被填满时,此策略为我们提供可伸缩队列。
** ⼀个简单的线程池Demo: Runnable + ThreadPoolExecutor **
package Pool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池
*/
public class DutyThreadPoolExecutor {
private static ThreadPoolExecutor pool=null;
private DutyThreadPoolExecutor(ThreadPoolExecutor pool) {
this.pool = pool;
}
private DutyThreadPoolExecutor() {
}
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static ThreadPoolExecutor getPool(){
if (pool ==null){
synchronized (DutyThreadPoolExecutor.class)
{
if (pool ==null){
pool=new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
}
return pool;
}
}
package Pool;
import java.util.Date;
import java.util.concurrent.Callable;
public class PoolCallable implements Callable<String> {
private String command;
public PoolCallable(String s) {
this.command = s;
}
@Override
public String toString() {
return this.command;
}
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + " Start.Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
return "线程操作成功";
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package Pool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
public class PoolTest {
public static void main(String[] args) {
ThreadPoolExecutor pool= DutyThreadPoolExecutor.getPool();
for (int i = 0; i < 10; i++) {
PoolCallable poolCallable = new PoolCallable("" + i);
FutureTask<String> future = new FutureTask<>(poolCallable);
try {
pool.submit(future);
} catch (RejectedExecutionException e) {
throw new RejectedExecutionException("系统任务繁忙,详细比对任务被拒绝执行");
}
try {
String s = future.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
/*
可以看到我们上⾯的代码指定了:
1. corePoolSize : 核⼼线程数为 5。
2. maximumPoolSize :最⼤线程数 10
3. keepAliveTime : 等待时间为 1L。
4. unit : 等待时间的单位为 TimeUnit.SECONDS。
5. workQueue :任务队列为 ArrayBlockingQueue ,并且容量为 100;
6. handler :饱和策略为 CallerRunsPolicy 。
*/
** 线程池 原理 分析 **
我们在代码中模拟了 10 个任务,我们配置的核⼼线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执⾏,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之⾏完成后,才会之⾏剩下的 5 个任务。
** Atomic 原子类 **
含义: Atomic 指的是 一个操作是不可中断的 ,即使是 多个 线程 一起 执行 的时候 ,一个操作 一旦开始 ,就 不会 被 其他 线程 干扰。 所以 , 所谓 原子类 说 简单 点 就是 具有 原子/ 原子操作 特征 的类。
并发包 java.util.concurrent 的原⼦类都存放在 java.util.concurrent.atomic 下,如下图
所示。
** JUC 包下 的 原子类 是 哪 四类? **
基本类型 :
使用原子的方式更新基本类型
AtomicLong : 长整型 原子类
AtomicInteger : 整形 原子类
AtomicBoolean : 布尔类型 原子类
数组类型 :
使用原子的方式 更新 数组 里 的某个元素
AtomicIntegerArray : 整数数组 原子类
AtomicLongArray : 长整型 数组 原子类
AtomicReferenceArray : 引用 类型 数组 原子类
引用类型 :
AtomicReference : 引用类型 原子类
AtomicStampedReference : 原子更新带有版本号的引用类型。 该类 将 整数型值 与引用关联 起来 , 可以 用于 解决 原子 的更新 数据 和数据 的 版本号 ,可以 解决使用 CAS 进行 原子更新 时 可能 出现 的 ABA 问题。
AtomicMarkableReference: 原子更新 带有 标记位 的引用类型
对象的属性修改类型
AtomicIntegerFiledUpdater : 原子更新 整形 字段 的更新器
AtomicLongFiledUpdater : 原子更新 长整型 字段 的更新器
** 讲讲 AtomicInteger 的 使用 **
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并⾃增
public final int getAndDecrement() //获取当前的值,并⾃减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输⼊的数值等于预期
值,则以原⼦⽅式将该值设置为输⼊值(update)
public final void lazySet(int newValue)//最终设置为newValue,使⽤ lazySet
设置之后可能导致其他线程在之后的⼀⼩段时间内还是可以读到旧的值。
AtomicInteger 类的使⽤示例
使⽤ AtomicInteger 之后,不⽤对 increment() ⽅法加锁也可以保证线程安全。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使⽤AtomicInteger之后,不需要对该⽅法加锁,也可以实现线程安全。
public void increment() {
//返回新值(即加1后的值)
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
** 能不能给我 简单 介绍 一下 AtomicInteger 类 的原理
AtomicInteger 线程安全原理简单分析
AtomicInteger 类的部分源码:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提
供“⽐较并替换”的作⽤)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 类 主要 利用 CAS + volatile 和 native 方法 来 保证 原子 操作, 从而避免了 synchronized 的 高开销 执行效率 大为提高。
CAS的原理 是 拿 期望值 和 原本值 做一个比较 ,如果相同 则 更新为新的值。
(UnSafe 类的objectFieldOffset() ⽅法是⼀个本地⽅法,这个⽅法是⽤来拿到“原来的值”的内存地址,返回值是valueOffset。)
另外 , value 是一个 volatile 变量 ,在内存中可见 ,因此 JVM 可以 保证 在 任何时刻 任何线程 都能 拿到 该变量 的最新值。
** ☆ AQS ☆ (重点)**