Java
Java
基础
BigDecimal(double)和BigDecimal(String)有什么区别?
因为double是不精确的,所以使用一个不精确的数字来创建BigDeciaml,得到的数字也是不精确的。如0.1这个数字,double只能表示他的近似值。
所以,当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。
而是0.1000000000000000055511151231257827021181583404541015625。这是因为double自身表示的只是一个近似值。
而对于BigDecimal(String) ,当我们使用new BigDecimal(“0.1”)创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。
那么他的标度也就是1
ClassNotFoundException和NoClassDefFoundError的区别是什么?
ClassNotFoundException是一个受查异常(checked exception)。他通常在运行时,在类加载阶段尝试加载类的过程中,找不到类的定义时触发。通常是由Class.forName()或类加载器loadClass或者findSystemClass时,在类路径中没有找到指定名称的类时,会抛出该异常。表示所需的类在类路径中不存在。这通常是由于类名拼写错误或缺少依赖导致的。
如以下方式加载JDBC驱动:
public class MainClass
{
public static void main(String[] args)
{
try
{
Class.forName("oracle.jdbc.driver.OracleDriver");
}catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
}
当我们的classpath中没有对应的jar包时,就会抛出这个ClassNotFoundException。
NoClassDefFoundError是一个错误(error),它表示运行时尝试加载一个类的定义时,虽然找到了类文件,但在加载、解析或链接类的过程中发生了问题。这通常是由于依赖问题或类定义文件(.class文件)损坏导致的。也就是说这个类在编译时存在,运行时丢失了,就会导致这个异常。
如以下情况,我们定义A类和B类,
class A
{
// some code
}
public class B
{
public static void main(String[] args)
{
A a = new A();
}
}
DO、DTO、VO都是干什么的?
DO、DTO、VO 是三个常见的 Java 对象,它们都是用来承载数据的,但是在不同的场景下有着不同的用途。
-
DO(Domain Object):领域对象,也称为实体对象。DO 通常用于数据库表的映射,DO中包含了实体的属性以及对实体的操作方法。DO 对应的是系统中的数据模型,通常与数据库表一一对应。
-
DTO(Data Transfer Object):数据传输对象。DTO 通常用于在不同层之间传输数据,例如在前端页面和后端服务之间传输数据时使用。DTO 对象封装了要传输的数据,避免了对数据的频繁访问和传输,从而提高了应用程序的性能。
-
VO(View Object):视图对象,也称为展示对象。VO 通常用于表示前端页面显示的数据,例如在 MVC 架构中的 View 层,VO 对应的是用户界面模型,通常与页面一一对应。
总的来说,DO、DTO、VO 都是用来承载数据的对象,它们在不同的场景下有着不同的作用。DO 用于表示实体对象,DTO 用于在不同层之间传输数据,VO 用于表示前端页面显示的数据。使用这三个对象可以有效地组织应用程序的数据模型,并且提高了应用程序的可维护性和可扩展性。
finally中代码一定会执行吗?
通常情况下,finally的代码一定会被执行,但是这是有一个前提的,:1、对应 try 语句块被执行, 2、程序正常运行。
如果没有符合这两个条件的话,finally中的代码就无法被执行,如发生以下情况,都会导致finally不会执行:
1、System.exit()方法被执行
2、Runtime.getRuntime().halt()方法被执行
3、try或者catch中有死循环
4、操作系统强制杀掉了JVM进程,如执行了kill -9
5、其他原因导致的虚拟机崩溃了
6、虚拟机所运行的环境挂了,如计算机电源断了
7、finally块即将被后台线程(deamon)执行前,其他的所有非后台线程都已执行完了
集合
ArrayList、LinkedList与Vector的区别?
List主要有ArrayList、LinkedList与Vector几种实现。这三者都实现了List 接口,使用方式也很相似,主要区别在于因为实现方式的不同,所以对不同的操作具有不同的效率。
ArrayList 是一个可改变大小的数组.当更多的元素加入到ArrayList中时,其大小将会动态地增长.内部的元素可以直接通过get与set方法进行访问,因为ArrayList本质上就是一个数组。
LinkedList 是一个双向链表,在添加和删除元素时具有比ArrayList更好的性能,但在get与set方面弱于ArrayList。当然,这些对比都是指数据量很大或者操作很频繁的情况下的对比,如果数据和运算量很小,那么对比将失去意义。
Vector 和ArrayList类似,但属于强同步类。如果你的程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用ArrayList是更好的选择。
Vector和ArrayList在更多元素添加进来时会请求更大的空间。Vector每次请求其大小的双倍空间,而ArrayList每次对size增长50%。
而 LinkedList 还实现了Queue和Deque接口,该接口比List提供了更多的方法,包括offer(),peek(),poll()等。
注意: 默认情况下ArrayList的初始容量非常小,所以如果可以预估数据量的话,分配一个较大的初始值属于最佳实践,这样可以减少调整大小的开销。
ArrayList是如何扩容的?
首先,我们要明白ArrayList是基于数组的,我们都知道,申请数组的时候,只能申请一个定长的数组,那么List是如何通过数组扩容的呢?ArrayList的扩容分为以下几步:
- 检查新增元素后是否会超过数组的容量,如果超过,则进行下一步扩容
- 设置新的容量为老容量的1.5倍,最多不超过2^31-1 (Java 8中ArrayList的容量最大是Integer.MAX_VALUE - 8,即2^31-9。这是由于在Java 8中,ArrayList内部实现进行了一些改进,使用了一些数组复制的技巧来提高性能和内存利用率,而这些技巧需要额外的8个元素的空间来进行优化。)
- 之后,申请一个容量为1.5倍的数组,并将老数组的元素复制到新数组中,扩容完成
如何利用List实现LRU?
LRU,即最近最少使用策略,基于时空局部性原理(最近访问的,未来也会被访问),往往作为缓存淘汰的策略,如Redis和GuavaMap都使用了这种淘汰策略。
我们可以基于LinkedList来实现LRU,因为LinkedList基于双向链表,每个结点都会记录上一个和下一个的节点,具体实现方式如下:
public class LruListCache<E> {
private final int maxSize;
private final LinkedList<E> list = new LinkedList<>();
public LruListCache(int maxSize) {
this.maxSize = maxSize;
}
public void add(E e) {
if (list.size() < maxSize) {
list.addFirst(e);
} else {
list.removeLast();
list.addFirst(e);
}
}
public E get(int index) {
E e = list.get(index);
list.remove(e);
add(e);
return e;
}
@Override
public String toString() {
return list.toString();
}
}
ArrayList的subList方法有什么需要注意的地方吗?
List的subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示。所以我们不能把subList方法返回的List强制转换成ArrayList等类。
视图和原List的修改还需要注意几点,尤其是他们之间的相互影响:
1、对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。
2、对子List做结构性修改,操作同样会反映到父List上。
3、对父List做结构性修改,会抛出异常ConcurrentModificationException。
ArrayList的序列化是怎么实现的?
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
ArrayList底层是通过Object数组完成数据存储的,但是这个数组被声明成了 transient,说明在默认的序列化策略中并没有序列化数组字段。
ArrayList重写了writeObject和readObject方法,如下所示:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
ConcurrentHashMap在哪些地方做了并发控制?
jdk1.8中ConcurrentHashMap是通过synchnized和CAS自旋保证的线程安全。要想知道ConcurrentHashMap是如何加锁的,就要知道HashMap在哪些地方会导致线程安全问题,如初始化桶数组阶段和设置桶,插入链表,树化等阶段,都会有并发问题。
解决这些问题的前提,就要知道到底有多少线程在对map进行写入操作,这里ConcurrentHashMap通过sizeCtl变量完成,如果其为负数,则说明有多线程在操作,且Math.abs(sizeCtl)即为线程的数目。
初始化桶阶段:
如果在此阶段不做并发控制,那么极有可能出现多个线程都去初始化桶的问题,导致内存浪费。所以Map在此处采用自旋操作和CAS操作,如果此时没有线程初始化,则去初始化,否则当前线程让出CPU时间片,等待下一次唤醒,源码如下
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 省略
}
} finally {
sizeCtl = sc;
}
break;
}
}
put元素阶段
如果hash后发现桶中没有值,则会直接采用CAS插入并且返回
如果发现桶中有值,则对流程按照当前的桶节点为维度进行加锁,将值插入链表或者红黑树中,源码如下:
// 省略....
// 如果当前桶节点为null,直接CAS插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
// 省略....
// 如果桶节点不为空,则对当前桶进行加锁
else {
V oldVal = null;
synchronized (f) {
}
}
扩容阶段
多线程最大的好处就是可以充分利用CPU的核数,带来更高的性能,所以ConcurrentHashMap并没有一味的通过CAS或者锁去限制多线程,在扩容阶段,ConcurrentHashMap就通过多线程来加加速扩容。
在分析之前,我们需要知道两件事情:
- ConcurrentHashMap通过ForwardingNode来记录当前已经桶是否被迁移,如果oldTable[i] instanceOf ForwardingNode则说明处于i节点的桶已经被移动到newTable中了。它里面有一个变量nextTable,指向的是下一次扩容后的table
- transferIndex记录了当前扩容的桶索引,最开始为oldTable.length,它给下一个线程指定了要扩容的节点
得知到这两点后,我们可以梳理出如下扩容流程:
- 通过CPU核数为每个线程计算划分任务,每个线程最少的任务是迁移16个桶
- 将当前桶扩容的索引transferIndex赋值给当前线程,如果索引小于0,则说明扩容完毕,结束流程,否则
- 再将当前线程扩容后的索引赋值给transferIndex,譬如,如果transferIndex原来是32,那么赋值之后transferIndex应该变为16,这样下一个线程就可以从16开始扩容了。这里有一个小问题,如果两个线程同时拿到同一段范围之后,该怎么处理?答案是ConcurrentHashMap会通过CAS对transferIndex进行设置,只可能有一个成功,所以就不会存在上面的问题
- 之后就可以对真正的扩容流程进行加锁操作了
ConcurrentHashMap是如何保证fail-safe的?
在 JDK 1.8 中,ConcurrentHashMap作为一个并发容器,他是解决了fail-fast的问题的,也就是说,他是一个fail-safe的容器。 通过以下两种机制来实现 fail-safe 特性:
首先,在 ConcurrentHashMap 中,遍历操作返回的是弱一致性迭代器,这种迭代器的特点是,可以获取到在迭代器创建后被添加到 ConcurrentHashMap 中的元素,但不保证一定能获取到在迭代器创建后被删除的元素。
也就是说,在迭代器遍历时,如果发现当前元素的 hash 值为 MOVED,即该元素所在的哈希桶正在进行扩容操作,那么就会帮助扩容操作,然后重新遍历当前哈希桶。
另外。在 JDK 1.8 中,ConcurrentHashMap 中的 Segment 被移除了,取而代之的是使用类似于cas+synchronized的机制来实现并发访问。在遍历 ConcurrentHashMap 时,只需要获取每个桶的头结点即可,因为每个桶的头结点是原子更新的,不会被其他线程修改,因此不需要加锁。
也就是说,ConcurrentHashMap 通过弱一致性迭代器和 Segment 分离机制来实现 fail-safe 特性,可以保证在遍历时不会受到其他线程修改的影响。
ConcurrentHashMap是如何保证线程安全的?
在JDK 1.7中,ConcurrentHashMap使用了分段锁技术,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。
虽然JDK 1.7的这种方式可以减少锁竞争,但是在高并发场景下,仍然会出现锁竞争,从而导致性能下降。
在JDK 1.8中,ConcurrentHashMap的实现方式进行了改进,使用分段锁和“CAS+Synchronized”的机制来保证线程安全。在JDK 1.8中,ConcurrentHashMap会在添加或删除元素时,首先使用CAS操作来尝试修改元素,如果CAS操作失败,则使用Synchronized锁住当前槽,再次尝试put或者delete。这样可以避免分段锁机制下的锁粒度太大,以及在高并发场景下,由于线程数量过多导致的锁竞争问题,提高了并发性能。
ConcurrentHashMap将哈希表分成多个段,每个段拥有一个独立的锁,这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。下面是jdk1.7ConcurrentHashMap中分段锁的代码实现:
static final class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
// ...
}
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
在上面的代码中,我们可以看到,每个Segment都是ReentrantLock的实现,每个Segment包含一个HashEntry数组,每个HashEntry则包含一个key-value键值对。
接下来再看下在JDK 1.8中,ConcurrentHashMap使用了一种称为“CAS+Synchronized”的机制。在添加或删除元素时,首先使用CAS操作来尝试修改元素,如果CAS操作失败,则使用Synchronized锁住整个段,再次尝试修改元素。下面是ConcurrentHashMap中CAS+Synchronized机制的代码实现:
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
// 对 key 的 hashCode 进行扰动
int hash = spread(key.hashCode());
int binCount = 0;
// 循环操作
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果 table 为 null 或长度为 0,则进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 如果哈希槽为空,则通过 CAS 操作尝试插入新节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 如果哈希槽处已经有节点,且 hash 值为 MOVED,则说明正在进行扩容,需要帮助迁移数据
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果哈希槽处已经有节点,且 hash 值不为 MOVED,则进行链表/红黑树的节点遍历或插入操作
else {
V oldVal = null;
// 加锁,确保只有一个线程操作该节点的链表/红黑树
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 遍历链表,找到相同 key 的节点,更新值或插入新节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
// 将新节点插入到链表末尾
if (casNext(pred, new Node<K,V>(hash, key,
value, null))) {
break;
}
}
}
}
// 遍历红黑树,找到相同 key 的节点,更新值或插入新节点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 如果插入或更新成功,则进行可能的红黑树化操作
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 如果替换旧值成功,则返回旧值
if (oldVal != null)
return oldVal;
break;
}
}
}
//
在上述代码中,如果某个段为空,那么使用CAS操作来添加新节点;如果某个段中的第一个节点的hash值为MOVED,表示当前段正在进行扩容操作,那么就调用helpTransfer方法来协助扩容;否则,使用Synchronized锁住当前节点,然后进行节点的添加操作。
HashMap、Hashtable和ConcurrentHashMap的区别?
线程安全:
HashMap是非线程安全的。
Hashtable 中的方法是同步的,所以它是线程安全的。
ConcurrentHashMap在JDK 1.8之前使用分段锁保证线程安全, ConcurrentHashMap默认情况下将hash表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。锁的粒度更细,所以他的性能更好。
ConcurrentHashMap在JDK 1.8中,采用了一种新的方式来实现线程安全,即使用了CAS+synchronized,这个实现被称为"分段锁"的变种,也被称为"锁分离",它将锁定粒度更细,把锁的粒度从整个Map降低到了单个桶。
继承关系:
HashTable是基于陈旧的Dictionary类继承来的。
HashMap继承的抽象类AbstractMap实现了Map接口。
ConcurrentHashMap同样继承了抽象类AbstractMap,并且实现了ConcurrentMap接口。
允不允许null值:
HashTable中,key和value都不允许出现null值,否则会抛出NullPointerException异常。
HashMap中,null可以作为键或者值都可以。
ConcurrentHashMap中,key和value都不允许为null。
默认初始容量和扩容机制:
HashMap的默认初始容量为16,默认的加载因子为0.75,即当HashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并将原来的元素重新分配到新的桶中。
Hashtable,默认初始容量为11,默认的加载因子为0.75,即当Hashtable中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍加1,并将原来的元素重新分配到新的桶中。
ConcurrentHashMap,默认初始容量为16,默认的加载因子为0.75,即当ConcurrentHashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并会采用分段锁机制,将ConcurrentHashMap分为多个段(segment),每个段独立进行扩容操作,避免了整个ConcurrentHashMap的锁竞争。
遍历方式的内部实现上不同 :
HashMap使用EntrySet进行遍历,即先获取到HashMap中所有的键值对(Entry),然后遍历Entry集合。支持fail-fast,也就是说在遍历过程中,若HashMap的结构被修改(添加或删除元素),则会抛出ConcurrentModificationException。如果只需要遍历HashMap中的key或value,可以使用KeySet或Values来遍历。
Hashtable使用Enumeration进行遍历,即获取Hashtable中所有的key,然后遍历key集合。遍历过程中,Hashtable的结构发生变化时,Enumeration会失效。
ConcurrentHashMap使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap的某个段被修改不会影响其他段的遍历。可以使用EntrySet、KeySet或Values来遍历ConcurrentHashMap,其中EntrySet遍历时效率最高。遍历过程中,ConcurrentHashMap的结构发生变化时,不会抛出ConcurrentModificationException异常,但是在遍历时可能会出现数据不一致的情况,因为遍历器仅提供了弱一致性保障。
HashMap在get和put时经过哪些步骤?
对于HashMap来说,底层是基于散列算法实现,散列算法分为散列再探测和拉链式。HashMap 则使用了拉链式的散列算法,即采用数组+链表/红黑树来解决hash冲突,数组是HashMap的主体,链表主要用来解决哈希冲突。这个数组是Entry类型,它是HashMap的内部类,每一个Entry包含一个key-value键值对
get方法
对于get方法来说,会先查找桶,如果hash值相同并且key值相同,则返回该node节点,如果不同,则当node.next!=null时,判断是红黑树还是链表,之后根据相应方法进行查找。
put方法
对于put方法来说,一般经过以下几步
- 如果数组没有被初始化,先初始化数组
- 首先通过定位到要put的key在哪个桶中,如果该桶中没有元素,则将该要put的entry放置在该桶中
- 如果该桶中已经有元素,则遍历该桶所属的链表:
- 如果该链表已经树化,则执行红黑树的插入流程
- 如果仍然是链表,则执行链表的插入流程,如果插入后链表的长度大于等于8,并且桶数组的容量大于等于64,则执行链表的树化流程
- 注意:在上面的步骤中,如果元素和要put的元素相同,则直接替换
- 校验是新增KV还是替换老的KV,如果是后者,则设置callback扩展(LinkedHashMap的LRU即通过此实现)
- 校验++size是否超过threshold,如果超过,则执行扩容流程(见下会分解~)
end
线程
AQS是如何实现线程的等待和唤醒的?
AQS(AbstractQueuedSynchronizer)是Java中实现锁和同步器的基础类,通过FIFO双向队列来管理等待线程和阻塞线程,实现线程之间的协作。
AQS中线程等待和唤醒主要依赖park和unpark实现的。
当一个线程尝试获取锁或者同步器时,如果获取失败,AQS会将该线程封装成一个Node并添加到等待队列中,然后通过LockSupport.park()将该线程阻塞。
当一个线程释放锁或者同步器时,AQS会通过LockSupport.unpark()方法将等待队列中的第一个线程唤醒,并让其重新尝试获取锁或者同步器。
除了基本的等待和唤醒机制,AQS还提供了条件变量(Condition)的实现,用于在某些条件不满足时让线程等待,并在条件满足时唤醒线程。具体实现是通过创建一个等待队列,将等待的线程封装成Node并添加到队列中,然后将这些线程从同步队列中移除,并在条件满足时将等待队列中的所有线程唤醒。
park&unpark
Java中的park()和unpark()方法是一对用于线程等待和唤醒的方法,一般用于实现锁、信号量、线程池等高级并发组件。
park()方法可以使调用线程进入休眠状态,等待被其他线程唤醒,具体实现会让线程进入等待队列中,等待被唤醒。park()方法可以通过传入一个Object类型的参数进行阻塞,这个参数是用来标识这个线程阻塞的原因,方便调试和排查问题。
unpark()方法可以使某个被阻塞的线程被唤醒,让其继续执行。unpark()方法需要传入一个Thread类型的参数,表示要唤醒的线程。
CAS一定有自旋吗?
CAS 操作都会采用自旋的方式,当 CAS 失败时,会重新尝试执行 CAS 操作,直到操作成功或达到最大重试次数为止。
因为,CAS 操作一般都是在多线程并发访问时使用,如果直接阻塞线程,会导致性能下降,而采用自旋的方式,可以让 CPU 空转一段时间,等待锁被释放,从而避免线程切换和阻塞的开销。
但是,如果自旋时间过长或者线程数过多,就会占用过多的 CPU 资源,导致系统性能下降,因此在使用 CAS 操作时,需要根据实际情况进行适当的调整。
CAS在操作系统层面是如何保证原子性的?
CAS是一种基本的原子操作,用于解决并发问题。在操作系统层面,CAS 操作的原理是基于硬件提供的原子操作指令。在x86架构的CPU中,CAS 操作通常使用 cmpxchg 指令实现。
为啥cmpxchg指令可以保证原子性呢?主要由以下几个方面的保障:
-
cmpxchg 指令是一条原子指令。在 CPU 执行 cmpxchg 指令时,处理器会自动锁定总线,防止其他 CPU 访问共享变量,然后执行比较和交换操作,最后释放总线。
-
cmpxchg 指令在执行期间,CPU 会自动禁止中断。这样可以确保 CAS 操作的原子性,避免中断或其他干扰对操作的影响。
-
cmpxchg 指令是硬件实现的,可以保证其原子性和正确性。CPU 中的硬件电路确保了 cmpxchg 指令的正确执行,以及对共享变量的访问是原子的。
CompletableFuture的底层是如何实现的?
CompletableFuture是Java 8中引入的一个新特性,它提供了一种简单的方法来实现异步编程和任务组合。他的底层实现主要涉及到了几个重要的技术手段,如Completion链式异步处理、事件驱动、ForkJoinPool线程池、以及CountDownLatch控制计算状态、通过CompletionException捕获异常等。
CompletableFuture 内部采用了一种链式的结构来处理异步计算的结果,每个 CompletableFuture 都有一个与之关联的 Completion 链,它可以包含多个 Completion 阶段,每个阶段都代表一个异步操作,并且可以指定它所依赖的前一个阶段的计算结果。(在 CompletableFuture 类中,定义了一个内部类 Completion,它表示 Completion 链的一个阶段,其中包含了前一个阶段的计算结果、下一个阶段的计算操作以及执行计算操作的线程池等信息。)
CompletableFuture 还使用了一种事件驱动的机制来处理异步计算的完成事件。在一个 CompletableFuture 对象上注册的 Completion 阶段完成后,它会触发一个完成事件,然后 CompletableFuture 对象会执行与之关联的下一个 Completion 阶段。
CompletableFuture 的异步计算是通过线程池来实现的。CompletableFuture在内部使用了一个ForkJoinPool线程池来执行异步任务。当我们创建一个CompletableFuture对象时,它会在内部创建一个任务,并提交到ForkJoinPool中去执行。
在 CompletableFuture 的异步计算过程中,会先执行前一个阶段的计算,然后将计算结果传递给下一个阶段。在执行计算过程中,会通过 CompletableFuture 内部的 CountDownLatch 来控制计算的完成状态,并通过 CompletableFuture 内部的 CompletionException 来捕获计算过程中出现的异常。
CountDownLatch、CyclicBarrier、Semaphore区别?
CountDownLatch、CyclicBarrier、Semaphore都是Java并发库中的同步辅助类,它们都可以用来协调多个线程之间的执行。
但是,它们三者之间还是有一些区别的:
CountDownLatch是一个计数器,它允许一个或多个线程等待其他线程完成操作。它通常用来实现一个线程等待其他多个线程完成操作之后再继续执行的操作。
CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。它通常用来实现多个线程在同一个屏障处等待,然后再一起继续执行的操作。
Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成操作的操作。
CountDownLatch适用于一个线程等待多个线程完成操作的情况
CyclicBarrier适用于多个线程在同一个屏障处等待
Semaphere适用于一个线程需要等待获取许可证才能访问共享
ForkJoinPool和ExecutorService区别是什么?
ForkJoinPool和ExecutorService都是Java中常用的线程池的实现,他们主要在实现方式上有一定的区别,所以也就会同时带来的适用场景上面的区别。
首先在实现方式上,ForkJoinPool 是基于工作窃取(Work-Stealing)算法实现的线程池,ForkJoinPool 中每个线程都有自己的工作队列,用于存储待执行的任务。当一个线程执行完自己的任务之后,会从其他线程的工作队列中窃取任务执行,以此来实现任务的动态均衡和线程的利用率最大化。
ExecutorService 是基于任务分配(Task-Assignment)算法实现的线程池,ExecutorService 中线程池中有一个共享的工作队列,所有任务都将提交到这个队列中。线程池中的线程会从队列中获取任务执行,如果队列为空,则线程会等待,直到队列中有任务为止。
ForkJoinPool 中的任务通常是一些可以分割成多个子任务的任务,例如递归地计算斐波那契数列。每个任务都可以分成两个或多个子任务,然后由不同的线程来执行这些子任务。在这个过程中,ForkJoinPool 会自动管理任务的执行、分割和合并,从而实现任务的动态分配和最优化执行。
ForkJoinPool 中的工作线程是一种特殊的线程,与普通线程池中的工作线程有所不同。它们会自动地创建和销毁,以及自动地管理线程的数量和调度。这种方式可以降低线程池的管理成本,提高线程的利用率和并行度。
ExecutorService 中线程的创建和销毁是静态的,线程池创建后会预先创建一定数量的线程,根据任务的数量动态调整线程的利用率,不会销毁线程。如果线程长时间处于空闲状态,可能会占用过多的资源。
在使用场景上也有区别,ForkJoinPool 适用于处理大量、独立、可分解的任务,并且任务之间不存在依赖关系。例如,计算斐波那契数列、归并排序、图像处理等任务。
ExecutorService 适用于处理较小的、相对独立的任务,任务之间存在一定的依赖关系。例如,处理网络请求、读取文件、执行数据库操作等任务。
Spring
Autowired和Resource的关系?
- 对于下面的代码来说,如果是Spring容器的话,两个注解的功能基本是等价的,他们都可以将bean注入到对应的field中
@Autowired
private Bean beanA;
@Resource
private Bean beanB;
不同点
- Autowired在获取bean的时候,先是byType的方式,再是byName的方式。意思就是先在Spring容器中找以Bean为类型的Bean实例,如果找不到或者找到多个bean,则会通过fieldName来找。举个例子:
@Component("beanOne")
class BeanOne implements Bean {}
@Component("beanTwo")
class BeanTwo implements Bean {}
@Service
class Test {
// 此时会报错,先byType找到两个bean:beanOne和beanTwo
// 然后通过byName(bean)仍然没办法匹配
@Autowired
private Bean bean;
// 先byType找到两个bean,然后通过byName确认最后要注入的bean
@Autowired
private Bean beanOne;
// 先byType找到两个bean,然后通过byName确认最后要注入的bean
@Autowired
@Qualifier("beanOne")
private Bean bean;
}
-
Resource在获取bean的时候,和Autowired恰好相反,先是byName方式,然后再是byType方式。当然,我们也可以通过注解中的参数显示指定通过哪种方式。同样举个例子:
@Component("beanOne") class BeanOne implements Bean {} @Component("beanTwo") class BeanTwo implements Bean {} @Service class Test { // 此时会报错,先byName,发现没有找到bean // 然后通过byType找到了两个Bean:beanOne和beanTwo,仍然没办法匹配 @Resource private Bean bean; // 先byName直接找到了beanOne,然后注入 @Resource private Bean beanOne; // 显示通过byType注入,能注入成功 @Resource(type = BeanOne.class) private Bean beanOne; }作用域不同
- Autowired可以作用在构造器,字段,setter方法上
- Resource 只可以使用在field,setter方法上
支持方不同
-
Autowired是Spring提供的自动注入注解,只有Spring容器会支持,如果做容器迁移,是需要修改代码的
-
Resource是JDK官方提供的自动注入注解(JSR-250)。它等于说是一个标准或者约定,所有的IOC容器都会支持这个注解。假如系统容器从Spring迁移到其他IOC容器中,是不需要修改代码的
BeanFactory和FactroyBean的关系?
他们的区别比较容易理解,从字面意思就能区分开发,BeanFactory是Bean工厂,而FactroyBean是工厂Bean。
BeanFactory,Spring中工厂的顶层规范,他是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。它定义了getBean()、containsBean()等管理Bean的通用方法。
Spring 容器中有两种Bean:普通Bean和工厂Bean。Spring直接使用前者,FactoryBean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该FactoryBean的getObject方法所返回的对象。
public interface FactoryBean<T> {
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
Spring通过反射机制利用的class属性指定的实现类来实例化bean 。在某些情况下,实例化bean过程比较复杂,如果按照传统的方式,则需要在中提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可能会得到更好的效果。Spring为此提供了一个org.Springframework.beans.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化bean的逻辑。
Spring框架本身就自带了实现FactoryBean的70多个接口,如ProxyFactoryBean、MapFactoryBean、PropertiesFactoryBean等