三年JAVA最全最精华面试题

三年JAVA最全最精华面试题,第1张

JAVA基础

下载地址

文章目录
  • JAVA基础
    • 1. Hashcode()和 equals()和==区别?
    • 2. String 为什么要设计成不可变的?
    • 3. . 重写一个对象的 hashcode 和 equals 怎么权衡 ,hashcode 一定要 重写吗
    • 4. String 的 hashCode 是怎么计算的?
    • 5. String / StringBuffer / StringBuilder 区别
    • 6. Object 有哪些方法
    • 7.ArrayList 和 LinkedList 区别?
    • 8.ArrayList 有初始容量吗?你看的是 jdk 几版本?
    • 9. ArrayList容量不够怎么办 ,扩容为多大?
    • 10. 如果 Arraylist 当前容量是 10,且有 9 个数据,那么是添加第 10 个 数据时扩容还是第 11 个?
    • 11.Arraylist 扩容怎么实现的,为什么采用复制数组的方式而不是往后直 接添加数据?
    • 12. Arraylist 为什么数组加 transient
    • 13 .linkedlist 是单向的还是双向的,为什么这么设计
    • 14. arraylist 是线程安全的吗?如何实现线程安全
    • 15. HashMap 的底层数据结构是什么?
    • 16. 说一下 HashMap 的特点
    • 17. 解决 hash 冲突的办法有哪些?HashMap 用的哪种?
    • 18. 为什么要在数组长度大于 64 之后,链表才会进化为红黑树
    • 19. 哈希表底层采用何种算法计算 hash 值?还有哪些算法可以计算出 hash 值?
    • 20.hashmap当两个对象的 hashCode 相等时会怎样
    • 21.HashMap 的 put 方法流程
    • 22.计算 hash 值时为什么要让低 16bit 和高 16bit 进行异或处理
    • 23. 计划用 HashMap 存 1k 条数据,构造时传 1000 会触发扩容吗
    • 24. 计划用 HashMap 存 1w 条数据,构造时传 10000 会触发扩容吗
    • 25. 如何在 Linux 上查找哪个线程 cpu 利用率最高?
    • 26. 创建线程的方式
    • 27. 线程生命周期
    • 28. Java 中用到的线程调度算法是什么?
    • 29. sleep() 和 wait() 有什么区别?
    • 30.为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
    • 31. 如何停止一个正在运行的线程?
    • 32. Java 中 interrupted 和 isInterrupted 方法的区别
    • 33.Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取 线程堆栈?
    • 34. 一个线程运行时发生异常会怎样?
    • 35.synchronized 原理
      • 为什么会有两个 monitorexit 呢?
    • 36. synchronized 和 ReentrantLock 区别是什么?
    • 37. volatile
    • 38. final 什么是不可变对象,它对写并发应用有什么帮助?
    • 39. CAS 会产生什么问题?
    • 40. AQS 原理

1. Hashcode()和 equals()和==区别?
  1. hashcode()方法跟 equals()在 java 中都是判断两个对象是否相等
  2. 两个对象相同,则 hashcode 至一定要相同,即对象相同---->成员变量相同 ---->hashcode 值一定相同
  3. 两个对象的 hashcode 值相同,对象不一定相等。总结:equals 相等则 hashcode 一定相等,hashcode 相等,equals 不一定相等。
  4. ==比较的是两个引用在内存中指向的是不是同一对象(即同一内存空间)
2. String 为什么要设计成不可变的?
  1. 字符串常量池需要 String 不可变。 因为 String 设计成不可变,当创建一个 String 对象时,若此字符串值已经存在于 常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变 量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。
  2. String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性, 因此可以缓存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符 串比较时,可以直接比较 hashCode,提高了比较性能;
  3. 安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反 射机制所需的 Strign 参数等,若 String 可变,将会引起各种安全隐患。
3. . 重写一个对象的 hashcode 和 equals 怎么权衡 ,hashcode 一定要 重写吗

equals : 保证比较对象是否是绝对相等的

hashCode :保证在最快的时间内判断两个对象是否相等,可能有误差值 一个是保证可靠,一个是保证性能。也就是说: 同一个对象的 hashCode 一定相等,不同对象的 hashCode 也可能相等,这是因为 hashCode 是根据地址 hash 出来的一个 int 32 位的整型数字,相等是在所难免。

equals 比较的是两个对象的地址,同一个对象地址肯定相同,不同的对象地址一定不同, 可靠性是这么来的。

4. String 的 hashCode 是怎么计算的?

源码:

可以发现,计算 hashcode 的最主要代码是 h = 31 * h + val[i];

5. String / StringBuffer / StringBuilder 区别

String: String 的值是不可变的,这就导致每次对 String 的 *** 作都会生成新的 String 对象, 不仅效率低下,而且浪费大量优先的内存空间

StringBuffer: StringBuffer 是可变类,线程安全的,任何对它指向的字符串的 *** 作都不会 产生新的对象。每个 StringBuffer 对象都有一定的缓冲区容量,当字符串大小没有超过容量 时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量

StringBuilder: 可变类,速度更快,线程不安全

6. Object 有哪些方法
  1. clone 方法
    保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常。
  2. getClass 方法 final 方法,获得运行时类型。
  3. toString 方法 该方法用得比较多,一般子类都有覆盖。
  4. finalize 方法 该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
  5. equals 方法 该方法是非常重要的一个方法。一般 equals 和==是不一样的,但是在 Object 中两者是一 样的。子类一般都要重写这个方法。
  6. hashCode 方法 该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法。这个方法在一些 具有哈希功能的 Collection 中用到。 一般必须满足 obj1.equals(obj2)==true。可以推出 obj1.hash- Code()==obj2.hashCode(), 但是 hashCode 相等不一定就满足 equals。不过为了提高效率,应该尽量使上面两个条件 接近等价。
  7. wait 方法 wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有 该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超 时间隔,如果在规定时间内没有获得锁就返回。 调用该方法后当前线程进入睡眠状态,直到以下事件发生:
    1. 其他线程调用了该对象的 notify 方法。
    2. 其他线程调用了该对象的 notifyAll 方法。
    3. 其他线程调用了 interrupt 中断该线程。
    4. 时间间隔到了。 此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。
  8. notify 方法 该方法唤醒在该对象上等待的某个线程。
  9. notifyAll 方法 该方法唤醒在该对象上等待的所有线程。
7.ArrayList 和 LinkedList 区别?
  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程 安全;
  2. 底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表数据结构(JDK1.6 之前为双向循环链表,JDK1.7 取消了循环。)
  3. 插入和删除是否受元素位置的影响: arrayList 查找快(随机访问),修改快, linkedlist 增加和删除快
  4. 是否支持快速随机访问:LinkedList 不支持高效的随机元素访问,而 ArrayList 支 持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index)方法)。
  5. 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的 容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多 的空间(因为要存放直接后继和直接前驱以及数据)
8.ArrayList 有初始容量吗?你看的是 jdk 几版本?

1.8 版本初始容量为 0,第一次创建数组的时候如果没有指定大小,默认是 {} 就是个空数 组,只有当第一次添加的时候才扩容。 1.7 初始容量为 10

9. ArrayList容量不够怎么办 ,扩容为多大?

每次扩容大小为 1.5 倍

10. 如果 Arraylist 当前容量是 10,且有 9 个数据,那么是添加第 10 个 数据时扩容还是第 11 个?

添加第 11 个

11.Arraylist 扩容怎么实现的,为什么采用复制数组的方式而不是往后直 接添加数据?

12. Arraylist 为什么数组加 transient

假如 elementData 的长度为 10,而其中只有 5 个元素,那么在序列化的时候只需要存储 5 个元素,而数组中后面 5 个元素是不需要存储的。于是将 elementData 定义为 transient, 避免了 Java 自带的序列化机制,并定义了两个方法,实现了自己可控制的序列化 *** 作。

13 .linkedlist 是单向的还是双向的,为什么这么设计

双向链表,因为双向链表可以使用二分查找思想,查询效率高 删除节点的时候需要改变引用,就需要找到前一个节点的引用,双向链表已经存储了前一个 节点的引用

14. arraylist 是线程安全的吗?如何实现线程安全

