Java集合+并发(部分)

Java集合

Java集合类的继承结构和各自的适用情况

Collection

​ — List

​ — ArrayList:动态数组

​ — LinkedList:底层是双向链表,应用于Queue接口可以用于实现队列,应用于Deque接口可以用于实现栈

​ — Vector:线程安全的动态数组,但由于性能较低已被废弃(其下有Stack)

​ — Set

​ — HashSet:哈希集合,底层数据结构是哈希表

​ — LinkedHashSet:底层数据结构是哈希表+链表,可以保证先进先出

​ — TreeSet:底层数据结构是红黑树

​ — Queue

​ — PriorityQueue:优先队列

​ — Deque:Double Ended Queue,双端队列,可以在队列两端进行插入、删除操作,因此既可以作为队列,也可以作为栈

​ — ArrayDeque

​ — LinkedList

Map

​ — HashMap(哈希表)

List适用于需要顺序存储的情况

Set适用于需要存储无顺序、不重复的情况

Queue适用于队列存储

Map适用于存储键值对

List

1. ArrayList底层实现

ArrayList底层是Object数组。

ArrayList实现了以下接口:

List:是一个顺序列表

RandomAccess:支持快速随机访问

Cloneable:支持深拷贝、浅拷贝

Serializable:支持序列化

观察ArrayList源码,默认长度是10,默认一开始是空数组,当插入第一个元素时,数组长度变为默认的10。当继续插入元素时,如果数组中元素达到长度极限,调用grow方法扩容数组,如果没有达到极限,直接插入。

观察ArrayList的构造方法,有三种。一种是什么都不传入,默认是空数组。第二种是传入容量,则创建对应大小的数组。第三种是传入Collection接口下的集合,将其他集合类型转为ArrayList。

2. ArrayList的扩容原理

当调用add方法添加元素时,首先调用ensureCapacityInternal方法,判断当前元素数量和当前数组长度的关系。如果数组够用,继续插入元素。如果数组不够用,调用grow方法进行数组扩容。

grow方法:

使用位运算将数组容量扩充为原来的1.5倍,检查是否满足需求,如果满足,将新容量作为数组容量,如果不满足,将最小需求作为数组容量。调用Arrays.copyOf方法创建一个拥有新容量的数组,并把原数组复制过去。

3. ArrayList与Vector的区别

ArrayList是线程不安全的,Vector内部调用了synchronized关键字,是线程安全的

4. ArrayList插入、删除元素的时间复杂度是多少

头部插入元素:所有元素右移,时间复杂度O(n)

尾部插入元素:如果不需要扩容,时间复杂度O(1);如果需要扩容,需要复制元素到新数组,时间复杂度O(n)

某一位置插入元素:右边所有元素右移,时间复杂度O(n)

头部删除元素:所有元素左移,时间复杂度O(n)

尾部删除元素:O(1)

某一位置删除元素:右边所有元素右移,时间复杂度O(n)

5. ArrayList是否实现了RandomAccess接口

RandomAccess是一个标记接口,表示该类支持快速随机访问(也就是通过索引在O(1)时间内快速访问某元素)。

ArrayList可以通过索引访问,是支持RandomAccess接口的。

6. LinkedList的底层原理是什么

LinkedList的底层原理是双向链表,内部的链表节点中存储:本节点值data、前一个节点指针prev、后一个节点指针next。

LinkedList实现的接口:

List:支持顺序存储

Deque:支持双端队列,由此可以作为栈或队列的底层数组结构

Cloneable:支持拷贝

Serializable:支持序列化

7. LinkedList插入、删除元素的时间复杂度

头部插入/删除:O(1)

尾部插入/删除:O(1)

指定位置插入、删除:需要遍历到指定位置,O(n)

8. LinkedList如何作为队列和栈

作为队列:

Queue<Integer> queue = new LinkedList();
queue.offer(1); // 添加元素
queue.poll();  // 弹出队首元素
queue.peek();  // 获取队首元素但不弹出

作为栈:

Deque<Integer> stack = new LinkedList();
stack.push(1);  // 入栈
stack.pop();  // 出栈
stack.peek();  // 获取栈顶元素但不弹出

另外,在回溯问题中,会使用LinkedList存储path,在添加入path时使用path.add(),在遍历完当前path后使用removeLast()方法撤销。最后,在将path添加到List<> ans中时,将其转为ArrayList,ans.add(new ArrayList(path));

9. CopyOnWriteArrayList保证线程安全的原理

ArrayList是线程不安全的,Vector是线程安全的,但Vector实现线程安全的方式是读写都加synchronized,性能较低,目前已被废弃。

现在实现线程安全的动态数组的是CopyOnWriteArrayList。

CopyOnWriteArrayList保证线程安全的原理是读写锁,即读不加锁,写加锁,这样保证了读操作的性能。

