1.List 、Set和Map 的区别
- List 以索引来存取元素,有序的,元素是允许重复的,可以插入多个null;
- Set 不能存放重复元素,无序的,只允许一个null;
- Map 保存键值对映射;
- List 底层实现有数组、链表两种方式;Set、Map 容器有基于哈希存储和红黑树两种方式实现;
- Set 基于 Map 实现,Set 里的元素值就是 Map的键值。
2.HashMap
HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。
2.1解决hash冲突的办法有哪些?HashMap用的哪种?
解决Hash冲突方法有:开放定址法、再哈希法、链地址法。HashMap中采用的是 链地址法 。
- 开放定址法基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H§,如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。
- 再哈希法提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。
- 链地址法将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
2.2HashMap扩容机制
HashMap的底层有数组 + 链表(红黑树)组成,数组的大小可以在构造方法时设置,默认大小为16,数组中每一个元素就是一个链表,jdk7之前链表中的元素采用头插法插入元素,jdk8之后采用尾插法插入元素,由于插入的元素越来越多,查找效率就变低了,所以满足某种条件时,链表会转换成红黑树。随着元素的增加,HashMap的数组会频繁扩容,如果构造时不赋予加载因子默认值,那么负载因子默认值为0.75,数组扩容的情况如下:
1:当添加某个元素后,数组的总的添加元素数大于了 数组长度 * 0.75(默认,也可自己设定),数组长度扩容为两倍。(如开始创建HashMap集合后,数组长度为16,临界值为16 * 0.75 = 12,当加入元素后元素个数超过12,数组长度扩容为32,临界值变为24)
JDK1.7版本扩容
①:先生成新数组;
②:遍历老数组中的每个位置上的链表上的每个元素;
③:获取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标;
④:将元素添加到新数组中去;
⑤:所有元素转移完之后,将新数组赋值给HashMap对象的table属性。
JDK1.8版本扩容
①:先生成新数组;
②:遍历老数组中的每个位置上的链表或红黑树;
③:如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去;
④:如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置;
a:统计每个下标位置的元素个数;
b:如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置;
c:如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置;
3 Java中的队列都有哪些,有什么区别。
Java中常用的队列有以下几种:
ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,按照先进先出的原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的有界阻塞队列,按照先进先出的原则对元素进行排序。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个支持延时获取元素的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,否则插入操作一直处于阻塞状态。
LinkedTransferQueue:一个基于链表结构的无界阻塞队列,支持生产者消费者模式。
ConcurrentLinkedQueue:一个基于链表结构的无界并发队列,按照先进先出的原则对元素进行排序。
Java 中的这些队列可以从不同的维度进行分类,例如可以从阻塞和非阻塞进行分类,也可以从有界和无界进行分类,而本文将从队列的功能上进行分类,例如:优先队列、普通队列、双端队列、延迟队列等。
阻塞队列和非阻塞队列
阻塞队列(Blocking Queue)
提供了可阻塞的 put
和 take
方法,它们与可定时的 offer
和 poll
是等价的。如果队列满了 put
方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么 take
方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put
方法和 take
方法就永远不会阻塞。
非阻塞队列
非阻塞队列也就是普通队列,它的名字中不会包含 BlockingQueue
关键字,并且它不会包含 put
和 take
方法,当队列满之后如果还有新元素入列会直接返回错误,并不会阻塞的等待着添加元素,如下图所示:
非阻塞队列的典型代表是 ConcurrentLinkedQueue
和 PriorityQueue
。
有界队列和无界队列
有界队列:是指有固定大小的队列,比如设定了固定大小的 ArrayBlockingQueue
,又或者大小为 0 的 SynchronousQueue
。
无界队列:指的是没有设置固定大小的队列,但其实如果没有设置固定大小也是有默认值的,只不过默认值是 Integer.MAX_VALUE,当然实际的使用中不会有这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的角度来看相当于 “无界”的。
按功能分类
接下来就是本文的重点了,我们以功能来划分一下队列,它可以被分为:普通队列、优先队列、双端队列、延迟队列、其他队列等,接下来我们分别来看。
1.普通队列
普通队列(Queue)是指实现了先进先出的基本队列,例如 ArrayBlockingQueue
和 LinkedBlockingQueue
,其中 ArrayBlockingQueue
是用数组实现的普通队列,如下图所示:
而 LinkedBlockingQueue
是使用链表实现的普通队列,如下图所示:
常用方法
普通队列中的常用方法有以下这些:
- offer():添加元素,如果队列已满直接返回 false,队列未满则直接插入并返回 true;
- poll():删除并返回队头元素,当队列为空返回 null;
- add():添加元素,此方法是对 offer 方法的简单封装,如果队列已满,抛出 IllegalStateException 异常;
- remove():直接删除队头元素;
- put():添加元素,如果队列已经满,则会阻塞等待插入;
- take():删除并返回队头元素,当队列为空,则会阻塞等待;
- peek():查询队头元素,但不会进行删除;
- element():对 peek 方法进行简单封装,如果队头元素存在则取出并不删除,如果不存在抛出 NoSuchElementException 异常。
2.双端队列
双端队列( LinkedBlockingDeque )是指队列的头部和尾部都可以同时入队和出队的数据结构,如下图所示:
import java.util.concurrent.LinkedBlockingDeque;
/**
* 双端队列示例
*/
static class LinkedBlockingDequeTest {
public static void main(String[] args) {
// 创建一个双端队列
LinkedBlockingDeque deque = new LinkedBlockingDeque();
deque.offer("offer"); // 插入首个元素
deque.offerFirst("offerFirst"); // 队头插入元素
deque.offerLast("offerLast"); // 队尾插入元素
while (!deque.isEmpty()) {
// 从头遍历打印
System.out.println(deque.poll());
}
}
}
3.优先队列
优先队列(PriorityQueue)是一种特殊的队列,它并不是先进先出的,而是优先级高的元素先出队。
优先队列是根据二叉堆实现的,二叉堆的数据结构如下图所示:
二叉堆分为两种类型:一种是最大堆一种是最小堆。以上展示的是最大堆,在最大堆中,任意一个父节点的值都大于等于它左右子节点的值。
因为优先队列是基于二叉堆实现的,因此它可以将优先级最好的元素先出队。
import java.util.PriorityQueue;
public class PriorityQueueTest {
// 自定义的实体类
static class Viper {
private int id; // id
private String name; // 名称
private int level; // 等级
public Viper(int id, String name, int level) {
this.id = id;
this.name = name;
this.level = level;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue(10, new Comparator<Viper>() {
@Override
public int compare(Viper v1, Viper v2) {
// 设置优先级规则(倒序,等级越高权限越大)
return v2.getLevel() - v1.getLevel();
}
});
// 构建实体类
Viper v1 = new Viper(1, "Java", 1);
Viper v2 = new Viper(2, "MySQL", 5);
Viper v3 = new Viper(3, "Redis", 3);
// 入列
queue.offer(v1);
queue.offer(v2);
queue.offer(v3);
while (!queue.isEmpty()) {
// 遍历名称
Viper item = (Viper) queue.poll();
System.out.println("Name:" + item.getName() +
" Level:" + item.getLevel());
}
}
}
以上代码的执行结果如下:
Name:MySQL Level:5
Name:Redis Level:3
Name:Java Level:1
从上述结果可以看出,优先队列的出队是不考虑入队顺序的,它始终遵循的是优先级高的元素先出队。
4.延迟队列
延迟队列(DelayQueue)是基于优先队列
PriorityQueue
实现的,它可以看作是一种以时间为度量单位的优先的队列,当入队的元素到达指定的延迟时间之后方可出队。
我们来演示一下延迟队列的使用:
import lombok.Getter;
import lombok.Setter;
import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class CustomDelayQueue {
// 延迟消息队列
private static DelayQueue delayQueue = new DelayQueue();
public static void main(String[] args) throws InterruptedException {
producer(); // 调用生产者
consumer(); // 调用消费者
}
// 生产者
public static void producer() {
// 添加消息
delayQueue.put(new MyDelay(1000, "消息1"));
delayQueue.put(new MyDelay(3000, "消息2"));
}
// 消费者
public static void consumer() throws InterruptedException {
System.out.println("开始执行时间:" +
DateFormat.getDateTimeInstance().format(new Date()));
while (!delayQueue.isEmpty()) {
System.out.println(delayQueue.take());
}
System.out.println("结束执行时间:" +
DateFormat.getDateTimeInstance().format(new Date()));
}
static class MyDelay implements Delayed {
// 延迟截止时间(单位:毫秒)
long delayTime = System.currentTimeMillis();
// 借助 lombok 实现
@Getter
@Setter
private String msg;
/**
* 初始化
* @param delayTime 设置延迟执行时间
* @param msg 执行的消息
*/
public MyDelay(long delayTime, String msg) {
this.delayTime = (this.delayTime + delayTime);
this.msg = msg;
}
// 获取剩余时间
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// 队列里元素的排序依据
@Override
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
return 1;
} else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
} else {
return 0;
}
}
@Override
public String toString() {
return this.msg;
}
}
}
以上代码的执行结果如下:
开始执行时间:2020-10-20 20:17:28
消息1
消息2
结束执行时间:2020-10-20 20:17:31
从上述结束执行时间和开始执行时间可以看出,消息 1 和消息 2 都正常实现了延迟执行的功能。
4.简述java中的锁机制。轻量锁,重量锁,自旋锁,CAS以及ABA问题。
Java中的乐观锁和悲观锁,以及CAS(Compare and Swap)可以通过下面的例子说明:
悲观锁:假设有多个线程同时访问一个共享资源,在使用悲观锁的情况下,每个线程在访问资源之前都会先获取独占锁,保证其他线程无法同时访问该资源。
public void transferMoney(Account fromAccount, Account toAccount, double amount) {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
乐观锁:与悲观锁不同,乐观锁在访问资源时不会上锁,而是在执行操作之前尝试对资源进行版本号等检查,保证没有其他线程的修改。如果检查通过,则执行操作并更新版本号。如果检查失败,则需要重试操作。
import java.util.concurrent.atomic.AtomicInteger;
public class ConcurrentCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int currentCount;
do {
currentCount = count.get();
} while (!count.compareAndSet(currentCount, currentCount + 1));
}
public int getCount() {
return count.get();
}
}
CAS ABA问题怎么解决?
在计算机科学中,CAS指的是“比较并交换”(Compare-And-Swap)操作。ABA问题是指在多线程编程中,当一个值先变成A,后又变成B,最后再变回A时,如果这期间有其他线程进行过一些修改,则可能导致某些意外结果。
为了解决ABA问题,可以使用带有版本号的CAS,也称为“比较、版本号并交换”(Compare-And-Version-Swap)。这种方法在每次更新共享变量时都会增加版本号。因此,在执行CAS操作之前,需要检查共享变量的当前值是否与事先保存的参考值以及版本号匹配,即确保它没有被修改过。只有在这种情况下才能执行CAS操作,否则就需要重新执行。
通过使用版本号,可以避免ABA问题,因为即使共享变量的值从A到B再到A再到B,但由于版本号已经发生了改变,因此CAS操作仍然会被视为无效,从而避免了潜在的错误。
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
public static void main(String[] args) {
String initialRef = "initial value";
int initialStamp = 0;
AtomicStampedReference<String> atomicRef = new AtomicStampedReference<>(initialRef, initialStamp);
// Thread 1
String newRef1 = "new value 1";
int stamp1 = atomicRef.getStamp();
atomicRef.compareAndSet(initialRef, newRef1, stamp1, stamp1 + 1);
// Thread 2
String currentValue = atomicRef.get();
int stamp2 = atomicRef.getStamp();
String newRef2 = "new value 2";
atomicRef.compareAndSet(currentValue, newRef2, stamp2, stamp2 + 1);
// Thread 3
boolean success = atomicRef.compareAndSet(newRef1, initialRef, stamp1, stamp1 + 1);
System.out.println("Success: " + success); // prints "Success: false"
}
}
5.hashmap和concurrenthashmap的区别是什么??
1.线程安全性:HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。多个线程可以同时读取和写入ConcurrentHashMap,而且不会导致数据不一致或其他异常情况。
2.性能:由于ConcurrentHashMap需要保证线程安全性,所以在并发访问时可能会带来一些额外的开销。但是,在具有高度并发需求的系统中,使用ConcurrentHashMap往往比使用HashMap更快。
3.内部结构:ConcurrentHashMap使用了分段锁机制,将整个Map分成了多个Segment(默认为16个),每个Segment上都有一个独立的锁。这样,在并发访问时只有当前操作的Segment上的锁被阻塞,其他Segment仍然可以被访问。而HashMap没有这种机制,需要在整个Map上加锁。
4.容量大小:ConcurrentHashMap的容量大小不需要事先指定,在运行时可以动态调整。而HashMap在创建时需要指定初始容量大小,如果容量不足时需要进行扩容操作,可能会影响性能。
5.迭代器:HashMap的迭代器(Iterator)是fail-fast的,即当在迭代过程中对Map进行了修改操作时,会抛出ConcurrentModificationException异常。而ConcurrentHashMap的迭代器则是weakly consistent的,即可以对其进行修改操作,但不能保证迭代器能看到所有修改。
5.HashMap 允许 key 和 value 为 null。而 ConcurrentHashMap 的 key 和 value 都不允许为 null。
总之,如果需要在高并发环境下使用Map,并且需要线程安全性,则应该选择ConcurrentHashMap;否则,如果不需要线程安全,则可以考虑使用HashMap。
6.feign进行服务调用,为什么给出服务名称,就能调用?
在使用 Feign 进行服务调用时,可以通过指定服务名称来发起调用。这是因为 Feign 集成了 Ribbon 负载均衡器,可以自动地将服务名称解析为具体的服务实例列表,并根据负载均衡策略选择一个实例进行访问。
Feign 的服务发现和负载均衡功能主要是通过 Ribbon 实现的。当我们使用 @FeignClient 注解声明一个接口,并且指定了服务名称时,Feign 会生成一个代理类,该代理类继承了 Feign.Builder 类,同时也会添加一些额外的逻辑,其中就包括了 Ribbon 的负载均衡逻辑。
下面是一个简单的代码示例:
java
@FeignClient(name = “user-service”)
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
上述代码中,@FeignClient 注解指定了服务名称为 “user-service”,代表我们要调用名为 “user-service” 的远程服务。在实际调用时,Feign 会通过 Ribbon 获取所有名为 “user-service” 的服务实例,并根据负载均衡策略选择一个实例进行访问。
总之,Feign 之所以能够通过服务名称进行服务调用,关键在于它集成了 Ribbon 负载均衡器,可以自动地完成服务发现和负载均衡的工作,从而方便地进行服务调用。
SpringBoot 自动装配过程。
1.启动类上面有一个@SpringBootApplication这个注解,这个注解是一个组合注解。它包含很多注解,我们可以点进去一下。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
主要涉及以下几个注解:
@SpringBootConfiguration
作用:标记这个类是一个Springboot的配置类。
@ComponentScan
作用:自动扫描包(扫描当前主启动类同级的包)并加载符合条件的组件或者bean,将这个bean定义加载到IOC容器中。
@EnableAutoConfiguration
作用:开启自动配置功能
2.点击@EnableAutoConfiguration,进入到这个注解里面,有以下几个注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
主要涉及以下几个注解:
@AutoConfigurationPackage
作用:自动注册包
@Import({AutoConfigurationImportSelector.class})
作用: 给容器导入组件
AutoConfigurationImportSelector.class
作用:自动配置导入选择器
3.点击AutoConfigurationImportSelector进入查看源码
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
selectImports()是AutoConfigurationImportSelector的核心函数,其核心功能就是获取spring.factories中EnableAutoConfiguration所对应的Configuration类列表。
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
看到selectImports方法里的这一行,我们点击继续查看进入到getAutoConfigurationEntry()这个方法,获取自动配置的实体。
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
此方法中有一个 this.getCandidateConfigurations(annotationMetadata, attributes); 获得候选配置的方法;点击这个方法进入到getCandidateConfiguration()这个方法。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
点击getSpringFactoriesLoaderFactoryClass()方法,这个方法,标注了EnableAutoConfiguration注解的类。
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
getCandidateConfiguration()方法又调用SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法------->获取所有的加载配置。
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
在loadFactoryNames()方法看到这一行
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
我们继续点击查看 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
loadSpringFactories 方法中的classLoader.getResources(“META-INF/spring.factories”) —》获取项目资源;
ClassLoader.getSystemResources(“META-INF/spring.factories”)—》获取系统资源。
整体流程图: