java12312312

  • HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表

  • ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
    ArrayBlockingQueue 和 LinkedBlockingQueue 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:
    1.底层实现:ArrayBlockingQueue 基于数组实现,而 LinkedBlockingQueue 基于链表实现。是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue 创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。
    2.锁是否分离: ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以防止生产者和消费者线程之间的锁争夺。
    3.内存占用:ArrayBlockingQueue 需要提前分配数组内存,而 LinkedBlockingQueue 则是动态分配链表节点内存。这意味着,ArrayBlockingQueue 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue 则是根据元素的增加而逐渐占用内存空间。

  • hashMap和table区别:线程安全,是否允许null,初始容量map:16、table:11,解决hash冲突,链表8 && 数组64才会转红黑树

  • 为啥hashmap容量是2的次幂:取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方

  • 1.7 头插导致环形链表 ;1.8尾插法解决,但仍会数据覆盖

  • JDK1.8 的 ConcurrentHashMap 不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度(8)时,链表会转换成红黑树。
    1.8后采用 Node + CAS (Node的增删)+ synchronized 来保证并发安全。锁粒度更小,对链表或红黑树首节点加锁,只要hash不冲突,就能并发执行

  • jdk动态代理:基于接口实现,cglib基于继承实现
    1.当代理对象实现了接口时,Spring默认使用JDK的动态代理。
    2.当Bean没有实现接口时,Spring默认使用CGLIB动态代理。
    3.当代理对象实现了接口时,也可以强制使用CGLIB

  • 可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

  • volatile内存屏障:volatile写操作前ss,后sl;volatile读操作后ll和ls

  • CAS经典ABA,版本号和时间戳,AtomicStampedReference

  • synchronized实现,反编译字节码可以发现; 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。不过两者的本质都是对对象监视器 monitor 的获取。

  • 对象头(类元数据;GC、锁、哈希码)、实例数据、对齐填充(8字节倍数)

  • 美团技术博客,所相关:https://tech.meituan.com/2018/11/15/java-lock.html

  • 锁升级:当有线程访问同步代码,升级为偏向锁,状态变为01,对相同markword指向当前线程;又来一个线程,升级问轻量级锁(00),自旋尝试获取锁;自选一定次数还为获取到升级为重量级锁

  • synchronized 和 volatile 区别。后者只保证可见,不保证有序

  • 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitoropen in new window实现的。每个对象中都内置了一个 ObjectMonitor对象。

  • synchronized和ReentrantLock的区别和联系:
    都是可重入锁
    区别:
    1>前者是JVM层面实现的(1.6做的各种优化,锁升级等都是jvm层面的),后者是jdk层面实现的
    2>前者只能非公平,后者支持公平
    3>后者支持停止等待(这种特性也叫可中断锁f),前置只能获得锁后再干其他事

  • Java 中常用的锁主要有两类,一种是 Synchronized 修饰的锁,被称为 Java 内置锁或监视器锁。另一种就是在 J2SE 1.5版本之后的 java.util.concurrent包(下称j.u.c包)中的各类同步器,包括 ReentrantLock(可重入锁),ReentrantReadWriteLock(可重入读写锁),Semaphore(信号量),CountDownLatch 等

  • CLH锁(为了解决自旋锁中锁饥饿和中心化问题):https://mp.weixin.qq.com/s/jEx-4XhNGOFdCo4Nou5tqg。
    当一个节点释放锁时,只有它的后一个节点才可以得到锁。

  • 说下AQS:AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁实现的
    CLH是对自旋锁的优化(只记录了持有锁的状态和前驱节点),将线程持有状态分散到多个节点中,每个节点只能获取前一个节点的锁。AQS是对CLH的进一步优化,将每一个请求共享资源的线程封装成一个Node,每个node包含线程信息,当前节点在队列中的状态,前驱和后置节点信息

  • 在AQS(AbstractQueuedSynchronizer)框架中,waitStatus字段用于表示节点(线程)的状态,但它并不直接表示某个节点是否持有锁。waitStatus主要用于控制节点的等待状态,以及如何唤醒后续的节点。以下是waitStatus的四种主要状态及其含义:

  1. CANCELLED (1):表示当前的节点因为超时或中断而被取消。一旦设置为此状态,节点将不会再变化。
  2. SIGNAL (-1):表示后继节点处于等待状态,当前节点在释放或取消时必须解除后继节点的阻塞状态。
  3. CONDITION (-2):表示节点在等待条件队列中,节点线程等待在Condition上。
  4. PROPAGATE (-3):表示下一次acquireShared应该无条件地传播。