在此基础上,使用写时复制(CopyOnWrite, COW)策略,进一步保证了写操作也不会影响读操作。具体来说,当需要进行写操作时,不会直接修改原数组,而是创建一份副本,在副本数组上进行修改,修改完后再将数组复制回去。这样保证了写操作不会影响读操作。

但这样做也有一些缺点:

1)写操作时间、空间开销,每次写操作都要复制一份数组,改后再复制回去

2)可能导致数据一致性问题

Set

1. HashSet底层原理

HashSet底层是用HashMap实现的,具体来说,HashSet的元素实际上存储在HashMap的key中,而value存储一个固定的Object对象。

HashSet的特点是无序性和不可重复性。

无序性指的是:HashSet不按元素插入顺序进行存储,而是根据其哈希值进行存储。

不可重复性是指:HashSet内部不允许出现重复元素,因此经常被用来去重。

2. HashSet插入、删除元素的时间复杂度

HashSet插入、删除元素的时间复杂度都是O(1)。

3. HashSet、LinkedHashSet、TreeSet的适用场景分别是什么

HashSet适用于无序、无重复元素场景。

LinkedHashSet适用于需要先进先出的场景。

TreeSet适用于需要自定义排序的场景。

Queue

1. Queue和Deque的区别

Queue是单端队列,只能队尾插入元素,队首删除元素。

Deque是双端队列,两端都能插入、删除。

2. ArrayQueue和LinkedList区别

ArrayQueue和LinkedList都实现了Deque接口,都可以实现双端队列的功能。

但二者的底层数据结构不一样。ArrayQueue是基于动态数组和双指针实现的,LinkedList是基于双向链表实现的。

3. PriorityQueue底层数据结构是什么,插入删除的时间复杂度是多少

底层数据结构是二叉堆,即父节点的值一定小于或等于子节点的值。PriorityQueue poll出的堆顶元素一定是最小的。

插入、删除元素的时间复杂度是O(logn)

遍历priorityqueue需要先将其放入ArrayList中,然后使用for-each遍历:

List<Integer> pq_list = new ArrayList(pq);
for(Integer i : pq_list) {System.out.println(i);
}

自定义排序:

PriorityQueue<Integer> pq = new PriorityQueue<Integer>(new Comparable<Integer>(){@Overridepublic int compare(Integer i1, Integer i2) {return i2 - i1;  // 大顶堆}
});

4. ArrayBlockingQueue是什么

ArrayBlockingQueue是阻塞队列,主要用于生产者和消费者之间的通信。

即生产者线程向其中添加元素,消费者线程从其中提取元素。

可以分别实现阻塞式和非阻塞式的put和take。

阻塞式:put和take,当队列满时,阻塞put操作,直至队列有空余。当队列空时,阻塞take操作,直至队列有元素。

非阻塞式:offer和poll,当队列满时,不阻塞,而是offer方法直接返回false,添加失败;当队列空时,不阻塞,而是poll直接返回null,获取到的是空。

使用BlockingQueue实现生产者—消费者模式:

class Producer extends Thread {private BlockingQueue<String> bq;public Producer(BlockingQueue<String> bq) {this.bq = bq;}@Overridepublic void run() {for (int i = 1; i <= 10; i++) {String new_product = "Product " + i;try {bq.put(new_product);System.out.println("Produced: " + new_product);} catch (InterruptedException e) {e.printStackTrace();}}}
}class Consumer extends Thread {private BlockingQueue<String> bq;public Consumer(BlockingQueue<String> bq) {this.bq = bq;}@Overridepublic void run() {for (int i = 1; i <= 10; i++) {try {String product = bq.take();System.out.println("Consumed: " + product);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class Main {public static void main(String[] args) {BlockingQueue<String> bq = new ArrayBlockingQueue(10);Producer producer = new Producer(bq);Consumer consumer = new Consumer(bq);producer.start();consumer.start();}
}

5. DelayQueue是什么

DelayQueue是延迟队列,可以用于实现延时任务,例如“订单下单15分钟未支付自动取消”功能。

DelayQueue中的元素必须实现Delayed接口,并重写getDelay()方法,用于计算是否到期。DelayQueue底层是用PriorityQueue实现的,默认按照到期时间升序排列,当getDealy()方法返回值小于0时,移除队列。

Map

1. HashMap底层原理

HashMap底层原理是哈希表+链表,即元素首先根据key的hashcode计算哈希值,存储在对应哈希表中。如果出现哈希碰撞,使用拉链法解决。当链表长度大于8时,会将链表转为红黑树,以提高搜索效率。(当整个哈希表长度不超过64时,不会转为红黑树,而是进行数组扩容)

HashMap添加、删除元素的时间复杂度:

如果没有出现哈希冲突,是O(1)。

如果出现了哈希冲突,且哈希冲突用链表解决,是O(n)

如果出现了哈希冲突,且哈希冲突用红黑树解决,是O(logn)

2. HashMap数组扩容的原理

HashMap默认长度是16,默认负载因子是0.75,也就是说,75%的数组有数据,25%的数据无数据,这是为了尽量减少哈希碰撞。当数组元素数量>数组容量*负载因子时,会对hashmap的数组进行扩容。

数组扩容的原理:

默认将数组扩容为原来的2倍。扩容方式是:根据新的长度重新计算所有元素的哈希值,将原来的元素复制到新的哈希位置。

3. HashMap数组长度为什么是2的幂次方/为什么扩容是乘以2

1)可以用位运算进行取余操作来获得哈希位置,效率更高

2)可以保证哈希值分布比较均匀

3)扩容时只需检查哈希值高位来判断是否变化位置

4. HashMap插入新元素的流程

首先,判断哈希数组是否为空,如果为空,先扩容到默认值16.

其次,计算哈希值。

如果对应哈希位置没有元素,直接插入在这一位置。

如果对应哈希位置有元素,判断这一元素key与要插入的key是否相同,如果相同直接修改对应value。

如果不相同,判断这一位置上的元素的红黑树节点还是链表节点。

如果是红黑树节点,调用putTreeVal放入树中。

如果是链表节点,遍历到链表,如果有相同的key,直接修改对应value,如果没有,遍历到链表末尾后在末尾插入新元素。在链表末尾插入元素后,判断是否需求将链表转为红黑树。

插入完成后,根据数组长度和负载因子判断是否需要扩容。

5. JDK1.7以前的HashMap在多线程环境下为什么会导致死循环

JDK1.7以前的Hashmap链表,插入新元素时采用的是头插法,而多线程环境下,多个线程同时操作链表,可能导致头节点指向错误的位置,从而导致形成环形链表。

为了解决这个问题,JDK1.8以后链表插入新元素改为了尾插法。

但多线程环境下还是建议使用ConcurrentHashMap。因此即使没有死循环的问题,多线程环境下也可能导致数据覆盖、数据丢失等情况。

6. 如何遍历hashmap

三种方法:1)使用keySet;2)使用valueSet;3)使用entrySet;

