Java面试常见问题一(非常详细)

问题一:集合你平时用的比较多的有哪些?

Collection 下的 List ArrayList LinkedList
Set集合的话是HashSet
Map 包中的HashMap, 设计到线程安全的话可能会用到ConcurrentHashMap在这里插入图片描述
List 有序 可重复
Set 无序 不可重复 值不相等的元素

问题二:先聊一下ArrayList和LinkedList吧 使用场景

从底层数据结构、线程安全性来描述
ArrayList
1、基于数组实现,查找 访问速度快(基于索引查找) 增删效率低 因为移动元素
2、线程不安全
3、初始容量:创建一个集合时,集合的初始容量为0,在第一次添加元素的时候,会对集合进行扩容,扩容之后,集合容量为10;之后,当向集合中添加元素达到集合的上限时,会对集合再次扩容,扩容为**((旧容量 * 3) / 2) + 1**(JDK1.6) JDK1.7 为 1.5倍。
LinkedList
1、基于带有头尾结点的双向链表实现,提供了头插 LinkedFirst , 尾插是LinkedLast
​ 适合插入删除比较频繁的情况,查询量大的时候比较慢(链表查询 按顺序从第一个开始比较)内部维护了链表的长度
2、线程不安全

问题三:线程安全的情况下,想使用List,怎么用?

Vector 是线程安全的,底层是数组,大部分的方法都是被Synchronized关键字所修饰,线程安全
扩容默认是2倍

问题四:说一下你对HashMap的了解

从底层数据结构、初始化核⼼参数、hash算法、寻址算法、hash冲突、扩容机制、put⽅法,get⽅法和remove⽅法执⾏流程、hashMap是否是线程安全 等角度进行描述

  1. 底层数据结构

    1.7 数组+链表

    1.8 数组+链表+红黑树 单链表长度>=8, hash桶 >= 64 会将单链表转换成红黑树存储 如果红黑树结点<=6,重新转成单链表

    为什么链表转红黑树的阈值是8?

    ​ 阈值为8是在时间和空间上权衡的结果

    为什么转回链表节点是用的6而不是8?

    ​ 如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。在这里插入图片描述
    在这里插入图片描述

  2. 初始化核心参数

    1)capacity:数组容量,默认初始容量是16,HashMap 的容量必须是2的N次方,可以扩容,扩容后数组大小为当前的 2 倍;

    2)threshold:扩容阈值,当 HashMap 的个数达到该值,触发扩容。

    3)loadFactor:负载因子,默认值是0.75,扩容阈值 = 容量 * 负载因子。

    默认情况:初始大小为 16 ,扩容因子 0.75 ,当容量为12的时候,比例已经是0.75 。触发扩容,扩容后的大小为 32

    为什么HashMap 的容量必须是 2 的 N 次方?

    计算机底层是二进制的,移位和或运算是非常快的,

    计算索引位置的公式为:(n - 1) & hash,当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值,如下图第一行,此时任何值跟 n - 1 进行 & 运算会等于其本身,达到了和取模同样的效果,实现了均匀分布。实际上,这个设计就是基于公式:x mod 2^n = x & (2^n - 1),因为 & 运算比 mod 具有更高的效率。如下图,当 n 不为 2 的 N 次方时,hash 冲突的概率明显增大。
    图片
    为什么负载因子是0.75 ?

    时间和空间上权衡的结果。如果值较高,例如1,此时会减少空间开销,但是 hash 冲突的概率会增大,增加查找成本;

    而如果值较低,例如 0.5 ,此时 hash 冲突会降低,但是有一半的空间会被浪费,所以折衷考虑 0.75 似乎是一个合理的值。

  3. hash算法

    拿到 key 的 hashCode,并将 hashCode 的高16位和 hashCode 进行异或(XOR)运算,得到最终的 hash 值。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    为什么要将 hashCode 的高16位参与运算?

    例如下图,如果不加入高位运算,由于 n - 1 是 0000 0111,所以结果只取决于 hash 值的低3位,无论高位怎么变化,结果都是一样的。图片
    如果我们将高位参与运算,则索引计算结果就不会仅取决于低位。
    图片

  4. 寻址算法

    (n-1)& hash

  5. hash冲突

    不同的关键字经过哈希函数运算之后可能获得相同的hash值,就发生了Hash冲突

    1)开放定址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

    ​ 线性探测再散列

    ​ 二次探测再散列

    ​ 伪随机探测再散列

    2)再哈希法:构造多个不同的哈希函数,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。

    3)链地址法:所有哈希地址为i的元素构成一个单链表

    4)建立公共溢出区:将哈希表分为基本表和溢出表两部分,将所有发生哈希冲突的记录都存储到溢出表。

  6. 扩容机制
    图片
    扩容之后如何定位到新表的位置呢?

    红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置

    为什么用e.hash & oldCap == 0 来定位?

    扩容前 table 的容量为16,a 节点和 b 节点在扩容前处于同一索引位置。
    图片
    扩容后,table 长度为32,新表的 n - 1 只比老表的 n - 1 在高位多了一个1(图中标红)。
    图片
    因为 2 个节点在老表是同一个索引位置,因此计算新表的索引位置时,只取决于新表在高位多出来的这一位(图中标红),而这一位的值刚好等于 oldCap。

    因为只取决于这一位,所以只会存在两种情况:

    1) (e.hash & oldCap) == 0 ,则新表索引位置为“原索引位置” ;

    2)(e.hash & oldCap) == 1,则新表索引位置为“原索引 + oldCap 位置”。

  7. put方法,get方法和remove方法执行流程
    put方法如下图
    图片

  8. hashMap是否是线程安全