不是

实现线程安全方式:

  1. Collections.synchronizedList
  2. CopyOnWriteArrayList 写时 CopyOnWriteArrayList 性能较差,且随着数据量的增大,几何级下跌。读 *** 作各方式 基本没有区别。 CopyOnWriteArrayList,适用于以读为主,读 *** 作远远大于写 *** 作的场景中使用,比如缓存。 Collections.synchronizedList 则可以用在 CopyOnWriteArrayList 不适用,但是有需要同步 的地方使用, 比如读写 *** 作都比较均匀的地方
15. HashMap 的底层数据结构是什么?

在 JDK1.7 中和 JDK1.8 中有所区别:

在 JDK1.7 中,由”数组+链表“组成,数组是 HashMap 的主体,链表则是主要为了解决哈希 冲突而存在的。

在 JDK1.8 中,有“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n)。

因此,JDK1.8 对数据结构做了进一步 的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换: 当链表超过 8 且数组长度(数据总量)超过 64 才会转为红黑树 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容, 而不是转换为红黑树,以减少搜索时间。

16. 说一下 HashMap 的特点
  1. hashmap 存取是无序的
  2. 键和值位置都可以是 null,但是键位置只能是一个 null
  3. 键位置是唯一的,底层的数据结构是控制键的
  4. jdk1.8 前数据结构是:链表+数组 jdk1.8 之后是:数组+链表+红黑树
  5. 阈值(边界值)>8 并且数组长度大于 64,才将链表转换成红黑树,变成红黑树的 目的是提高搜索速度,高效查询
17. 解决 hash 冲突的办法有哪些?HashMap 用的哪种?

解决 Hash 冲突方法有:开放定址法、再哈希法、链地址法(HashMap 中常见的拉链法)、 简历公共溢出区。

HashMap 中采用的是链地址法。

  1. 开放定址法也称为再散列法,基本思想就是,如果 p=H(key)出现冲突时,则以 p 为基础,再次 hash,p1=H§,如果 p1 再次出现冲突,则以 p1 为基础,以此类推,直到 找到一个不冲突的哈希地址 pi。因此开放定址法所需要的 hash 表的长度要大于等于所需要 存放的元素,而且因为存在再次 hash,所以只能在删除的节点上做标记,而不能真正删除 节点

  2. 再哈希法(双重散列,多重散列),提供多个不同的 hash 函数,R1=H1(key1)发 生冲突时,再计算 R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增 加了计算的时间。

  3. 链地址法(拉链法),将哈希值相同的元素构成一个同义词的单链表,并将单链 表的头指针存放在哈希表的第 i 个单元中,查找、插入和删除主要在同义词链表中进行,链 表法适用于经常进行插入和删除的情况。

  4. 建立公共溢出区,将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数 据统一放到溢出区 注意开放定址法和再哈希法的区别是 开放定址法只能使用同一种hash 函数进行再次 hash, 再哈希法可以调用多种不同的 hash 函数进行再次 hash

18. 为什么要在数组长度大于 64 之后,链表才会进化为红黑树

在数组比较小时如果出现红黑树结构,反而会降低效率,而红黑树需要进行左旋右旋,变色, 这些 *** 作来保持平衡,同时数组长度小于 64 时,搜索时间相对要快些,总之是为了加快搜 索速度,提高性能

JDK1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分 百均匀分布。当 HashMap 中有大量的元素都存放在同一个桶中时,这个桶下有一条长长的 链表,此时 HashMap 就相当于单链表,假如单链表有 n 个元素,遍历的时间复杂度就从 O (1)退化成 O(n),完全失去了它的优势,为了解决此种情况,JDK1.8 中引入了红黑树 (查找的时间复杂度为 O(logn))来优化这种问题

19. 哈希表底层采用何种算法计算 hash 值?还有哪些算法可以计算出 hash 值?

hashCode 方法是 Object 中的方法,所有的类都可以对其进行使用,首先底层通过调用 hashCode 方法生成初始 hash 值 h1,然后将 h1 无符号右移 16 位得到 h2,之后将 h1 与 h2 进行按位异或(^)运算得到最终 hash 值 h3,之后将 h3 与(length-1)进行按位与(&) 运算得到 hash 表索引