HashMap<Integer, Integer> map = new HashMap();
for(Integer key : map.keySet()){}
for(Integer value: map.valueSet()){}
for(Map.Entry<Integer, Integer> entry : map.entrySet()){}

7. ConcurrentHashMap是怎么保证线程安全的

JDK1.8以前,ConcurrentHashMap是通过分段加锁保证线程安全的。

而JDK1.8以后,是使用CAS+synchronized关键字实现的,只锁定当前数组元素或链表和红黑树的头节点,锁粒度更细,只要不在同一个节点,就不影响其他线程,效率更高。

当插入新元素时,先根据key计算出哈希值,如果当前位置没有元素,使用CAS尝试插入元素,如果失败就不断重复尝试自旋插入,如果成功,直接break。

如果当前位置有元素,获取synchronized锁,锁定当前链表/红黑树插入元素。

8. LinkedHashMap的底层原理

LinkedHashMap底层是使用HashMap+双向链表实现的,在hashmap哈希存储节点时,还维护了一个双向链表,来存储元素的插入顺序。

当使用迭代器遍历LinkedHashMap时,按照插入顺序遍历。

9. LinkedHashMap如何按照访问顺序存储

当希望按照访问顺序输出时,只需将LinkedHashMap的accessOrder设置为true,这样,当使用get方法访问元素时,会将其移动到链表末尾。当按照链表遍历时,即可得到访问顺序输出。

10. LinkedHashMap如何实现LRU缓存

LinkedHashMap可以用于实现LRU缓存。

1)继承LinkedHashMap

2)构造方法:调用super构造方法并设置accessOrder为true,当访问某元素时,将其移到链表最末尾。存储容量。

3)重写LinkedHashMap的removeEldestEntry方法,这个方法返回一个布尔值,告知LinkedHashMap是否需要移除链表head元素,在这个方法中,判断链表长度是否超过容量。

Java并发

线程和进程

1. 什么是进程,什么是线程,Java中是如何规定的

进程是内存分配的基本单位,线程是CPU调度的基本单位。线程是轻量级的进程,执行开销小,但因为共享部分资源,不利于资源的管理和保护。

Java中多个线程共享进程的堆和方法区,但每个线程有独立的程序计数器、虚拟机栈、本地方法栈。

Java中的线程是内核级线程,一个Java线程对应一个内核线程,可以利用多核CPU。

程序计数器为什么是私有的?

程序计数器用于指示线程执行的位置,保证线程切换时能够恢复到正确的位置,因此必须是线程私有的。

虚拟机栈用于存储线程执行中的局部变量、操作数栈、常量池引用等信息,每个线程都不一样,所以是线程私有的。

本地方法栈与虚拟机栈类似,但虚拟机栈存储的是Java方法执行信息,本地方法栈存储的是Native方法执行信息。

2. 线程的生命周期

初始态(NEW):线程刚被创建,还没有调用start方法执行

运行态(RUNNABLE):线程调用start方法执行