线程不安全,HashMap 在并发下存在数据覆盖、遍历的同时进行修改会抛出 ConcurrentModificationException 异常等问题,

JDK 1.8 之前还存在死循环问题。导致死循环的根本原因是 JDK 1.7 扩容采用的是“头插法”,会导致同一索引位置的节点在扩容后顺序 反掉。而 JDK 1.8 之后采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。

1.7在put的时候有resize的一个过程,这个过程头插会形成环形链表导致一直死循环,导致CPU利用率接近100%

  1. JDK 1.8 的主要优化哪些地方

    1)底层数据结构从“数组+链表”改成“数组+链表+红黑树”,主要是优化了 hash 冲突较严重时,链表过长的查找性能:O(n) -> O(logn)。

    2)计算 table 初始容量的方式发生了改变,老的方式是从1开始不断向左进行移位运算,直到找到大于等于入参容量的值;新的方式则是通过“5个移位+或等于运算”来计算。

    3)优化了 hash 值的计算方式,新的只是简单的让高16位参与了运算

    4)扩容时插入方式从**“头插法”改成“尾插法”**,避免了并发下的死循环。

    5)扩容时计算节点在新表的索引位置方式从“h & (length-1)”改成“hash & oldCap”,性能可能提升不大,但设计更巧妙、更优雅。

问题五:HashMap线程不安全,如何处理保证线程安全?

使用ConcurrentHashMap 保证线程安全

**回答思路:**底层数据结构、hash算法、寻址算法、put⽅法流程,get⽅法,remove⽅法,size⽅法、jdk 1.7和1.8底层实现区别,1.7如何去锁segment

分段锁:整个 ConcurrentHashMap 由一个个 Segment 组成,

1.7 为了保证线程安全,采用分段锁,Segment 继承于 ReetrantLock ,

1.8 数组+单链表+红黑树的数据结构 1.8之后逐渐放弃分段锁机制,而是采用Synchronized+CAS 机制来实现

1.7 的底层数据结构

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
在这里插入图片描述
ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:

 static final class HashEntry<K,V> {  
     final K key;  
     final int hash;  
     volatile V value;  
     final HashEntry next;  
 } 

可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next 引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。

但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。这在讲解删除操作时还会详述。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。

问:1.7分段锁 如果要找到某个具体的值,需要经过几次哈希?怎么去找到具体数值的呢?

在读取和写入的时候需要需要做两次哈希,但这两次哈希换来的是更细力粒度的锁,也就意味着可以支持更高的并发

那么在插入和获取元素的时候,必须先通过哈希算法定位到Segment。可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次再哈希再哈希,其目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。

(1)确定元素在segment数组中的位置

