序号 | 内容 |
---|---|
1 | 基础面试题 |
2 | JVM面试题 |
3 | 多线程面试题 |
4 | MySql面试题 |
5 | 集合容器面试题 |
6 | 设计模式面试题 |
7 | 分布式面试题 |
8 | Spring面试题 |
9 | SpringBoot面试题 |
10 | SpringCloud面试题 |
11 | Redis面试题 |
12 | RabbitMQ面试题 |
13 | ES面试题 |
14 | Nginx、Cancal |
15 | Mybatis面试题 |
16 | 消息队列面试题 |
17 | 网络面试题 |
18 | Linux、Kubenetes面试题 |
19 | Netty面试题 |
线程
线程状态
新建(new):新创建了一个线程对象。调用start方法启动线程,进入就绪状态。
就绪(ready):等待被线程调度选中,获取cpu的使用权。
运行(running):等就绪线程被CPU选中后执行,可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
park、sleep、wait、interrupted
创建线程和启动方式
1、继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
System.out.println("线程正在运行...");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
2、实现Runable
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
System.out.println("线程正在运行...");
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
3、Callable带返回值的
public class MyCallableTask implements Callable<String> {
@Override
public String call() throws Exception {
// 模拟耗时任务
Thread.sleep(1000);
return "Result of task " ;
}
}
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 创建一个Callable任务
Callable<String> task = new MyCallableTask(1);
// 提交任务并获取Future对象
Future<String> future = executorService.submit(task);
try {
// 等待任务完成,并获取结果
String result = future.get();
System.out.println("Task result: " + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭线程池
executorService.shutdown();
}
}
线程安全
线程安全:在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用以及线程如何去交替执行,在程序中不做任何同步干预的操作的情况下,这个方法或者对象的执行(修改)都能按照预期结果来反馈。
线程安全具体体现在原子性、有序性、可见性。
原子性:一段程序只能由一个线程完整的执行完成,而不能存在多个线程干扰。CPU的上下文切换是导致原子性问题的核心,JVM中使用synchronized关键字解决原子性的问题。
有序性:程序编写的指令和CPU运行的指令顺序可能出现不一致的问题(指令重排),有序性可能会导致可见性的问题。
可见性:在多线程环境下,由于读写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,其他线程不是实时可见的。导致可见性问题的原因例如:CPU高速缓存、指令重排。
可见性和有序性可以通过JVM提供的关键字volatile关键字解决。
导致原子性、有序性、可见性问题的本质 是由于提升CPU利用率导致的。例如:为了提升CPU利用率,设计了三级缓存、设计了缓存行的预读机制、在操作系统中设计了线程模型。
如何保证线程安全
1、互斥同步: synchronized 和 ReentrantLock
2、非阻塞同步: CAS, AtomicXXXX
3、无同步方案: 栈封闭,Thread Local,可重入代码
如何保证缓存一致性
1、总线锁定
处理器提供lock#信号,当其中一个核心在总线上输出lock#时,其它处理器的请求将会被阻塞,该处理器独占内存。把CPU和内存之间通信锁住了,在这期间其他处理器不能操作内存地址的数据。效率低下。
2、缓存锁定
现代CPU默认模式。通过缓存一致性协议来保证多核心的缓存副本的一致性问题,当其它核心更新了数据写回被核心1锁定的缓存行时,缓存将会失效。
MESI缓存一致性协议工作原理
缓存一致性协议(MESI):对单个缓存行进行加锁不会影响到内存中其他数据的读写。
M(modified):修改。该缓存行(cache line)有效,数据被修改过了,和内存中的数据不一致,数据只存在于当前缓存中
E(ex clu sive):独享。该缓存行(cache line)有效,数据和内存中的数据一致,只存在于当前缓存中
S(shared):共享。该缓存行(cache line)有效,数据和缓存中的数据一致,存在于对个缓存中
I(invalid): 无效。改缓存行(cache line)无效
MESI的大体流程就是:A线程首先读取数据到核心的高速缓存中,此时缓存行状态为独占。如果此时有B线程也读取了数据到另一个核心的高速缓存中,那么总线嗅探器就会将缓存行状态变为共享。如果此时A线程修改了值,那么嗅探器就将A的缓存行置为修改,B的缓存行将置为失效,B再进行运算时将会抛弃缓存行的副本,进而去主存中读取。
3、总线窥探机制(Bus Snooping)
CPU和内存通过总线(BUS)互通消息。
CPU感知其他CPU的行为(比如读、写某个缓存行)就是是通过嗅探(Snoop)线性中其他CPU发出的请求消息完成的,有时CPU也需要针对总线中的某些请求消息进行响应。这被称为”总线嗅探机制“。
CPU的写事务会被窥探器在总线上嗅探到,窥探器会检查该变量的副本是否在其他核心缓存上也有一份,如果有该副本,则窥探器会执行策略来保证副本的一致性,这个策略可以使刷新缓存或者让缓存失效,这取决于缓存一致性协议的实现。
总线风暴
当lock前缀指令十分频繁,或者volatile关键字使用得非常频繁,那么CPU的缓存一致性协议所带来的的嗅探机制会在总线上发生大量的总线事务,这些消息统称为总线一致性流量。如果大量使用volatile并且在多线程高并发环境下遇到CAS无锁自旋机制,那么CPU总线上的一致性流量将会激增,总线的带宽和速率是固定的,这一部分流量占满了总线,CPU的利用率自然下降,系统吞吐量自然下降,所以volatile或者lock前缀指令在高并发环境下尽量少用和CAS自旋操作这类的组合。
伪共享(缓存行共享)问题
如果多个核的线程在操作同一份缓存行中的不同变量数据,就会发生频繁的缓存失效,CPU频繁去主存中取值,造成性能问题。
避免伪共享的2中方案。
1、缓存行填充。
在Linux中,可以通过执行cat/proc/cpuinfo命令查看缓存行大小,默认是64字节,根据空间局部性原理,读取某个变量时,将其内存地址附近的变量一起读入缓存行中。
class Test {
volatile long x;
// 缓存行64字节填充
long l1,l2,l3,l4,l5,l6,l7;
volatile long y;
}
2、@Sum.misc.Contended 注解(JDK8) (-XX:RestrictContended)
注解可以使用在类上,也可以使用在变量上。
为什么wait、notify 和 notifyAll会在Object中
1、为什么没被定义到Thread 中
- 当线程执行到临界代码块的时候,线程只负责自己拿没拿到锁,没有责任和义务去通知其他线程。
- wait的时候会释放锁资源,如果一个线程获取到了多个不同的锁,这个时候如果调用 Thread.wait()方法,这个线程就不知道要释放哪个锁了
2、为什么被定义在Object中
- Object 里的wait、notify 和 notifyAll必须在synchronized 修饰的方法内执行
- synchronized 依赖底层monitor(监视器锁)实现锁住一个对象,由该对象所在的对象头里mark word 来标识,加锁状态本身就是存在对象头中,所以由对象自己来释放锁。
JMM
java内存模型。是一种规则,规定了线程和内存之间的关系。系统存在一个主内存,java 中所有变量都存在主内存中,对所有线程都是共享的。每个线程都有自己的工作内存,工作内存保存的是主内存中变量的拷贝,线程对所有变量的操作都是在工作内存中执行的。线程之间直接相互访问变量传递均需要通过主内存完成。
通过这组规则控制程序中各个变量在共享数据区和私有数据区的访问方式,JMM是围绕原子性,有序性,可见性展开的。
如何保证可见性
1、加锁。当共享变量或者某一段程序加了锁以后,线程要想访问它必须获得锁,如果获取不到只有阻塞等待,获得锁的线程执行完毕以后就会将其刷会主存中,所以加锁的方式是能够保证可见性的
2、加volatile关键字
3、让CPU高速缓存行失效
如何保证有序性
加volatile关键字。volatile修饰的变量,在读写操作的前后都会进行屏障(内存屏障)的插入来保证执行执行的顺序,不被编译器等优化器所重排序。
如何保证原子性
- 内置锁 - 显式锁 - CAS
内存屏障
功能:阻止屏障两边的指令重排、刷新处理器缓存。
1.LoadLoad:在load2读取之前,保证load1读取的数据全部读取完毕。
2.LoadStore:在load2写回之前,保证load1读取的数据全部读取完毕。
3.StoreStore:在store2写入之前,保证store1的写入对其操作可见。
4.StoreLoad:在store2写入之前,保证store1的写入对其操作可见。这个StoreLoad是一个万能的,它包含了前三种屏障的功能
as-if-serial
单线程程序不管内部怎么重排序(编译器和处理器为了提高并行度),程序执行的结果和串行顺序执行的结果一致。(编译器、runtime和处理器都必须遵守as-if-serial语义)。
happens-before
Java 内存模型中保证多线程可见性的机制。Happens-Before 是一种可见性模型,也就是说在多线程环境下,原本因为指令重排序的存在会导致数据的可见性问题,也就是 A 线程修改某个共享变量对 B 线程不可见。因此,JMM 通过 Happens-Before 关系向开发人员提供跨越线程的内存可见性保证。
Happens-Before 关系只是描述结果的可见性,并不表示指令执行的先后顺序,也就是说只要不对结果产生影响,仍然允许指令的重排序。
as-if-serial和happens-before 区别
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度
AQS
AbstractQueuedSynchronizer,抽象同步队列,是实现同步器的基础组件。是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、
CountDownLatch、Semaphore 等都用到了 AQS.从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。AQS主要由四部分组成,Node,state,CLH队列,conditionObject条件变量。
state
ASQ中维护的一个同步变量,getState方法是获取同步状态,setState 修改同步状态,compareAndSetState 以CAS的方式修改同步状态。资源的获取、释放是否成功都是由state决定的。
state的具体语义由具体的实现者去定义,同步的实现有不同的意思。
ReentrantLock 中state 为0表示没有线程拿到锁,state为1有线程持有锁。
ReentrantReadWriteLock 中state高16位代表读锁状态,低16位代表写锁状态
Semaphore 中state用来表示可用信号的个数
CountDownLatch 中 state用来表示计数器的值
CLH队列
CLH是AQS内部维护的FIFO(先进先出)双端双向队列(方便尾部节点插入),基于链表数据结构,当一个线程竞争资源失败,就会将等待资源的线程封装成一个Node节点,
通过CAS原子操作插入队列尾部,最终不同的Node节点连接组成了一个CLH队列。
CLH队列具有如下几个优点
先进先出保证了公平性
非阻塞的队列,通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁快速插入
采用了自旋锁思想,所以CLH也是一种基于链表的可扩展、高性能、公平的自旋锁
Node
ASQ的内部类,每个等待资源的线程都会封装成一个Node节点,最终组成CLH队列。
prev:前驱节点,next:后驱节点,thread:等待资源线程,waitStatus:节点等待状态,EXCLUSIVE:独占式标记,SHARE:共享式标记,nextWaiter:特殊标记
waitStatus:
SIGNAL(-1):后驱节点在等待当前节点唤醒。后驱节点入队时,会将前驱节点的状态更新为SIGNAL。‘
CANCELLED(1):表示当前节点已取消调度。
CONDITION(-2):表示节点在等待队列上,当其他线程调用了condition的signal方法后,CONDITION状态的节点将从等待队列转移到同步队列中,等待获取资源。
PROPAGATE(-3):共享模式下的节点,前驱节点不仅唤醒其后驱节点,同时也可能唤醒后驱的后驱节点。
0:新节点入队时默认状态
nextWaiter
Node对象在CLH队列时,表示共享或独占式标记
Node在条件队列(ConditionObject)时表示下个Node节点的指针
Node执行流程
线程获取资源失败,封装成Node节点从C L H队列尾部入队并阻塞线程,某线程释放资源时会把C L H队列首部Node节点关联(第一个已释放,原来的第二个变第一个)的线程唤醒,再次获取资源。
入队
线程获取资源失败后需要入队,调用addWaiter和enq方法
addWaiter方法,将当前线程封装成Node对象,第二步获取全局变量尾节点,判断尾节点是否为空,如果为空则调用enq方法,如果不为空,将全局变量尾节点设置为当前线程封装成node对象的前驱节点,
通过cas的方式将封装当前线程的node对象设置为全局的尾节点,如果设置成功,则将原来的尾节点的next属性设置为当前节点,并返回当前线程封装的node,当前线程封装的node的next为空。
如果cas设置全局尾节点失败,则调用enq方法。
enq
通过自旋,先判断尾节点是否为空,如果尾节点为空,通过cas的方式创建一个哨兵节点,head与tail都指向哨兵节点。
如果尾节点不为空,则当前节点的前驱节点为尾节点,通过cas的方式将当前节点设置尾节点。如果设置成功,则将原来的尾节点的后驱节点设置为当前节点。失败就继续循环
private Node addWaiter(Node mode) {
//根据当前线程创建节点,等待状态为0
Node node = new Node(Thread.currentThread(), mode);
// 获取尾节点
Node pred = tail;
if (pred != null) {
//如果尾节点不等于null,把当前节点的前驱节点指向尾节点
node.prev = pred;
//通过cas把尾节点指向当前节点
if (compareAndSetTail(pred, node)) {
//之前尾节点的下个节点指向当前节点
pred.next = node;
return node;
}
}
//如果添加失败或队列不存在,执行end函数
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) { //循环
//获取尾节点
Node t = tail;
if (t == null) {
//如果尾节点为空,创建哨兵节点,通过cas把头节点指向哨兵节点
if (compareAndSetHead(new Node()))
//cas成功,尾节点指向哨兵节点
tail = head;
} else {
//当前节点的前驱节点设指向之前尾节点
node.prev = t;
//cas设置把尾节点指向当前节点
if (compareAndSetTail(t, node)) {
//cas成功,之前尾节点的下个节点指向当前节点
t.next = node;
return t;
}
}
}
}
出队
CLH队列中的节点都是获取资源失败的线程,当持有资源的线程释放锁时,会将head.next(下一个节点)唤醒,如果唤醒节点获取资源成功,这个被唤醒的节点清空信息,并设置成罪行的哨兵节点,原来的头部节点出队。
调用的是acquireQueued方法
ConditionObject
条件变量,为了配合lock使用,类似于wait和notify配合synchronize使用一样,起到线程之间的通信作用。ConditionObject内部维护着一个单向条件队列。
条件队列只会把执行await的线程节点入队,并且加入条件队列的节点,不能在CLH队列,条件队列出队,会入队到CLH队列。
当线程执行了await方法,线程会阻塞,会被封装成Node添加到条件队列末端。其他线程执行signal方法,会将条件队列的头部节点转移到CLH队列中竞争资源。
ConditionObject 是单向队列,prev 和 next 为null。
AQS为什么设计成双向队列
首先,双向链表的特点是它有两个指针,一个指针指向前置节点,一个指针指向后继节点。
所以,双向链表可以支持 常量O(1) 时间复杂度的情况下找到前驱结点,基于这样的特点。
双向链表在插入和删除操作的时候,要比单向链表简单、高效。
对ReentrantLock的理解
介绍
ReentrantLock是基于AQS实现的一种可重入独占锁。ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock。
ReentrantLock是通过操作state来表示当前线程的状态,0表示没有线程拿到锁,大于等于1表示有线程持有锁。
ReentrantLock类核心Sync,Sync是一个抽象的内部类,继承自AbstractQueuedSynchronizer(AQS)。Sync有两个子类,NonfairSync(非公平锁)和FairSync(公平锁)
ReentrantLock无参的构造方法是new了一个NonfairSync(非公平锁),所以ReentrantLock默认是创建一个非公平锁,可以通过传入参数创建公平锁。
非公平锁:非公平锁是抢占模式,线程不会关注队列中是否存在其他线程,也不会遵守先来后到的原则,直接尝试获取锁
公平锁:公平锁相当于有一个线程等待队列,先进入队列的线程会先获得锁,按照 “FIFO(先进先出)” 的原则,对于每一个等待线程都是公平的。
原理
首先尝试让当前线程获取锁(调用tryAcquire(arg)方法),当前锁的状态为0:没有线程使用锁,直接把设置该锁对象被当前线程拥有。当前锁的状态>0:检查是否是重入锁。是重入锁就直接返回true,不是则返回false表示获取锁失败
如果获取锁失败(tryAcquire(arg)方法返回false),构造Node节点保存当前线程,并且把node插入到等待队列尾。注意这个入队方法会循环执行保证入队一定成功。
把当前线程park掉。在这之前如果当前线程是等待队列里唯一一个线程,会先尝试让当前线程再次获取锁;如果获取失败,就把它的前驱节点的waitStatus置为-1,然后park掉该线程。
加锁
//非公平锁。
final void lock() {
//当前线程设置state状态为1,成功则设置当前线程为独占线程。失败调用acquire方法。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//tryAcquire: 尝试获取锁。
//尝试获取锁失败后进入acquireQueued方法中。acquireQueued:获取
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//尝试获取非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//使用CAS的方式操作state,抢资源,抢到资源就将当前线程设置为独占线程,抢不到就返回false。
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//将当前线程追加到CLH队列的尾节点。
private Node addWaiter(Node node) {
//mode : static final Node EXCLUSIVE = null;
//将当前线程封装成一个独占的Node。
Node node = new Node(Thread.currentThread(), mode);
//双向队列尾节点。
Node pred = tail;
//如果尾节点不为空
if (pred != null) {
//设置当前线程Node的头节点的值为 上个节点的尾节点。
node.prev = pred;
//使用CAS的方式 将当前节点设置为双向队列的尾节点
if (compareAndSetTail(pred, node)) {
//原来的尾节点(当前线程节点的上个节点,的 next(下个节点) 设置为当前线程)
pred.next = node;
return node;
}
}
//如果尾节点为空 或者 使用CAS的方式设置当前双向队列的尾节点失败的时候。
enq(node);
//此时node已经追加到双向队列的对尾,将当前线程封装的Node 返回。
return node;
}
//使用自旋的方式设置双向队列。node:当前线程封装的Node对象。
private Node enq(final Node node) {
for (;;) {
//双向队列的尾节点。
Node t = tail;
//如果尾节点为空,代表当前CLH队列还没有Node。
if (t == null) { // Must initialize
//使用CAS的方式设置双向队列的头节点,为一个新建的Node。
if (compareAndSetHead(new Node()))
//尾节点也是一个新建的Node
tail = head;
} else {
//尾节点不为空,当前线程的Node的头节点指向 双向队列的尾节点。
node.prev = t;
//使用CAS的方式将当前线程封装Node设置为CLH队列的尾节点。
if (compareAndSetTail(t, node)) {
//原来的双向队列的尾节点(现在变成了倒数第二个)的下个节点指向当前线程封装的Node。
t.next = node;
return t;
}
}
}
}
//先执行 addWaiter方法,在判断是否需要执行 enq方法,最后执行acquireQueued。
//node:当前线程封装的Node,并且已经将当前线程追加到双向队列的对尾。arg:1。
//作用:当前节点是否被park(挂起)?没有,就将当前线程挂起。
final boolean acquireQueued(final Node node, int arg) {
//true:表示失败,需要执行出队的逻辑,false:表示当前线程抢占锁成功,不需要执行cancelAcquire方法
boolean failed = true;
try {
//当前线程是否被中断标识,true:执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 判断后的方法 selfInterrupt ,打断当前线程。
//false 为不执行。
boolean interrupted = false;
for (;;) {
//当前线程封装的Node的前驱节点
final Node p = node.predecessor();
//p == head。当前线程的前驱节点为头节点,说明当前线程是头结点的下一个节点。
//tryAcquire。当前线程尝试加锁,如果加锁成功,说明头结点已经释放锁了。失败,则当前线程任然被挂起。
if (p == head && tryAcquire(arg)) {
//抢到锁后,将当前线程前驱为头节点。同时将当前线程和当前节点的前驱节点置为空
setHead(node);
//当前线程的前置节点,已经释放锁了,需要和后面的队列断开。所以将上个节点的next设置为空,下次GC的时候能直接回收。
p.next = null; // help GC,没有对象引用了,通过可达性分析算法,可以将这个当成垃圾进行回收
//当前线程获取锁过程中没有异常,不需要执行cancelAcquire。
failed = false;
//返回当前线程的中断标识,
return interrupted;
}
//shouldParkAfterFailedAcquire:当前线程获取锁资源失败后,是否需要挂起。true:挂起,false:不需要挂起。
if (shouldParkAfterFailedAcquire(p, node) &&
//parkAndCheckInterrupt:基于Ussafe类的park方法挂起线程,当前线程开始阻塞。
parkAndCheckInterrupt())
//当前node对应的线程是被中断信号唤醒的。
interrupted = true;
}
} finally {
//想要执行cancelAcquire方法,failed必须等于true,但是上面的自旋必须要将failed设置为false才能跳出循环。
//另一种就是代码抛异常,可能会发生异常的地方有 parkAndCheckInterrupt() 方法,但是这个地方抛异常是当JVM内部出问题的时候才会抛出异常。
//cancelAcquire 被执行的概率很小。
if (failed)
cancelAcquire(node);
}
}
//pred:当前线程的前驱节点。node:当前线程封装的Node对象。
//shouldParkAfterFailedAcquire:作用是保证前驱节点的状态是 -1 ,才会返回true。状态不是-1,都返回false。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态。
int ws = pred.waitStatus;
//前驱节点的等待状态:SIGANL(-1):后驱节点在等待当前节点唤醒
if (ws == Node.SIGNAL)
return true;
//如果前驱节点的状态大于0,说明前驱节点已经失效了。
if (ws > 0) {
do {
//通过当前线程,找当前线程前驱节点的前驱节点,设置前驱节点的前驱节点为当前线程的前驱节点。。。
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//pred 已经不是方法传进来的那个前驱节点了,是传进来的前驱节点的前驱节点。
//将 这个前驱节点 的后驱节点设置为当前线程封装的node。
pred.next = node;
} else {
//小于等于0 ,但是不等于 -1。 小于 -1 是无效的节点。
//将 前驱节点 的状态使用CAS的方式 设置为 -1 。 因为这样才能唤醒 前驱节点的下一个节点。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//当前node取消排队。
private void cancelAcquire(Node node) {
// 当前节点为空,直接返回
if (node == null)
return;
//node 不为null的前提下执行下面的代码
// 将当前node 的线程置为空,当前线程不去排队了。
node.thread = null;
// 获取当前node的前驱节点
Node pred = node.prev;
//判断前驱节点的状态是否大于0,大于0 是失效节点。找前驱节点的前驱节点。
//使用while循环,判断 如果前驱节点的前驱节点还是失效节点,就继续向前找。
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取最近的有效的前驱节点的后驱(下一个)节点。
Node predNext = pred.next;
//将当前线程的状态设置为CANCELLED,失效。
node.waitStatus = Node.CANCELLED;
//如果当前node是尾节点,且 将 通过 while循环找到的那个前驱节点设置成功(因为上一步代码已经将当前节点置为失效节点,所以需要更新尾节点为当前node的前驱节点)。
//处理的是尾节点的逻辑。else 是 非尾节点的逻辑。
if (node == tail && compareAndSetTail(node, pred)) {
//用CAS的方式,将尾节点的后驱节点(下一个节点)设置为null。因为当前node已经是最后一个了,后面没有了。就不需要后驱节点了。
compareAndSetNext(pred, predNext, null);
} else {
//这块代码操作 头结点 和 头、尾之间的中间节点
int ws;
//如果前驱节点不是头结点,
if (pred != head &&
// 前驱节点的状态 是不是 SIGNAL (后驱节点在等待当前节点唤醒,说明当前节点是有效的)
((ws = pred.waitStatus) == Node.SIGNAL ||
//如果前驱节点的状态 <= 0,并且 设置前驱节点的状态为SIGNAL成功(前驱节点需要唤醒后面的节点,所以需要设置为SIGNAL -1 ),并且前驱节点线程不为空。
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//next : 当前node的后驱节点
Node next = node.next;
//如果下一个节点不为空,并且node的后驱节点的状态 <= 0(有效的).
if (next != null && next.waitStatus <= 0)
//使用CAS的方式将当前node 前驱节点的后驱节点设置为 next。因为当前node已经是失效状态了。
compareAndSetNext(pred, predNext, next);
} else {
//如果node是头结点,唤醒后驱节点。
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
公平锁和非公平锁的区别在于是否需要排队,公平锁需要的线程在一个先进先出的队列中,等到上个线程释放资源后,下个线程被唤醒,并持有当前锁。
final void lock() {
acquire(1);
}
/**
* Acquire : 获取。tryAcquire:尝试获取。公平锁。
*/
protected final boolean tryAcquire(int acquires) {
//当前线程。
final Thread current = Thread.currentThread();
//AQS状态。获取状态。
int c = getState();
//如果状态为0,说明没有线程持有锁。
if (c == 0) {
//hasQueuedPredecessors:判断当前线程是不是CLH队列被唤醒的线程,如果是执行下一个步骤
//compareAndSetState:使用CAS的方式将state的状态设置为1。表示当前线程持有锁。
//setExclusiveOwnerThread:设置当前线程为独占锁,返回成功。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程 是尺有所的线程
else if (current == getExclusiveOwnerThread()) {
//状态加1。
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//设置状态的值。将AQS中state属性设置为nextc。
setState(nextc);
return true;
}
//如果状态不为0,且当期线程不是持有锁的线程。返回false。
return false;
}
解锁
//arg:1。
public final boolean release(int arg) {
//尝试释放锁,判断释放是否成功。
if (tryRelease(arg)) {
Node h = head;
//如果头节点不为空,且头节点的状态不是0,则把头结点传到unparkSuccessor中取唤醒等待的资源(唤醒节点的后继节点(如果存在))
if (h != null && h.waitStatus != 0){
unparkSuccessor(h);
}
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//getState 获取当前锁的状态值,然后 减 1 。
int c = getState() - releases;
//如果当前线程不是获取到锁资源的线程,就抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
// 是否释放的标识
boolean free = false;
//c == 0 : 前提是 getState() 等于 1 ,状态是1(CANCELLED:取消调度) ,
if (c == 0) {
//返回值为true
free = true;
//独占线程置为空
setExclusiveOwnerThread(null);
}
//设置当前锁状态 为 0.表示 新节点入队时默认状态。(别的线程可以开始抢锁资源了)
setState(c);
return free;
}
//如果头结点存在可唤醒的后驱节点,则唤醒后驱节点。
private void unparkSuccessor(Node node) {
// 获取头结点的状态
int ws = node.waitStatus;
//如果头结点的状态 小于0 (表示需要唤醒),SIGNAL(-1),CONDITION(-2),PROPAGATE(-3)
if (ws < 0) {
// 将头节点的状态使用CAS的方式设置为 0 ,
compareAndSetWaitStatus(node, ws, 0);
}
//获取头结点的后驱节点
Node s = node.next;
//如果后驱节点为空,获取后驱节点的状态大于0(取消状态)
if (s == null || s.waitStatus > 0) {
//先将找到的这个后驱节点置为空
s = null;
//通过循环双向队列,从队尾开始找,一直向头找去,直到找到离头节点最近的可以被唤醒的节点,并将这个节点赋值到s上。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果后驱节点不为空,则调用unpark方法唤醒后驱节点的线程。
if (s != null) {
LockSupport.unpark(s.thread);
}
}
什么是可重入锁? 它用来解决什么问题?ReentrantLock中的state如何实现可重入加锁的?
1)可重入指同一个线程可以多次获取锁。
2)解决什么问题:主要就是为了避免死锁:假设a和b都需要做同步,a调用了b,如果锁不可重入,下面代码就会死锁
3)当一个线程获取锁的时候,会先判断state的状态是否为0,如果为0表示没有线程持有锁,可以尝试获取锁。如果不为0,则通过current == getExclusiveOwnerThread判断当前线程是否就是独占这个锁的线程,如果是当前线程持有锁,
则将state加1,表示重入返回。
ReentrantLock 是如何实现锁公平和非公平性的
公平,指的是竞争锁资源的线程,严格按照请求顺序来分配锁。
非公平,表示竞争锁资源的线程,允许插队来抢占锁资源。
ReentrantLock 默认采用了非公平锁的策略来实现锁的竞争逻辑。
ReentrantLock 内部使用了 AQS 来实现锁资源的竞争,没有竞争到锁资源的线程,会加入到 AQS 的同步队列(FIFO 的双向链表)里面。公平锁的实现方式就是,线程在竞争锁资源的时候判断AQS 同步队列里面有没有等待的线程。如果有,就加入到队列的尾部等待。而非公平锁的实现方式,就是不管队列里面有没有线程等待,它都会先去尝试抢占锁资源,如果抢不到,再加入到 AQS 同步队列等待。
之所以设计为非公平锁,是因为如果按照公平的策略去阻塞等待,同时 AQS 再把等待队列里面的线程唤醒,这里会涉及到内核态的切换,对性能的影响比较大。如果是非公平策略,当前线程正好在上一个线程释放锁的临界点抢占到了锁,就意味着这个线程不需要切换到内核态,虽然对原本应该要被唤醒的线程不公平,但是提升了锁竞争的性能。
Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理地使用公共资源。
Semaphore通过使用计数器来控制对共享资源的访问。 如果计数器大于0,则允许访问。 如果为0,则拒绝访问。 计数器所计数的是允许访问共享资源的许可。 因此,要访问资源,必须从信号量中授予线程许可。
CountDownLatch
CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。CountDownLatch是基于AbstractQueuedSynchronizer(AQS)实现的,其通过state作为计数器。构造CountDownLatch时初始化一个state,以后每调用countDown()方法一次,state减1;当state=0时,唤醒在await()上被挂起的线程。
synchronize
synchronize使用的三种方式
修饰普通函数,监视器锁便是对象实例(this)
修饰静态静态函数,监视器锁便是对象的Class实例(每个对象只有一个Class实例)
修饰代码块,监视器锁是指定对象实例
原理
任何对象都有一个监视器锁(monitor)关联,线程执行monitorenter指令时尝试获取monitor的所有权。如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程为monitor的所有者。如果线程已经占有该monitor,重新进入,则monitor的进入数加1,线程执行monitorexit,monitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit。如果其他线程已经占用monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
优化
锁粗化
互斥的临界区范围应该尽可能小,这样做的目的是为了使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,锁粗化就是将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
例如:for循环中有synchronize加锁,jvm会将加锁范围粗化到这一串操作的外面,使得这一串的操作只加一次锁就行
锁消除
jvm在即时编译时(JIT),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗。
例如:StringBuffer的append方法,加了synchronize。执行多个append,如果StringBuffer对象没有发生逃逸,则JIT会将synchronized 锁去掉。
方法逃逸:当一个对象在方法中被定以后,它可能被外部别的方法所引用。就是当成参数传到另一个方法中。称为方法逃逸。
全局变量赋值逃逸:定义一个全局变量,在A方法中中引用这个全局变量对象。称为全局变量赋值逃逸。
方法返回值逃逸: 一个有返回值的方法, 将这个对象返回出来。就是方法返回值逃逸
锁升级(锁膨胀)
在JVM中,每个对象都有一个对象头,synchronized用的锁是存在对象头中的。对象头由Mark World 、指向类的指针、以及数组长度三部分组成。Mark World 记录了对象的HashCode、分代年龄和锁标志位信息。Synchronized的升级顺序是 无锁–>偏向锁–>轻量级锁–>重量级锁,只会升级不会降级。
无锁
当一个对象被创建之后,还没有线程进入,这个时候对象处于无锁状态。存储的内容:对象的hashCode、对象分代年龄、是否是偏向锁(0:否)
锁标记:01
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。存储的内容:偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1:是)
锁标记:01
轻量级锁
当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。存储的内容:指向栈中锁记录的指针
锁标记:00
重量级锁
锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。存储的内容:指向重量级锁的指针
锁标记:10
volatile
保证可见性,和禁止指令重排。 无法保证原子性。
cpu会在保证happend-before的前提下,对指令进行重新排序,从而提高效率 。
为了实现禁止指令重排,JVM虚拟机提出了规范,内存屏障。LoadLoad,StoreLoad,StoreStore,LoadStroe
volatile缓存可见性实现原理
从两个层面实现:
1、JMM内存交互层面:volatile修饰的变量每次被线程访问时(读操作),都强制从主内存中重新读取该变量的值。修改后必须立即同步回主内存(写操作),使用时必须从主内存刷新,由此保证volatile修饰的变量的可见性。当一个变量被volatile修饰时,一旦主内存中的值发生改变,volatile会及时通知所有的线程,确保它们对这个变化可见。
2、底层实现(CPU层面):通过汇编的lock前缀指令,它会锁定变量缓存行区域并写回主内存(缓存锁定)。缓存一致性协议会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存,会导致其他处理器的缓存无效。
volatile禁止指令重排
编译器和处理器在生成指令序列时,会在volatile变量的读写操作前后插入一些内存屏障(Memory Barrier)指令,来禁止特定类型的编译器重排序和处理器重排序。
volatile与synchronized的区别
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。
synchronize:1、自动释放锁,可以使用unsafe的方法手动。2、悲观锁。3、java关键字,C++实现的。4、可重入锁。5、不可中断。6、只有一个等待队列
reentrantlock:1、手动加锁和解锁。2、采用cas+volatile管理线程,主动阻塞的乐观锁。3、JUC并发包实现的。4、可重入锁。5、可以调用lockInterruptibly方法中断线程。6、一把锁可以对应多个等待队列。
CAS
//执行函数:CAS(V,E,U),其包含3个参数:内存值V,旧预期值E,要修改的值U。
//① 当且仅当 预期值E 和 内存值V 相同时,才会将内存值修改为U并返回true;
//② 若V值和E值不同,则说明已经有其他线程做了更新,则当前线程不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。
Compare And Swap。比较和交换,如果传入的值是期望的值,就修改为新值。。线程修改主内存中的值A改为B。首先线程需要从主内存中拿值A, 预期修改为B。改的时候有个原子性操作,将取出来的预期值和主内存中的值比较是否相同。如果相同,则修改主内存中的值。
如果是并发操作,另一个线程将希望将主内存的值改为C,由于是原子操作(线程一个一个的执行),如果这个线程先将主内存中的值改为了C,则另一个线程在修改主内存的值比较发现不一样,则取消修改。
CAS会导致ABA问题。
如果失败次数过多,会占用CPU资源。synchronize中是使用自适应自旋锁,如果自旋次数过多,就挂起线程。
只能保证一个共享变量的原子操作(保证一个数据的安全)。AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
reentrantLock 内部是使用的CAS实现了锁的效果。
ABA问题
一个线程读取到A的时候,发生了阻塞。此时另一个线程也拿到这个值,对A进行了操作,把A修改成了B,然后又修改成了A 。这个时候前面那个线程拿到的值还是A,但其实A的属性或者状态已经发生了变化。
1、通过加版本号。JUC的包提供了AtomicStampeReference类,它通过控制变量的版本来保证CAS的正确性。
2、使用synchronize互斥锁。
3、AtomicMarkableReference 也可以用来解决ABA的问题。
Atomic 原子类的原理
Atomic 原子操作类是基于无锁 CAS + volatile 实现的,并且类中的所有方法都使用 final 修饰,进一步保证线程安全。而 CAS 算法的具体实现方式在于 Unsafe 类中,Unsafe 类的所有方法都是 native 修饰的,也就是说所有方法都是直接调用操作系统底层资源进行执行相应任务。Atomic 使用乐观策略,每次操作时都假设没有冲突发生,并采用 volatile 配合 CAS 去修改内存中的变量,如果失败则重试,直到成功为止。
AtomicInteger
AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。
- volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值
- CAS 保证数据更新的原子性。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 获取指针类Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
//下述变量value在AtomicInteger实例对象内的内存偏移量
private static final long valueOffset;
static {
try {
//通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
//通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//当前值加1,返回新值,底层CAS操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//当前值减1,返回新值,底层CAS操作
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
}
AtomicStampedReference
原子更新引用类型, 内部使用Pair来存储元素值及其版本号,可以解决ABA问题
内部使用Pair来存储元素值及其版本号。
Pair对象
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
如何使用AtomicStampedReference: https://blog.csdn.net/shuttlepro/article/details/127791275
public class AtomicStampedReferenceDemo {
// 关联更新引用对象,设置初始版本号为0
static AtomicStampedReference<Integer> atomicStampedReference =
new AtomicStampedReference<>(0, 0);
public static void main(String[] args) {
threadA();
threadB();
threadC();
}
public static void threadA() {
new Thread(() -> {
System.out.println("线程A 希望值从 0 -> 2,版本号从 0 -> 1");
System.out.println("线程A 修改前值: " + atomicStampedReference.getReference()
+ " 版本号: " + atomicStampedReference.getStamp());
try {
// 睡眠 1s 等待线程B、C 操作完
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(0, 2, 0, 1);
System.out.println("线程A 修改后值: " + atomicStampedReference.getReference()
+ " 版本号: " + atomicStampedReference.getStamp());
}).start();
}
public static void threadB() {
new Thread(() -> {
System.out.println("线程B 希望值从 0 -> 3,版本号从 0 -> 1");
System.out.println("线程B 修改前值: " + atomicStampedReference.getReference()
+ " 版本号: " + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(0, 3, 0, 1);
System.out.println("线程B 修改后值: " + atomicStampedReference.getReference()
+ " 版本号: " + atomicStampedReference.getStamp());
}).start();
}
public static void threadC() {
new Thread(() -> {
System.out.println("线程C 希望值从 3 -> 0,版本号从 1 -> 2");
System.out.println("线程C 修改前值: " + atomicStampedReference.getReference()
+ " 版本号: " + atomicStampedReference.getStamp());
try {
// 睡眠 200ms 等待线程B 操作完
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(3, 0, 1, 2);
System.out.println("线程C 修改后值: " + atomicStampedReference.getReference()
+ " 版本号: " + atomicStampedReference.getStamp());
}).start();
}
}
线程池
是一种管理和重用线程的机制,它包含一组可用于执行任务的线程。线程池的主要目的是避免创建和销毁线程的开销,以及控制并发线程的数量,以防止资源耗尽和性能下降。
使用线程池的原因
1、降低线程创建销毁的开销: 创建和销毁线程是昂贵的操作,线程池通过重用线程可以减少这些开销。
2、控制并发度: 线程池可以限制同时执行的线程数量,避免资源过度占用。
3、提高响应速度: 可以更快地启动执行任务,因为线程已经准备好。
4、提供线程管理和监控: 线程池提供了管理和监控线程执行的机制,便于调优和故障排除。线程池的核心参数
线程池的种类
//使用ThreadPoolExecutor创建一个线程池。
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,10,60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
// 使用线程池执行任务
for (int i = 0; i < 20; i++) {
threadPoolExecutor.execute(new Task(i));
}
// 关闭线程池
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()) {
// 等待所有任务完成
}
System.out.println("所有任务执行完毕");
}
static class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("执行任务: " + taskId);
//任务
}
}
SingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
FixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
CachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
ScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池的参数
核心线程数:int corePoolSize
最大线程数:int maximumPoolSize。最大线程有存活时间。
生存时间:long keepAliveTime
生存时间单位:TimeUnit unit
工作队列:workQueue
线程工厂:ThreadFactory threadFactory 创建线程。
拒绝策略:RejectedExecutionHandler handler
线程池执行流程或者原理
1、提交任务到线程池,让线程池中的线程去执行任务。
2、判断如果存在核心线程,直接执行。如果没有空闲的核心线程,尝试创建核心线程,去执行任务。
3、如果当前线程池中线程已经达到了核心线程数量,则将任务放到阻塞队列中排队,等待核心线程执行完其他线程在来执行。
4、如果任务队列满了,则构建最大线程数量。
5、如果最大线程也已经构建满了,执行拒绝策略。
线程池都有哪几种工作队列
1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
线程工厂
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
//创建一个线程
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
线程池拒绝策略
AbortPolicy:直接抛异常。
DiscardPolicy:什么都没做,
DiscardOldestPolicy:丢掉队列中排队时间最久的
CallerRunsPolicy:调用者处理,造成调
自定义拒绝策略
实现RejectedExecutionHandler 接口,重写rejectedExecution方法。可以将任务放到另一个队列中等待后续处理
//自定义拒绝策略,任务放到队列中等待
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
private final BlockingQueue<Runnable> waitingTasks;
public CustomRejectedExecutionHandler(BlockingQueue<Runnable> waitingTasks) {
this.waitingTasks = waitingTasks;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
waitingTasks.put(r); // 将任务放入等待队列
System.out.println("Task rejected, put into waiting queue.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
import java.util.concurrent.*;
public class CustomThreadPoolDemo {
private static final int THREAD_POOL_CORE_SIZE = 5;
private static final int THREAD_POOL_MAX_SIZE = 10;
private static final long THREAD_POOL_KEEP_ALIVE_TIME = 60L;
private static final BlockingQueue<Runnable> TASK_QUEUE = new LinkedBlockingQueue<>(100);
private static final BlockingQueue<Runnable> WAITING_TASK_QUEUE = new LinkedBlockingQueue<>();
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor( ...
new CustomRejectedExecutionHandler(WAITING_TASK_QUEUE) // 使用自定义拒绝策略
);
// 提交一些任务到线程池
for (int i = 0; i < 150; i++) {
int finalI = i;
executor.submit(() -> {
System.out.println("Executing task: " + finalI);
try {
Thread.sleep(1000); // 模拟耗时任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 演示如何从等待队列中取出任务并重新提交到线程池
executor.shutdown(); // 关闭线程池,不再接受新任务
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 尝试停止所有正在执行的任务,停止处理正在等待的任务,并返回等待执行的任务列表
}
} catch (InterruptedException ie) {
executor.shutdownNow(); // (Re-)Cancel if current thread also interrupted
}
// 从等待队列中取出任务并重新提交到线程池(这里仅作为示例,实际情况可能需要更复杂的逻辑)
Runnable task;
while ((task = WAITING_TASK_QUEUE.poll()) != null) {
// 重新提交任务到线程池(这里假设线程池已经重新初始化或能够处理新任务)
// 注意:这里只是一个示例,实际中线程池可能已关闭,需要相应处理
executor.execute(task);
}
}
}
excute 和 submit 的区别
1、提交任务类型:submit 允许提交Callable和Runnable类型的任务。Callable类型的任务可以返回执行结果,而Runnable类型的任务则没有返回值。excute 只接受Runnable类型的任务,它没有返回值。
2、返回值:submit 会返回一个Future对象,这个对象可以用于检索任务的执行结果(如果任务类型是Callable)或取消任务。excute:没有返回值
3、异常处理:submit 方法 当任务执行过程中抛出异常时,这个异常会被包装在一个ExecutionException中并重新抛出。excute:会重新抛出任务执行过程中抛出的异常,因此开发人员需要自己处理异常
submit取消任务
submit方法提交的任务可以通过返回的Future对象来取消。但是,要成功取消一个任务,需要满足一些条件:
1、任务必须尚未开始执行。一旦任务已经开始执行,就不能再取消它。
2、任务必须能够响应中断。这意味着任务中的代码必须检查中断状态,并在适当的时候退出。
使用Future.cancel取消任务。cancel方法的参数是一个布尔值,表示如果任务尚未开始,是否应该中断它。如果任务已经开始执行,则无论此参数为何值,cancel方法都会返回false,并且任务将继续执行直到完成。如果任务成功被取消,cancel方法返回true,并且当你尝试通过future.get()获取结果时,会抛出CancellationException。
线程池的状态
RUNNING:线程池正在工作,可以处理提交的任务
SHUTDOWN:调用线程池的shutDown方法,从running 到 shutdown。不接收新的任务,会处理现有的任务
STOP:需要调用shutdownNow方法,从running 到 stop。不接收新的任务,中断正在处理的任务。不管工作队列任务,
TIDYING:过渡状态,会从shutdown 和 stop 转到tidying。工作队列为空,工作线程为空会转为过渡状态,马上就要停止了,但是还没停。
TERMINATED:当线程池达到了tidying状态后,源码中就会自动调用terminated,进入到TERMINATED状态。
线程池的回收
线程池里面分为核心线程和非核心线程。由于非核心线程是为了解决任务过多的时候临时增加的,所以当任务处理完成后,工作线程处于空闲状态的时候,就需要回收。因为所有工作线程都是从阻塞队列中去获取要执行的任务,所以只要在一定时间内,阻塞队列没有任何可以处理的任务,那这个线程就可以结束了。这个功能是通过阻塞队列里面的 poll 方法来完成的。默认情况下,线程池只会回收非核心线程,如果希望核心线程也要回收,可以设置 allowCoreThreadTimeOut 这个属性为 true,一般情况下我们不会去回收核心线程。因为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时候是处于阻塞状态并没有占用 CPU 资源。
什么是工作线程
线程池中的工作线程是用worker对象表示的。Worker :线程池中的一个内部类。继承了AQS,实现了Runnable。线程池执行任务实际就是调用了Worker类中的run方法内部的runWorker方法。继承AQS是为了判断当前工作线程是否可以被打断。
addWorker(Runnable,true/false)。Runnable 具体执行任务,true:添加的是核心线程,false:添加的是最大线程。
存储在了线程池的一个HashSet中。
线程复用
因为线程本身并不是一个受控的技术,线程的生命周期是由任务运行的状态决定的,无法人为的控制。为了实现线程的复用,线程里面会用到阻塞队列,就是线程池中的工作线程一直处于运行状态,它会从阻塞队列中获取待执行的任务,一旦队列空了,那么这个工作线程就会被阻塞,直达下次有任务进来。工作线程是根据任务的情况实现阻塞和唤醒,从而达到线程复用。
如何在线程池执行任务前后做额外处理
前置钩子函数:beforeExecute(wt, task);
后置钩子函数:afterExecute(task, thrown);
beforeExecute 和 afterExecute 方法中没有做任何操作,需要自己手动实现。通过继承线程池,重写beforeExecute 和 afterExecute 方法。可以再任务执行之前和执行之后做一些操作,类似切面的操作。
如何合理的分配线程池的大小
没有固定数。
IO密集型:更多的时候是线程在等待响应。线程数多一些,cpu内核数 * 2
CPU密集型:更多的时候是CPU在做计算,一直在工作。线程数少点,内核数 + 1
线程池如何知道一个线程的任务已经执行完成
线程池通过Future或FutureTask对象来跟踪线程任务的执行状态,从而知道一个线程的任务是否已经执行完成。
当你向线程池提交一个任务时,线程池会返回一个Future或FutureTask对象。这个对象代表了异步计算的结果。
fail-safe机制与 fail-fast机制
fail-safe 和 fail-fast,是多线程并发操作集合时的一种失败处理机制。
fail-safe:表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出 ConcurrentModificationException。是由于采用安全失败机制的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。例如:CopyOnWriteArrayList,ConcerrentHashMap。
fail-fast:表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出 ConcurrentModificationException 异常,从而导致遍历失败。
例如:定义一个 Map 集合,使用 Iterator 迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生 fail-fast。
Future
Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果等操作。存储执行的将来才会产生的结果。带返回值的异步多线程方法,future.get() 方法是阻塞的,知道拿到返回值才会继续向下执行
Callable<String> callable = new Callable(){
@Override
public String call() throw Exception{
return "...";
}
};
ExexutorsService service = Exexutors.newCachedThreadPool();
Future future = service.submit(callable);
sout(future.get());//线程阻塞的,等拿到返回值才会继续向下执行
service.shutdown();
FutureTask
FutureTask实现了RunnableFuture接口,RunnableFuture同时也是Future和Runnable的实现类。主要用于异步执行任务,并且可以方便地获取任务执行的结果。
FutureTask允许你将一个任务包装为一个Future,然后提交给线程池异步执行。这意味着你可以继续执行其他任务或阻塞等待任务的完成,而不需要等待任务完成后再继续执行。
通过调用get 方法可以获取任务的执行结果,如果线程没有执行完,任务是阻塞的。可以通过cancel方法取消尚未执行的任务,FutureTask是线程安全的。
FutureTask<Integer> futuerTask = new FutureTask<>(() ->{
TimeUnit.MILLISECONDS.sleep(1000);
return 1000;
});
new Thread(futuerTask).start();
sout(futuerTask.get());//线程阻塞的,等拿到返回值才会继续向下执行
CompletableFuture
对于一堆任务的管理。多个异步任务执行操作,可以使用CompletableFuture管理这些异步任务,可以使用CompletableFuture的allof、anyof方法对多个异步任务的返回值同事获取。
CompletableFuture completableFuture1 = CompletableFuture.supplyAsync(() -> task1());
CompletableFuture completableFuture2 = CompletableFuture.supplyAsync(() -> task2());
CompletableFuture completableFuture3 = CompletableFuture.supplyAsync(() -> task3());
//使用allof获取三个任务的返回值
CompletableFuture.allof(completableFuture1,completableFuture2,completableFuture3).join();
CompletableFuture 提供了 5 种不同的方式,把多个异步任务组成一个具有先后关系的处理链,然后基于事件驱动任务链的执行。
第一种,thenCombine,把两个任务组合在一起,当两个任务都执行结束以后触发事件回调。
第二种,thenCompose,把两个任务组合在一起,这两个任务串行执行,也就是第一个任务执行完以后自动触发执行第二个任务。
第三种,thenAccept,第一个任务执行结束后触发第二个任务,并且第一个任务的执行结果作为第二个任务的参数,这个方法是纯粹接受上一个任务的结果,不
返回新的计算值。
第四种,thenApply,和 thenAccept 一样,但是它有返回值。
第五种,thenRun,就是第一个任务执行完成后触发执行一个实现了 Runnable接口的任务。
Queue
BlockingQueue
阻塞操作意味着当队列为空时,从队列中获取元素的操作会阻塞,直到队列中有可用元素为止;当队列已满时,向队列中添加元素的操作会阻塞,直到队列有空闲位置为止。通过使用BlockingQueue,可以实现线程之间的安全通信和协调。
put:put方法用于将元素放入队列中。如果队列已满,put()方法会阻塞当前线程,直到队列有空闲位置可放入元素。
take:take方法用于从队列中取出元素。如果队列为空,take()方法会阻塞当前线程,直到队列中有元素可供取出。
ArrayBlockingQueue
是先进先出(FIFO)的顺序队列(栈是先进后出),即最先插入的元素会最先被取出。它在内部使用一个固定大小的数组来存储元素。ArrayBlockingQueue创建时需要指定队列的容量大小,一旦达到容量上限,后续的插入操作将被阻塞,直到队列中的元素被消费或者被移除。如果队列为空,获取操作会被阻塞。
ArrayBlockingQueue支持公平性策略,即按照线程的到达顺序来处理元素。当公平性设置为true时,线程会按照插入的顺序进行获取元素;当公平性设置为false时,线程获取元素的顺序是不确定的。
ArrayBlockingQueue是线程安全的,多个线程可以同时对队列进行操作。它内部使用了锁和条件变量来保证线程安全性。
ArrayBlockingQueue的插入和获取操作可以设置一个超时时间,如果在指定的时间内仍然无法完成操作,那么操作将返回特定的结果。
//入队
public void put(E e) throws InterruptedException {
//针对元素非空校验
checkNotNull(e);
//获取一把独占锁,支持中断
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//当队列中元素数量等于数组长度(队列满了),调用notFull.await()将生产者线程阻塞
while (count == items.length)
notFull.await();
//如果队里没有满,需要调用enqueue入队
enqueue(e);
} finally {
//最后调用lock.unlock()唤醒队里的消费者线程
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
//将当前要添加的元素放入数组(putIndex指向的是下一个索引位置,所以可以直接定位)
final Object[] items = this.items;
items[putIndex] = x;
//如果++putIndex正好等于数组长度将putIndex置为0(环形数组)
if (++putIndex == items.length)
putIndex = 0;
//元素个数+1
count++;
//准备唤醒从阻塞队列中获取元素的消费者线程notEmpty.signal()
notEmpty.signal();
}
//出队
public E take() throws InterruptedException {
//获取一把独占锁(注意取与放操作是互斥的)
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列是空的,调用notEmpty.await()阻塞消费者线程
while (count == 0)
notEmpty.await();
//如果不是空的 ,调用dequeue方法,进行出队,将出队位置元素置null
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
//如果takeIndex移动到数组末尾,则进行重置takeIndex = 0
if (++takeIndex == items.length)
takeIndex = 0;
//数组中的元素个数-1,count–
count--;
if (itrs != null)
itrs.elementDequeued();
//调用notFull.signal()唤醒生产者线程,因为此时队列已经有空位置了
notFull.signal();
return x;
}
LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表结构的阻塞队列,它按照FIFO(先进先出)的原则对元素进行排序。新元素会被插入到队列的尾部,而队列的获取操作则是从队列的头部获取元素。LinkedBlockingQueue的一个主要特点是其容量是可选的,如果不指定队列的容量,其默认大小将等于Integer.MAX_VALUE。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
//构造Node结点,使用put锁加锁,保证入队线程安全
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
//如果count.get() == capacity队列容量满了,生产者线程调用notFull.await()阻塞
while (count.get() == capacity) {
notFull.await();
}
//否则调用enqueue,将node结点入队,插入链表尾部
enqueue(node);
//count.getAndIncrement(),元素个数+1
c = count.getAndIncrement();
//如果count+1小于队列容量capacity,准备唤醒生产者线程(队列没有满)
if (c + 1 < capacity)
notFull.signal();
} finally {
//putLock.unlock()释放锁进行唤醒
putLock.unlock();
}
//这里代表队列有一个元素(count.getAndIncrement()会返回旧值,实际1)
if (c == 0)
//signalNotEmpty() 唤醒消费者线程
signalNotEmpty();
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
//获取一把take锁,take锁加锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//如果count.get() == 0,说明队列是空的,执行notEmpty.await() 需要阻塞消费者线程
while (count.get() == 0) {
notEmpty.await();
}
//执行dequeue方法进行出队,头结点出队
x = dequeue();
//执行count.getAndDecrement(),将队列元素个数-1
//如果队列还剩一个空闲位置,尝试唤醒一个put的等待线程count.getAndDecrement()返回的是旧值。
c = count.getAndDecrement();
//如果c > 1,队列有元素,准备唤醒消费者线程
if (c > 1)
notEmpty.signal();
} finally {
//调用takeLock.unlock(),释放锁,进行唤醒
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
DelayQueue
无界队列,用于存储具有过期时间的元素。在DelayQueue中,元素必须实现Delayed接口,该接口定义了元素的过期时间以及与其他元素的比较方法。DelayQueue是一个按照元素的过期时间进行排序的队列,即最先过期的元素会被放在队列的头部,最晚过期的元素会被放在队列的尾部。当从DelayQueue中获取元素时,只有在元素的过期时间到达后,该元素才能被取出。
优点:安全,高效,提供可靠的任务延迟调度机制。
缺点:占内存,不能改元素的延迟时间
SynchronousQueue
SynchronousQueue是一种用于线程间同步通信的队列。它的特点是在插入元素时,如果没有其他线程正在等待移除元素,插入操作将会阻塞,直到有其他线程移除该元素;而在移除元素时,如果没有其他线程正在等待插入元素,移除操作也会被阻塞,直到有其他线程插入元素。
它并不存储元素。相反,它只是在插入和移除操作之间传递元素。这使得SynchronousQueue非常适合于一些需要线程间直接传递数据的场景,例如线程池的任务分发等。
LinkedBlockingDeque
基于链表的双向阻塞队列,它可以在队列的两端进行插入和删除操作,并且支持线程安全的并发访问。是线程安全的,多个线程可以同时对队列进行插入和删除操作,而不需要额外的同步机制。可以指定容量限制,即队列中可以存放的最大元素数量。当队列达到容量限制时,后续的插入操作将会被阻塞,直到有空间可用。支持双向操作,可以在队列的两端进行插入和删除操作。
缺点:占内存,可能导致内存溢出
ThreadLocal
本质是一个Map,它提供了线程本地变量。这些变量不同于它们的正常变量,因为每一个访问这个变量的线程都有其自己独立初始化的变量副本。ThreadLocal实例通常是私有的静态字段,在类中用作线程间的一个共享变量,但是每个使用该变量的线程都会有一个该变量副本,这就达到了线程间数据隔离的目的。
ThreadLocal的主要作用是解决多线程并发时共享变量的问题。多个线程同时修改同一个共享变量,可能会导致数据不一致。通过使用ThreadLocal,每个线程可以拥有该变量的一个独立副本,避免了多线程并发时的数据共享问题,从而提高了线程安全性。
原理
ThreadLocal实例中的数据是通过ThreadLocalMap来保存的(当前线程的map,这个map的中K,V只能这个线程使用,别的线程是无法访问这个线程的东西的),它们专门用来存储当前线程共享变量的副本,后续线程对于共享变量的操作,都是从这个ThreadLocalMap里面进行变更,不会影响全局共享变量的值。
当一个线程调用set()方法设置ThreadLocal实例中的数据时,会将当前ThreadLocal实例作为key,将要设置的值作为value存入到ThreadLocalMap中。当一个线程调用get()方法获取ThreadLocal实例中的数据时,会先从ThreadLocalMap中查找当前线程对应的ThreadLocal实例,然后再从该实例中获取数据。
spring 的事务用到了ThreadLocal。
为什么设置为弱引用
Map的key设置成弱引用。设计成弱引用的话,在每次GC时,发现没有其他强引用指向ThreadLocal了,便会将其回收。
在哪设置为弱引用
ThreadLocalMap的Entry对象集成了WeakReference。
ThreadLocal内存泄漏
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。
强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
软引用
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用
弱引用
无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用
虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。
锁
什么是死锁
死锁:一组互相竞争资源的线程因为互相等待导致永久阻塞的现象,就是死锁。
发生死锁的原因
- 互斥条件:共享资源x和y只能被一个线程占用,
- 占有且等待:线程t1已经取得了共享资源x,在等待共享资源y的时候,不释放共享资源x。
- 不可抢占:其他线程不能强行抢占线程t1占有的资源
- 循环等待:线程t1等待线程t2占有的资源,线程t2等待线程t1占有的资源就是循环等待。
如何避免死锁
发生死锁的原因是需要同时满足这四个条件,我们只需要打破其中一个就能避免死锁。在这四个条件中,第一个互斥条件是无法被打破的,因为锁本身就是通过互斥来解决线程安全的问题的。
对于占有且等待:可以一次性获取全部资源,这样就不存在等待了。
对于不可抢占:占有部分资源的线程,进一步申请其他资源的时候,如果申请不到可以主动释放它占有的资源,这样就打破了不可抢占的条件。
对于循环等待:可以按顺序申请资源来进行预防。按序申请是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,在申请资源序号大的,这样线性化后自然就不存在循环等待了。