阻塞态(BLOCKED):线程被锁阻塞

等待态(Waiting):线程需要等待其他线程通知或中断,调用wait方法可以进入该状态

超时等待态(TIME_WAITING):同样等待其他线程动作,但超时后自动返回。使用sleep(long millis)或wait(long millis)可以进入该状态

终止态(TERMINATED):线程运行完毕

3. Java中如何创建线程

有两种方法:

1)继承Thread类

public class MyThread extends Thread {@Overridepublic void run() {// }
}MyThread myThread = new MyThread();
myThread.start();

2)实现Runnable接口

public class MyRunnable implements Runnable {@Overridepublic void run() {//}
}
Thread myThread = new Thread(new MyRunnable());
myThread.start();

4. 什么是线程的上下文,什么时候会发生上下文切换

线程的上下文就是线程执行所需的程序计数器、虚拟机栈等信息。

线程上下文切换就是指当切换线程时,先保存当前线程的上下文信息,然后将下一个线程的上下文信息加载到CPU中。

线程切换的几种情况:

1)调用sleep()、wait()方法时主动让出CPU

2)时间片用完

3)发生阻塞,例如IO阻塞

4)被终止或线程结束运行

5. Thread的sleep方法和Object类的wait方法有什么异同

相同:二者都会暂停线程的执行,让出CPU

不同:sleep方法不会释放锁,wait方法会释放锁。wait后线程不会主动苏醒,必须有其他线程notify;sleep到时间后线程会自动苏醒。

6. 可以直接调用Thread类的run方法来启动线程吗

不能。直接调用Thread类的run方法只是将这个方法当作main线程中的一个普通方法去调用,不会启动新线程。而调用Thread类的start方法时,会启动一个新线程,执行相应准备工作,然后调用run方法。

7. 什么是并发,什么是并行

并发:多个线程在同一时间段内同时执行

并行:多个线程在同一时刻同时执行

8. 什么是同步,什么是异步

同步:发出一个调用后必须等待调用返回才能继续执行

异步:发出一个调用后不等待返回,直接向下执行

9. 为什么要使用多线程

1)从计算机底层角度来说,对于单核CPU,在执行耗费时间的IO操作时可以切换到其他线程,提高CPU利用率;对于多核CPU,可以有效利用多核CPU的能力。

2)从互联网发展趋势来说,多线程并发编程是提高实现系统高并发的基础。

10. Java使用的系统线程调度方式是什么

操作系统调度线程有两种方式:抢占式和协同式。

抢占式:操作系统决定何时切换线程,会造成上下文切换的开销,但公平性较好,不容易出现线程阻塞;

协同式:线程自己决定何时切换并通知系统,上下文切换开销较小,但公平性较差。

Java使用的是抢占式,JVM本身不负责线程调度,而是交给操作系统。

11. 单核CPU能运行多线程吗,一定能够提高效率吗

单核CPU可以运行多线程,操作系统通过时间片轮转的方式将CPU时间分配给不同线程。

对于IO密集型任务,可以提高效率,因为在执行IO任务时可以将CPU给其他线程使用;

对于CPU密集型任务,不能提高效率,因为大量线程都需要使用CPU,进行线程切换反而会造成上下文切换的开销。

12.什么是死锁,死锁形成的四个条件,怎样检测、预防、避免死锁

死锁是指线程循环等待其他线程持有的锁,导致多个线程被同时阻塞。

死锁形成的四个条件:

1)互斥条件:一个锁不能被多个线程占有

2)请求和保持条件:线程已占用一些资源,且同时请求另一些资源,而且等待资源时不释放已有资源

3)不剥夺条件:线程占有的资源未使用完之前不会被其他线程强行剥夺,只能自己使用完毕后释放

4)循环等待条件:多个线程构成循环等待链

检测死锁:

可以用jmap、jstack等查看堆栈内存的分配情况;或使用Visual VM、JConsole等工具,连接对应程序后排查死锁。

预防死锁:

  1. 破坏互斥条件:一般不太可行,因为很多资源本身就是天然互斥的。
  2. 破坏请求与保持条件:可以要求进程一次性请求所有需要的资源,而不是逐步请求。
  3. 破坏不可剥夺条件:当一个进程请求新的资源得不到满足时,释放已占有的资源。
  4. 破坏循环等待条件:可以对资源进行编号,规定进程只能按照编号递增的顺序请求资源。

避免死锁:

银行家算法:通过提前判断资源分配的安全性,来决定是否为进程分配资源。

JMM

1. JMM是什么

JMM是Java内存模型,它是Java程序与JVM实际内存区域之间的一种抽象的模型,并不是真实存在的。

JMM将JVM内存区域分为主内存和本地内存。主内存是所有线程共有的,所有线程创建的对象实例都存储在主内存中。本地内存是线程私有的,本地内存中存储的是主内存中共享变量的副本。

2. happens-before原则和指令重排序