put方法为例,假如segment数组的大小为2的n次方,则hash >>> segmentShift正好取了key的哈希值的高n位,再与掩码segmentMask相与相当与仍然用key的哈希的高位来确定数据项在segment数组中的位置。

(2)跟HashMap类似 ,找到HashEntry 数值中的位置

put操作
从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

get操作

ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

size操作
第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的;
第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。

扩容

  • **是否需要扩容。**在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阀值,数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。
  • **如何扩容。**扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

问:1.7 分段锁 在高并发下的情况下如何保证取得的元素是最新的?

用于存储键值对数据的HashEntry,在设计上它的成员变量value等都是volatile类型的,这样就保证别的线程对value值的修改,get方法可以马上看到。

问:1.8 底层数据结构 如何进行插入、获取元素、删除元素?

1.8 数组+单链表+红黑树的数据结构 1.8之后逐渐放弃分段锁机制,而是采用Synchronized+CAS 机制来实现
4102b1f94ad6ab6adc33293e31ff32543e9.jpg

这个put的过程很清晰,对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程
  2. 如果没有hash冲突就直接CAS插入
  3. 如果还在进行扩容操作就先进行扩容
  4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
  5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
  6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

ConcurrentHashMap 1.8 的get操作的流程很简单,也很清晰,可以分为三个步骤来描述

  1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
  2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

问:ConcurrentHashMap 1.8 做了哪些修改?

  1. JDK1.8取消了segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率。
  2. JDK1.8存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个的时候。
  3. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  4. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  5. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
  6. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,有以下几点:
    1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
    2. JVM1.6 对synchronized 锁进行了升级
    3. 在大量的数据操作下,基于API的ReentrantLock会开销更多的内存

问:什么是CAS ? 实际的应用?存在什么问题?

CAS 存在的问题:(1) 忙循环cpu开销大;(2) 只能保证一个共享变量原子操作(AtomicReference)(3) ABA 标志位 时间戳

实际应用:并发包 并发修改 底层源码

CAS 比较 交换 轻量级加锁的过程 锁竞争不激烈 低并发性能好一些

缺点:ABA 问题 两次读之间可能被其他线程修改过,这个问题可以通过添加一个时间戳或者标志位来解决, 加版本号、标志

为什么不用HashTable或Collection.Synchronized 来替代,为什么选择了ConcurrentHashMap

HashTable容器使用synchronized来保证线程安全,访问HashTable的线程都必须竞争同一把锁,线程竞争激烈的情况下HashTable的效率非常低下, 因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap1.7 , 容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率, 将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

ConcurrentHashMap 并发度更高

问题六:说一下Synchronized

回答思路:使用场景、保证特性(原子性、可见性、有序性、可重入性)、锁升级、底层的实现⽅式、与Lock的比较

  1. 使用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-leLTSe8I-1644506234806)(C:\Users\baoqi\Desktop\image-20220210124303936.png)]
在这里插入图片描述
两个对象同时对一个对象的同步方法进行操作,只有一个线程能抢到锁,只有一个线程能够抢到锁,因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,即其他线程不能访问该对象的其他synchronized 实例方法,但是可以访问该对象的非synchronized 修饰方法。

Synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么

Synchronized锁对象是存在哪里的呢?—— 是存在锁对象的对象头的MarkWord中。

那么MarkWord在对象头中到底长什么样,也就是它到底存储了什么呢?

img

在32位的虚拟机中:

img

  1. 保证特性(原子性、可见性、有序性、可重入性)

  2. 锁升级以及加锁过程

锁的状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
在这里插入图片描述
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,在虚拟机栈中开辟一个空间 Lock Record, 将锁对象的Mark Word 写入,再尝试将Lock Record 的指针 使用CAS去修改锁对象头区域,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

重量级锁,互斥锁,加锁过程:同步代码块,在编译之后,会在代码块前后加上两个指令monitorenter、monitorexit,一个线程来时,它发现这个对象图中,锁标志位是01,锁对象会跟监视器锁monitor对象进行关联,它会在monitor的一个锁定期加1,并且将monitor 对象的指针写入到对象头中,并修改锁对象标志位为10,而且是可重入的,再次获取锁不需要重复加锁释放锁,如果再来一个线程,他会检查这个锁对象头,monitor监视器上计数器不为0,它会在monitor 监视状态下去竞争这个锁,如果操作结束了,退出释放锁,并且逐步将加上的锁释放几次,将计数器清零,完成对锁的释放,其他线程去竞争这个锁。