在AQS中,锁的持有情况是通过state字段来表示的,而不是通过waitStatusstate字段表示同步状态,对于独占模式的锁,state为0表示锁未被任何线程持有,大于0表示锁被某个线程持有;对于共享模式的锁,state的具体值则表示资源的可用状态,具体含义取决于实现。

  • threadlocal原理:每个线程中维护了一个threadLocalMap类型的变量,该变量的key是threadLocal,value是对应的object。

  • threadlocal内存泄漏问题 ,key是弱引用,value是强引用,出现key为null,value没被清掉。解决:调remove

  • 四种饱和策略:AbortPolicy(报错),callruns,discard(oldest)

  • 任务队列,无界队列(fix,single),延迟队列,同步队列(cachePool)
    在这里插入图片描述

  • 线程池大小
    CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

  • 在这里插入图片描述

  • NIO是一种基于通道的多路复用模型,它通过Selector监听多个Channel,当Channel准备好进行读写操作时,操作系统会通知应用程序,应用程序再进行相应的读写操作。这种方式相比传统的同步非阻塞I/O模型,在处理多个并发连接时更加高效,因为它减少了应用程序对I/O操作的轮询检查,从而减少了CPU的使用率,提高了应用程序的性能。

  • JDK 1.7 为什么要将字符串常量池移动到堆中?主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

  • 对象的创建过程:
    1 类检查,到运行时常量池中看类元数据是否被加载过,若没有需要重新加载类信息
    2 分配内存空间。类检查完毕后,对象的大小就能确定了,可以在对上分配空间,有两个方法:指针碰撞(内存规整时,指针向空闲区域移动一段距离)和空闲列表(维护了空闲的内存块,从中挑一块用)。内存是否规整是由使用了什么gc算法决定的。(标记清楚和标记整理)
    3 赋零值。给对象的实例字段赋初始值0
    4 初始化对象头:包括对象运行时的数据:gc分代信息、哈希码、。类元数据信息
    5 执行init方法,为变量赋值

  • 内存分配的并发问题:cas+重试 && tlab

  • 对象的访问定位:句柄(栈上的引用指向句柄池,句柄池中存有对象的内存地址和类元数据信息)、直接指针

  • 引用记数 && 可达性分析
    gc roots:栈上引用的对象,类的静态变量与常量,jni,锁持有的对象
    被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

  • 类被回收条件
    该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。加载该类的 ClassLoader 已经被回收。该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • gc方法:目前jvm使用的是分代收集算法,对于年轻代,变更频繁,使用复制算法。老年代,对象大,没有额外分配。使用标记清除和标记整理

  • Parallel Scavenge更加关注吞吐量,其他收集器更注重停顿时间

  • CMS,以减少停顿时间为有限,注重用户体验,因此尽量做到应用程序和垃圾收集线程同步执行(标记-清除算法)
    cms的gc流程:
    1 初始标记,暂停用户线程。记录与root直接相连的对象
    2 并发标记与root可达的对象,这一步和用户线程同时运行,
    3 重新标记,暂停用户线程,此步时间极短,为了解决第二步用户线程继续云翔导致的对象引用变化

  1. 并发 清理:标记清理
    缺点:内存碎片。jdk9已经被停用了
  • G1,G1和CMS的四步收集流程十分类似,维护了一个优先级列表,根据允许的停顿时间选择最值得回收的region来清理
    g1特点:1. 充分利用服务器多核的优势,并发的执行gc
    2 使用的是标记整理算法,局部看是标记复制
    3.可以自定义最小垃圾回收时间(可预测的停顿时间模型)
    4 保留了分代的概念

  • ZGC,,jdk15才出现,简单了解。stw时间更少,回收时间不受堆内存影响,但是牺牲了吞吐量

  • 类的生命周期:加载、链接、初始化在这里插入图片描述
    加载:1.根据类的全限定名读取class文件的二进制字节流。2.根据二进制字节流在方法区中创建数据结构。3.通过双亲委派机制生成class对象,作为收据访问的入口
    验证 验证合法性(class文件是否合法,防止恶意注入等)
    准备 初始化静态变量,赋0(final直接赋值)
    解析(字面量替换为直接引用)
    初始化 给类变量赋值,执行 ()

  • jvm自带的三个类加载器加载的类,都不会被回收,因为类被卸载回收的前提是没有引用的对象,和加载该类的加载器也被回收了

  • BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。

  • 双亲委派模型:先看类是否加载过,加载过直接返回,没加载过先委派给父类,一直到启动类加载器,父类无法加载才会下方给子类加载,避免核心api被污染

  • 动态线程池:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

  • 死锁四条件(互斥【资源只能被一个线程占用】、不可剥夺【不能被其他线程剥夺】、持有并保持、循环等待)
    解决:
    a.一段时间获取不到资源后主动释放 || 一次性申请所有的资源。(破坏持有并保持:例如mysql50秒后回滚事务)
    b.按顺序获取资源(先获取资源1再获取资源2,破坏循环等待)

  • 多线程的两种场景
    响应速度优先(例如访问某个页面):目的是为了尽快的返回商品的所有信息给用户,速度优先,尽量调大corePoolSize,尽快处理
    计算任务,虽然目的也是为了使计算尽可能快,但是由于任务计算量巨大,这里首要关注吞吐量。设置任务队列缓冲任务,避免上下文频繁切换导致的吞吐量降低

  • 扩容
    ArrayList
    初始容量:当我们创建一个ArrayList时,如果没有指定初始容量,默认的初始容量是10。
    扩容机制:当向ArrayList添加元素,且元素数量超过当前容量时,ArrayList会进行扩容。扩容的新容量是旧容量的1.5倍,即int newCapacity = oldCapacity + (oldCapacity >> 1)。
    扩容过程:扩容时,会创建一个新的数组,大小为新容量,然后将旧数组的元素复制到新数组中。
    HashMap
    初始容量和加载因子:HashMap有两个重要的参数:初始容量(默认16)和加载因子(默认0.75)。
    扩容机制:当HashMap中的元素数量超过容量与加载因子的乘积时,即元素数量 > 容量 * 加载因子,HashMap会进行扩容。扩容后的容量是原容量的两倍。
    扩容过程:扩容时,HashMap会创建一个新的数组,大小为新容量,然后将旧数组中的所有元素重新计算哈希并放入新数组中。
    HashSet
    HashSet底层是通过HashMap实现的,因此其扩容机制与HashMap相同。
    LinkedList
    LinkedList是基于链表实现的,不涉及扩容问题,因为它是通过节点的动态添加和删除来实现元素管理的。
    总结
    ArrayList:默认初始容量为10,扩容为原来的1.5倍。
    HashMap:默认初始容量为16,加载因子为0.75,扩容为原来的两倍。
    HashSet:扩容机制与HashMap相同。
    LinkedList:基于链表,不涉及扩容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值