happens-before原则按照程序顺序规则、解锁规则、volatile变量规则、传递规则、线程启动规则等梳理指令的顺序。

指令重排序时根据梳理出的happens-before规则来进行。

a happens-before b的含义是,a指令的结果对b是可见的

3. 并发编程的三个特性

原子性:一个原子操作要么不执行,如果执行就要执行完

可见性:线程对共享变量的修改对所有线程都是可见的。Java中用volatile关键字保证变量的可见性,底层原理是强制变量是主存中分配

有序性:JVM编译器进行指令重排序时,会保证按照happens-before原则进行,这是单线程环境下是正确的,但多线程环境下不一定保证正确

Java并发常用关键字和类

1. volatile关键字有什么作用

volatile关键字有两个作用:

1)保证变量的可见性,指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取;

2)禁止指令重排序,如果将一个变量声明为volatile,在对这个变量进行读写时,会插入特定的内存屏障的方式来禁止指令重排序。

volatile只能保证变量的可见性,不能保证对变量操作的原子性。

2. 双重校验锁实现单例模式

public class Singleton {private volatile static Singleton singleInstance;private Singleton() {}public static Singleton getSingleInstance() {if (singleInstance == null) {synchronized(Singleton.class) {if (singleInstance == null) {singleInstance = new Singleton();}}}return singleInstance;}
}

第一次校验:在同步代码块之外校验单例是否为空,如果不为空直接返回,避免进入同步代码块;

第二次校验:在进入同步代码块之后,再次校验单例是否为空,避免多个线程同时通过第一次校验进行同步代码块,导致创建多个实例。

另外,使用volatile关键字的作用:1)保证单例变量的修改对所有线程可见,避免出现一个线程初始化变量后,其他线程检测到的仍然是null的情况。2)禁止指令重排序,避免出现已分配内存还没初始化的时候,由于指令重排序,直接返回了没有初始化的内存地址的情况。

3. 乐观锁和悲观锁是什么,各有什么优势和缺点,适合什么场景

乐观锁是指总是假设最好的情况,认为共享资源每次被访问都不会出现问题,因此不加锁,只是在提交修改时验证资源是否被其他线程修改了,验证方法:CAS算法/版本号机制。

悲观锁,总是假设最坏的情况,认为共享资源每次被访问都会出现问题,因此每次操作都加锁。

乐观锁可以避免锁竞争造成线程阻塞,也不会有死锁问题。但当写占比非常多的情况下,冲突频繁发生,会不断尝试自旋重试,因此会导致频繁失败重试影响性能。因此乐观锁更适用于读操作较多,写操作较少的场景。

悲观锁可以避免频繁失败重试影响性能,但无法避免锁的开销。因此悲观锁更适用于写操作较多,读操作较少的场景。

4.乐观锁的实现方式:CAS算法

CAS算法:Compare and Swap,当变量当前值等于期望值时,认为没有线程修改过变量,可以更新。如果不相等,自旋重复尝试。

Java中CAS算法是用Unsafe类实现的,提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong等方法,这些方法都是native方法,说明是用本地代码实现的,通常是C或C++

CAS算法可能的问题:

1)ABA问题

即变量先是A,被其他线程改成了B,又被另一个线程改回了A。此时与期望值仍然相同,CAS算法误以为没有被其他线程修改过。

2)循环开销时间大

由于 CAS 操作可能会因为并发冲突而失败,因此通常会与while循环搭配使用,在失败后不断重试,直到操作成功。这就是自旋锁机制,这会导致循环开销。

3)只能保证一个共享变量的原子操作

CAS操作仅对单个共享变量有效。

解决ABA问题的方法:

版本号机制:在变量前加版本号或时间戳,先检测变量值是否等于预期值,再检查变量版本号是否等于预期版本号。版本号一般是在前面加一个version字段,每次修改version++。

4. synchronized关键字是什么,有什么作用,如何使用,底层原理是什么

synchronized关键字用于保证资源访问同步性,它可以保证任何时刻只有一个线程进入代码块。

synchronized关键字有三种使用方法:

  1. 修饰实例方法:获取对象锁
  2. 修饰静态方法:获取类锁
  3. 修饰代码块:取决于括号里的参数,如果是类就是类锁,如果是某个object对象或this,就是对象锁

synchronized关键字的底层原理:

synchronized修饰代码块时,本质是使用monitorenter指令和monitorexit指令,其中monitorenter指向代码块起始位置,monitorexit指向代码块结束位置。

synchronized修饰方法时,没有使用monitorenter和monitorexit指令,而是给方法添加ACC_SYNCHRONIZED标识,表示该方法是一个同步方法。

但无论是使用monitorenter/monitorexit还是使用ACC_SYNCHRONIZED标识,本质都是获取JVM的监视器锁Monitor。

5. synchronized锁升级

每个对象头中会有有个Mark Word,用于记录哈希码、锁状态、GC分代年龄等信息。

对象的锁状态有四种:

1)无锁状态。