锁升级的过程,大量的锁升级成重量级锁,但是过了高峰,但是回不去了,QPS 稳定 很高的情况下使用?

为什么要引入偏向锁?

—— 大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价

为什么要引入轻量级锁?

——竞争锁对象的线程不多,而且线程持有锁的时间也不长,线程需要CPU从用户态转到内核态代价较大,可以自旋这等待锁释放。

为什么要引入重量级锁?

——自旋的时间太长也不行,因为自旋是要消耗CPU的,自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转

img

  1. 底层的实现方式

1、当多个线程同时访问该方法,那么这些线程会先被放进EntryList队列,此时线程处于blocking状态
2、当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的owner指向当前线程,count加1表示当前对象锁被一个线程获取
3、当running状态线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的owner变为null,count减1,同时线程进入WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入Owner区

4、如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的owner变为null,_count减1
那么Synchronized修饰的代码块如何获取monitor对象的呢?

Synchronized代码块同步在需要同步的代码块开始的位置插入monitorenter指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。

那么Synchronized修饰的方法如何获取monitor对象的呢?

Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。

  1. 与Lock的比较

Synchronized 是JVM 关键字;ReentrantLock类手动编码 手动lock,配合try finally 代码块手动释放锁

ReentrantLock 高级特性:

1、ReentrantLock如何避免死锁:响应中断、可轮询锁、定时锁

(1)如果有一个线程长期等待不到一个锁,为了防止死锁,可以手动调用lockInterruptibly () 方法 释放自己的资源不去等待

(2)可轮询锁:通过boolean tryLock()获取锁。如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false
(3)定时锁:通过boolean tryLocklong time,TimeUnit unit)throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁
并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态。

2、提供了公平锁的方式,ReentrantLock(boolean fair)中传递不同的参数创建公平锁,默认是非公平锁,不推荐使用,使得ReentrantLock 性能下降

3、对外提供了Condition ,可以指定唤醒绑定到Condition身上的线程,实现选择性通知的机制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wr75ScDP-1644506234806)(C:\Users\baoqi\AppData\Roaming\Typora\typora-user-images\image-20220210144856541.png)]
在这里插入图片描述

问题六:JUC 下还用过哪些包?

CountDownLatch、CyclicBarrier 、Semaphore

CountDownLatch 适合于单线程在运行时,有一段过程又希望并发执行,后又回归到一个单线程状态,它适合一个线程去等待一批线程到达一个同步点,之后再继续进行

计数器时不能重用的

CyclicBarrier 类似,它是一批线程同时到达一个临界点,之后再往下走,计数器可以留下来的

CountDownLatch 是如何实现计数功能的?

高并发,对数据加减,会存在并发竞争,AQS 里面有个标志位,用volatile修饰

问:volatile有了解吗?

回答思路:MESI、Java内存模型JMM、可见性有序性、跳出死循环、AtomicInteger

volatile 是JVM 提供的最轻量级的一个关键字,CPU和内存之间的线程效率是差好多数量级的,但为了保证他们之间的计算,不影响CPU的计算,中间有好多像LLV 缓存,我们线程再这个缓存中去工作,首先它会取数据从主内存到工作内存,工作内存中计算,计算完之后传回去,这个时候就有问题,

多个线程之间的可见性是如何保证的?

计算机层面的话有好多协议,JVM 为了解决这些比较复杂的东西,提供了JMM模型,volatile 去修饰一个变量,保证这个变量在线程之间的可见性,修改这个变量之后,立刻刷到主内存,它在使用时会立刻从主内存中取出来刷新的那个值,volatile 是不能保证原子性的,像自增自减这种操作是不能保证数据安全的

那原子性怎么保证呢?

原子性可以使用Synchronized Reentrantlock 通过加锁来保证原子性 AtomicInteger


hashMap 知识转载自:https://mp.weixin.qq.com/s/wIjAj4rAAZccAl-yhmj_TA
Synchronized 知识转载自:https://blog.csdn.net/tongdanping/article/details/79647337

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值