其他可以计算出 hash 值的算法有:平方取中法,取余数,伪随机数法

20.hashmap当两个对象的 hashCode 相等时会怎样

hashCode 相等产生 hash 碰撞,hashCode 相等会调用 equals 方法比较内容是否相等,内 容如果相等则会进行覆盖,内容如果不等则会连接到链表后方,链表长度超过 8 且数组长 度超过 64,会转变成红黑树节点

21.HashMap 的 put 方法流程
  1. 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标
  2. 如果数组是空的,则调用 resize 进行初始化;
  3. 如果没有哈希冲突直接放在对应的数组下标里
  4. 如果冲突了,且 key 已经存在,就覆盖掉 value
  5. 如果冲突后是链表结构,就判断该链表是否大于 8,如果大于 8 并且数组容量小于 64, 就进行扩容;如果链表节点数量大于 8 并且数组的容量大于 64,则将这个结构转换成红黑 树;否则,链表插入键值对,若 key 存在,就覆盖掉 value
  6. 如果冲突后,发现该节点是红黑树,就将这个节点挂起来
22.计算 hash 值时为什么要让低 16bit 和高 16bit 进行异或处理

我们计算索引需要将 hashCode 值与 length-1 进行按位与运算,如果数组长度很小,比如 16,这样的值和 hashCode 做异或实际上只有 hashCode 值的后 4 位在进行运算,hash 值 是一个随机值,而如果产生的 hashCode 值高位变化很大,而低位变化很小,那么有很大 概率造成哈希冲突,所以我们为了使元素更好的散列,将 hash 值的高位也利用起来

23. 计划用 HashMap 存 1k 条数据,构造时传 1000 会触发扩容吗

HashMap 初始容量指定为 1000,会被 tableSizeFor() 调整为 1024; 但是它只是表示 table 数组为 1024;

负载因子是 0.75,扩容阈值会在 resize() 中调整为 768(1024 * 0.75) 会触发扩容
如果需要存储 1k 的数据,应该传入 1000 / 0.75(1333) tableSizeFor() 方法调整到 2048,

不会触发扩容

24. 计划用 HashMap 存 1w 条数据,构造时传 10000 会触发扩容吗

当我们构造 HashMap 时,参数传入进来 1w 经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384 负载因子是 0.75f,可存储的数据容量是 12288(16384 * 0.75f) 完全够用,
不会触发扩容

25. 如何在 Linux 上查找哪个线程 cpu 利用率最高?

linux 下可以用 top 这个工具看。

  1. 找出 cpu 耗用厉害的进程 pid, 终端执行 top 命令,然后按下 shift+p 查找出 cpu 利用最厉害的 pid 号
  2. 根据上面第一步拿到的 pid 号,top -H -p pid 。然后按下 shift+p,查找出 cpu 利 用率最厉害的线程号,比如 top -H -p 1328
  3. 将获取到的线程号转换成 16 进制,去百度转换一下就行
  4. 使用 jstack 工具将进程信息打印输出,jstack pid 号 > /tmp/t.dat,比如 jstack 31365 > /tmp/t.dat 编辑/tmp/t.dat 文件,查找线程号对应的信息
26. 创建线程的方式
  1. 继承 Thread 类;
  2. 实现 Runnable 接口;
  3. 实现 Callable 接口;
  4. 使用 Executors 工具类创建线程池
27. 线程生命周期
  1. 新建(new):新创建了一个线程对象。
  2. 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状 态,等待被线程调度选中,获取 cpu 的使用权。
  3. 运行(running):可运行状态(runnable)的线程获得了 cpu 时间片(timeslice),执行程序 代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行, 首先必须处于就绪状态中
  4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止 执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到 运行状态。
  5. 死亡(dead):线程 run()、main()方法执行结束,或者因异常退出了 run()方法,则该线程 结束生命周期。死亡的线程不可再次复生。
28. Java 中用到的线程调度算法是什么?

有两种调度模型:分时调度模型和抢占式调度模型。

  1. 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
  2. Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果 可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态 的线程会一直运行,直至它不得不放弃 CPU
29. sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行

  1. sleep() 是 Thread 线程类的静态方法; wait() 是 Object 类的方法。
  2. sleep() 不释放锁;wait() 释放锁。
  3. Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  4. wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对 象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。 或者可以使用 wait(long timeout)超时后线程会自动苏醒
30.为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

Java 中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在 Object 类中。

wait(), notify()和 notifyAll()这些方法在同步代码块中调用

有的人会说,既然是线程放弃对象锁,那也可以把 wait()定义在 Thread 类里面啊,新定义 的线程继承于 Thread 类,也不需要重新定义 wait()方法的实现。然而,这样做有一个非常 大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁? 当然了,这种设计并不是不能实现,只是管理起来更加复杂

31. 如何停止一个正在运行的线程?

在 java 中有以下 3 种方法可以终止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
  2. 使用 stop 方法强行终止,但是不推荐这个方法,因为 stop 和 suspend 及 resume 一样都 是过期作废的方法。
  3. 使用 interrupt 方法中断线程。
32. Java 中 interrupted 和 isInterrupted 方法的区别

interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状 态为并做处理。支持线程中断的方法(也就是线程中断后会抛出 interruptedException 的方 法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异 常。

interrupted:是静态方法,查看当前中断信号是 true 还是 false 并且清除中断信号。如果一 个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。

isInterrupted:查看当前中断信号是 true 还是 false

33.Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取 线程堆栈?

Dump 文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到 dump 文件中。 在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java 应用的 dump 文件。

34. 一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。 Thread.UncaughtExceptionHandler 是用于处 理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的 时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程 的 UncaughtExceptionHandler 并将线程和异常作为参数传递 给 handler 的 uncaughtException()方法进行处理。

35.synchronized 原理

在执行同步代码块之前之后都有一个 monitor 字样,其中前面的是 monitorenter,后面的是 离开 monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程 就是 monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行 monitorexit 指令。

为什么会有两个 monitorexit 呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等 待的线程永远获取不到锁)。因此最后一个 monitorexit 是保证在异常情况下,锁也可以得 到释放,避免死锁。 仅有 ACC_SYNCHRONIZED 这么一个标志,该标记表明线程进入该 方法时,需要 monitorenter,退出该方法时需要 monitorexit。

36. synchronized 和 ReentrantLock 区别是什么?
  1. synchronized 关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是 类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以 有各种各样的类变量

  2. 相同点:两者都是可重入锁 两者都是可重入锁。
    “可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获 得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还 是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器 都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;

  3. ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。 二者 的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,

  4. synchronized *** 作的应该是对象头中 mark word Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础: 普通同步方法,锁是当前实例对象 静态同步方法,锁是当前类的 class 对象 同步方法块,锁是括号里面的对象

37. volatile

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。

volatile 提 供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量 被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它 会去内存中读取新值。

从实践角度而言,
volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参 见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。 volatile 常用于多线程环境下的单次 *** 作(单次读或者单次写)

38. final 什么是不可变对象,它对写并发应用有什么帮助?

不可变对象即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之 即为可变对象。

不可变对象的类即为不可变类。Java 平台类库中包含许多不可变类,如 String、基本类型 的包装类、BigInteger 和 BigDecimal 等。

只有满足如下状态,一个对象才是不可变的;它的状态不能在创建后再被修改; 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提 升了代码执行效率。

39. CAS 会产生什么问题?
  1. ABA 问题: 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A, 并且 two 进行了一些 *** 作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线 程 one 进行 CAS *** 作发现内存中仍然是 A,然后 one *** 作成功。尽管线程 one 的 CAS *** 作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个 类 AtomicStampedReference 来解决 ABA 问题

  2. 循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多 的 CPU 资源,效率低于 synchronized。

  3. 只能保证一个共享变量的原子 *** 作: 当对一个共享变量执行 *** 作时,我们可以使用循环 CAS 的方式来保证原子 *** 作,但是对多 个共享变量 *** 作时,循环 CAS 就无法保证 *** 作的原子性,这个时候就可以用锁。

40. AQS 原理

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工 作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套 线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将 暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟 的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是 将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/874175.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存