2)偏向锁:当只有一个线程访问同步代码块时,会使用偏向锁;

3)轻量级锁:当多个线程竞争锁时,JVM 会将偏向锁升级为轻量级锁。轻量级锁通过 CAS 操作尝试获取锁,避免线程阻塞。

4)重量级锁:当自旋等待超过一定次数后,JVM 会将锁升级为重量级锁。重量级锁会导致线程阻塞,进入操作系统内核态,性能开销较大。

synchronized修饰的代码块会使锁随线程数增多不断升级,锁只能升级不能降级。

synchronized和volatile的关系

synchronized和volatile是互补的,经常互相配合使用。

synchronized用于修饰代码块,保证多线程访问资源的同步性。volatile用于修饰变量,保证变量的可见性。

volatile只能保证可见性和有序性,不能保证原子性。synchronized三者都能保证。

volatile比synchronized更轻量级。

6. ReentrantLock是什么

ReentrantLock是可重入锁,即如果一个线程已经占有这个锁,当它再次请求这个锁时可以请求到。

实际上synchronized锁也是可重入的,但ReetrantLock比synchronized功能更完善。

ReentrantLock有公平锁和非公平锁两种实现,默认使用非公平锁。

ReentrantLock底层是用AQS实现的。

7. synchronized与ReentrantLock的异同

相同:都是可重入锁

不同:

1)ReentrantLock可以实现公平锁也可以实现非公平锁,synchronized只能实现非公平锁;

2)synchronized基于JVM实现,ReentrantLock基于JDK实现

3)ReentrantLock还提供了等待中断、超时、条件等高级功能。

7. 公平锁、非公平锁;可中断锁、不可中断锁;共享锁、独占锁

公平锁:按照申请顺序分配锁,保证了时间上的绝对顺序,但性能较差;

非公平锁:不按申请顺序,而是按照一定的优先级分配锁。

可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后才能进行其他逻辑处理。ReentrantLock就属于是可中断锁。

不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized就属于是不可中断锁。

共享锁:一把锁可以被多个线程同时获得,比如读锁是共享锁

独占锁:一把锁只能被一个线程获得,比如写锁是独占锁

8. 读写锁

读写锁既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

一般锁的互斥条件:读读互斥、读写互斥、写写互斥

读写锁的互斥原理:读读不互斥、读写互斥、写写互斥

Java中实现的读写锁:ReentrantReadWriteLock、StampedLock。其中ReentrantReadWriteLock是可重入的读写锁,StampedLock是不可重入的读写锁。

读写锁适用于读多写少的场景。

AQS

AQS全称是AbstractQueueSynchronizer,即抽象队列同步器。

顾名思义,AQS是一个用于构建锁和同步器的抽象类,主要提供了可重入锁、信号量、倒计时器等一些通用框架。

AQS的底层原理

AQS底层是基于CLH锁队列的变体实现的。

CLH锁是一种基于CAS算法的优化。在CAS算法中,获取不到锁时会自旋重复获取。这种自旋方式可能导致某个线程长时间获取不到锁,造成饥饿。

为了解决这个问题,CLH引入了一个队列来组织并发竞争的线程。每个竞争的线程按顺序加入到队列中排队,保证公平性。

当使用CAS方法获取锁失败时,先短暂自旋尝试获取,如果仍然失败,阻塞线程加入队列等待被唤醒,防止重复自旋占用CPU。

AQS中CLH锁队列

队列中由各个节点构成,每个节点都有自己的状态。

例如三个线程获取锁时,T1先获取到,初始化等待队列,其中有一个头节点,状态为0;当T2请求锁时,锁已被占用,将T2加入等待队列中,放在头节点的next位置,此时头结点的状态改为SIGNAL(-1),表示当前节点退出时需唤醒后继节点;同样T3申请锁时加入等待队列,放在T2的后面,T2状态改为SIGNAL(-1);当T1释放锁时,唤醒后继节点T2,T2获取锁,成为新的head。

Semaphore

用于访问数量有限的资源,控制访问资源的线程数量。

使用场景:数据库连接池、线程池等数量有限的资源

CountDownLatch

CountDownLatch的作用是让count个线程阻塞,直到所有线程执行完毕。

使用场景:确保在某些任务完成后再执行后续操作,例如多个线程完成数据加载后再进行数据处理。

CyclicBarrier

CyclicBarrier的作用是让count个线程阻塞在屏障处,直到所有线程到达屏障。

使用场景:多线程并行计算时,在某个点合并结果再继续计算。

Atomic原子类

Atomic原子类是指具有原子操作特性的类,使用原子类的变量在操作时要么完整执行,要么不执行。

Atomic实现原理是CAS算法。

Atomic原子类有哪些?

1)基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean

2)数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

3)引用类型原子类:AtomicReference、**AtomicStampedReference**、AtomicMarkableReference

4)对象属性修改原子类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater原子更新引用类型中某个字段

Atomic的应用场景:

并发计数

ThreadLocal

对于ThreadLocal创建的变量,每个线程都有一个本地副本。线程内通过threadLocal.get()获取,通过threadLocal().set()设置其值。

ThreadLocal应用场景

数据库连接管理:每个线程可以有一个独立的数据库连接,避免了多个线程共享数据库连接的问题。

ThreadLocal底层原理

每个线程内部都有一个ThreadLocalMap,当用ThreadLocal创建变量时,ThreadLocal为key,对应的变量Object类为value,存储在map中。

ThreadLocal什么情况下会导致内存泄漏?

正常使用继承Thread类来创建线程是不会导致内存泄漏的。因为线程的ThreadLocalMap只被当前线程引用,当前线程结束任务退出以后,这种引用消失,线程对应的ThreadLocalMap就会被GC回收。

但实际项目开发中多使用线程池来实现多线程,这种情况下使用ThreadLocal就可能导致内存泄漏。因为线程池中的线程不会退出,是循环使用的,所以对应的ThreadLocalMap的引用不会消失,ThreadLocalMap不会被GC回收。但是ThreadLocalMap内部存储的是Entry数组,其中有多个key-value对,其中key是ThreadLocal对象,是弱引用的,value是对应的Object对象,是强引用的。当当前线程执行完毕,投入下一次使用时,弱引用的key会被GC回收,但value是强引用的,不会被回收,这就导致value对应的key为null,长期无法被回收就会导致内存泄漏。

如何避免内存泄漏?

每次使用完ThreadLocal对象后,调用remove方法将其移除ThreadLocalMap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/894392.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第1章 量子暗网中的血色黎明

月球暗面的危机与阴谋 量子隧穿效应催生的幽蓝电弧&#xff0c;于环形山表面肆意跳跃&#xff0c;仿若无数奋力挣扎的机械蠕虫&#xff0c;将月球暗面的死寂打破&#xff0c;徒增几分诡异。艾丽伫立在被遗弃的“广寒宫”量子基站顶端&#xff0c;机械义眼之中&#xff0c;倒映着…

AI-ISP论文Learning to See in the Dark解读

论文地址&#xff1a;Learning to See in the Dark 图1. 利用卷积网络进行极微光成像。黑暗的室内环境。相机处的照度小于0.1勒克斯。索尼α7S II传感器曝光时间为1/30秒。(a) 相机在ISO 8000下拍摄的图像。(b) 相机在ISO 409600下拍摄的图像。该图像存在噪点和色彩偏差。©…

【Git】初识Git Git基本操作详解

文章目录 学习目标Ⅰ. 初始 Git&#x1f4a5;注意事项 Ⅱ. Git 安装Linux-centos安装Git Ⅲ. Git基本操作一、创建git本地仓库 -- git init二、配置 Git -- git config三、认识工作区、暂存区、版本库① 工作区② 暂存区③ 版本库④ 三者的关系 四、添加、提交更改、查看提交日…

使用 Spring JDBC 进行数据库操作:深入解析 JdbcTemplate

目录 1. Spring JDBC 简介 2. JdbcTemplate 介绍 3. 创建数据库和表 4. 配置 Spring JDBC 5. 创建实体类 6. 使用 JdbcTemplate 实现增、删、改、查操作 7. Spring JDBC 优点 8. 小结 1. Spring JDBC 简介 Spring JDBC 是 Spring 框架中的一个模块&#xff0c;旨在简化…

BUUCTF [Black Watch 入群题]PWN1 题解

1.下载文件 exeinfo checksec 32位 IDA32 看到关键函数 read两次 第一次read的变量s在bss段&#xff1b;第二次的buf到ebp距离为 24 但是第二次的read字节只能刚好填满返回地址 传不进去变量 所以想到栈迁移 将栈移动到变量s所在位置上来 同时 这题开了NX 无直接的binsh和s…

Cubemx文件系统挂载多设备

cubumx版本&#xff1a;6.13.0 芯片&#xff1a;STM32F407VET6 在上一篇文章中介绍了Cubemx的FATFS和SD卡的配置&#xff0c;由于SD卡使用的是SDIO通讯&#xff0c;因此具体驱动不需要自己实现&#xff0c;Cubemx中就可以直接配置然后生成SDIO的驱动&#xff0c;并将SD卡驱动和…

java练习(2)

回文数&#xff08;题目来自力扣&#xff09; 给你一个整数 x &#xff0c;如果 x 是一个回文整数&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 回文数 是指正序&#xff08;从左向右&#xff09;和倒序&#xff08;从右向左&#xff09;读都是一样的整…

使用 Tauri 2 + Next.js 开发跨平台桌面应用实践:Singbox GUI 实践

Singbox GUI 实践 最近用 Tauri Next.js 做了个项目 - Singbox GUI&#xff0c;是个给 sing-box 用的图形界面工具。支持 Windows、Linux 和 macOS。作为第一次接触这两个框架的新手&#xff0c;感觉收获还蛮多的&#xff0c;今天来分享下开发过程中的一些经验~ 为啥要做这个…

ComfyUI安装调用DeepSeek——DeepSeek多模态之图形模型安装问题解决(ComfyUI-Janus-Pro)

ComfyUI 的 Janus-Pro 节点&#xff0c;一个统一的多模态理解和生成框架。 试用&#xff1a; https://huggingface.co/spaces/deepseek-ai/Janus-1.3B https://huggingface.co/spaces/deepseek-ai/Janus-Pro-7B https://huggingface.co/spaces/deepseek-ai/JanusFlow-1.3B 安装…

索引的底层数据结构、B+树的结构、为什么InnoDB使用B+树而不是B树呢

索引的底层数据结构 MySQL中常用的是Hash索引和B树索引 Hash索引&#xff1a;基于哈希表实现的&#xff0c;查找速度非常快&#xff0c;但是由于哈希表的特性&#xff0c;不支持范围查找和排序&#xff0c;在MySQL中支持的哈希索引是自适应的&#xff0c;不能手动创建 B树的…

RK3568中使用QT opencv(显示基础图像)

文章目录 一、查看对应的开发环境是否有opencv的库二、QT使用opencv一、查看对应的开发环境是否有opencv的库 在开发板中的/usr/lib目录下查看是否有opencv的库: 这里使用的是正点原子的ubuntu虚拟机,在他的虚拟机里面已经安装好了opencv的库。 二、QT使用opencv 在QT pr…

29.Word:公司本财年的年度报告【13】

目录 NO1.2.3.4 NO5.6.7​ NO8.9.10​ NO1.2.3.4 另存为F12&#xff1a;考生文件夹&#xff1a;Word.docx选中绿色标记的标题文本→样式对话框→单击右键→点击样式对话框→单击右键→修改→所有脚本→颜色/字体/名称→边框&#xff1a;0.5磅、黑色、单线条&#xff1a;点…

省级-新质生产力数据(2010-2022年)-社科数据

省级-新质生产力数据&#xff08;2010-2022年&#xff09;-社科数据https://download.csdn.net/download/paofuluolijiang/90028612 https://download.csdn.net/download/paofuluolijiang/90028612 新质生产力是指在现代科技和经济社会发展的推动下&#xff0c;由新的生产要素…

【PyTorch】7.自动微分模块:开启神经网络 “进化之门” 的魔法钥匙

目录 1. 梯度基本计算 2. 控制梯度计算 3. 梯度计算注意 4. 小节 个人主页&#xff1a;Icomi 专栏地址&#xff1a;PyTorch入门 在深度学习蓬勃发展的当下&#xff0c;PyTorch 是不可或缺的工具。它作为强大的深度学习框架&#xff0c;为构建和训练神经网络提供了高效且灵活…

【数据分析】案例04:豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask)

豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask) 豆瓣电影Top250官网:https://movie.douban.com/top250写在前面 实验目的:实现豆瓣电影Top250详情的数据分析与Web网页可视化。电脑系统:Windows使用软件:PyCharm、NavicatPython版本:Python 3.…

Ubuntu20.04 深度学习环境配置(持续完善)

文章目录 常用的一些命令安装 Anaconda创建conda虚拟环境查看虚拟环境大小 安装显卡驱动安装CUDA安装cuDNN官方仓库安装 cuDNN安装 cuDNN 库验证 cuDNN 安装确认 CUDA 和 cuDNN 是否匹配&#xff1a; TensorRT下载 TensorRT安装 TensorRT 本地仓库配置 GPG 签名密钥安装 Tensor…

元宇宙与Facebook:社交互动的未来方向

随着技术的飞速发展&#xff0c;元宇宙逐渐成为全球科技领域关注的焦点。作为一种集沉浸式体验、虚拟空间和数字社交互动为一体的新型平台&#xff0c;元宇宙正在重新定义人类的社交方式。而在这一变革中&#xff0c;Facebook&#xff08;现改名为Meta&#xff09;作为全球领先…

【赵渝强老师】K8s中Pod探针的ExecAction

在K8s集群中&#xff0c;当Pod处于运行状态时&#xff0c;kubelet通过使用探针&#xff08;Probe&#xff09;对容器的健康状态执行检查和诊断。K8s支持三种不同类型的探针&#xff0c;分别是&#xff1a;livenessProbe&#xff08;存活探针&#xff09;、readinessProbe&#…

python 语音识别

目录 一、语音识别 二、代码实践 2.1 使用vosk三方库 2.2 使用SpeechRecognition 2.3 使用Whisper 一、语音识别 今天识别了别人做的这个app,觉得虽然是个日记app 但是用来学英语也挺好的,能进行语音识别,然后矫正语法,自己说的时候 ,实在不知道怎么说可以先乱说,然…

Node.js——body-parser、防盗链、路由模块化、express-generator应用生成器

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…