Java面试题

4 篇文章 0 订阅

1Java基础

1.1什么是Java虚拟机

Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。

1.2JDK 和 JRE 有什么区别?

JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。包含了JRE、编译器和其它工具(如:javaDOc、java调试器)
JRE:Java Runtime Environment 的简称,java 运行环境,包含java虚拟机和java程序所需的核心类库。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。

1.3Java内存模型

https://zhuanlan.zhihu.com/p/38348646
在这里插入图片描述
主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
JVM的内存结构大概分为:

  • 堆(Heap):线程共享,jvm只有一个。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。静态成员变量跟随着类定义一起也存放在堆上
  • 方法区(Method Area):线程共享,jvm只有一个。存储类信息、常量、静态变量、即时编译器编译后的代码。
  • 方法栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。方法调用时进栈,执行完退栈。堆中对象的引用。存储基本数据类型。
  • 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
  • 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
    补充:
  • 一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。
  • 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  • 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  • 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

1.4Java 类加载的过程

类加载过程分为三个主要步骤:加载,连接,初始化,具体行为在 Java 虚拟机规范里有非常详细的定义。

首先是加载过程(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,比如 jar 文件,class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是连接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转入 JVM 运行的过程中。这里可进一步细分成三个步骤:
1,验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
2,准备(Pereparation),创建类或者接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显示初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
3,解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在 Java 虚拟机规范中,详细介绍了类,接口,方法和字段等各方面的解析。
最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
再来‘’‘’谈谈双亲委派模型,简单说就是当加载器(Class-Loader)试图加载某个类型的时候,除非父类加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。

1.5== 和 equals 的区别

== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;
equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。

1.6两个对象的 hashCode()相同,equals()不一定 true

1.7final 在 java 中有什么作用

  1. final 修饰的类叫最终类,该类不能被继承。
  2. final 修饰的方法不能被重写。
  3. final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改

1.8为什么很多类要定义成抽象类

写成抽象类,这样别人看到你的代码,或你看到别人的代码,你就会注意抽象方法,而知道这个方法是在子类中实现的,所以,有个提示作用。

1.9抽象类必须要有抽象方法吗?

不需要,抽象类不一定非要有抽象方法。
示例代码:
abstract class Cat {
public static void sayHi() {
System.out.println(“hi~”);
}
}
上面代码,抽象类并没有抽象方法但完全可以正常运行。

1.10普通类和抽象类有哪些区别?

普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。

抽象类能使用 final 修饰吗?

不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类,如下图所示,编辑器也会提示错误信息:
在这里插入图片描述

接口和抽象类有什么区别?

  • 默认方法实现:抽象类可以有默认的方法实现;接口不能有默认的方法实现。

  • 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。

  • 构造函数:抽象类可以有构造函数;接口不能有。

  • main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。

  • 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。

  • 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是 private,protected 或者是 public修饰符。

  • Java 接口中声明的变量默认都是 final 的。抽象类可以包含非 final 的变量。

  • 抽象类可以在不提供接口方法实现的情况下实现接口。

  • 接口是绝对抽象的,不可以被实例化(java 8已支持在接口中实现默认的方法)。抽象类也不可以被实例化,但是,如果它包含 main 方法的话是可以被调用的。

Java支持多继承么?如果不支持,如何实现?

在java中是单继承的,也就是说一个类只能继承一个父类。
java中实现多继承有两种方式,一是接口,而是内部类.

java 中操作字符串的类String, StringBuffer StringBuilder的区别

操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。

String str="i"与 String str=new String(“i”)一样吗?

不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。

如何将字符串反转?

使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。

String 类的常用方法都有那些?

indexOf():返回指定字符的索引。charAt():返回指定索引处的字符。replace():字符串替换。trim():去除字符串两端空白。split():分割字符串,返回一个分割后的字符串数组。getBytes():返回字符串的 byte 类型数组。length():返回字符串长度。toLowerCase():将字符串转成小写字母。toUpperCase():将字符串转成大写字符。substring():截取字符串。equals():字符串比较。

java 中的 Math.round(11.5) 等于多少? Math.round(-11.5)等于多少?

Math.round 四舍五入大于 0.5 向上取整的

Math.round(11.5)12 Math.round(-11.5)-11 round 方法返回与参数 最接近的长整数,参数加 1/2 后求其 floor.

Java支持的基础的数据类型

基础类型有 8 种:byte、boolean、char、short、int、float、long、double;

String 不属于基础类型,而 String 属于对象。

什么是自动拆装箱?

https://www.cnblogs.com/dolphin0520/p/3780005.html

自动拆装箱是java从jdk1.5引用,目的是将原始类型自动的装换为相对应的对象,也可以逆向进行,即拆箱。这也体现java中一切皆对象的宗旨。

所谓自动装箱就是将原始类型自动的转换为对应的对象,而拆箱就是将对象类型转换为基本类型。java中的自动拆装箱通常发生在变量赋值的过程中,如:

Integer object = 3; //自动装箱
int o = object; //拆箱

在java中,应该注意自动拆装箱,因为有时可能因为java自动装箱机制,而导致创建了许多对象,对于内存小的平台会造成压力。

什么是值传递和引用传递?java中是值传递还是引用传递,还是都有?

https://blog.csdn.net/weixin_41990707/article/details/83306088

值传递 就是在方法调用的时候,实参是将自己的一份拷贝赋给形参,在方法内,对该参数值的修改不影响原来实参,常见的例子就是刚开始学习c语言的时候那个交换方法的例子了。
引用传递 是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。
在java中只有一种传递方式,那就是值传递.可能比较让人迷惑的就是java中的对象传递时,对形参的改变依然会影响到该对象的内容。

Java迭代器(iterator详解以及和for循环的区别)

采用ArrayList对随机访问比较快,而for循环中的get()方法,采用的即是随机访问的方法,因此在ArrayList里,for循环较快

采用LinkedList则是顺序访问比较快,iterator中的next()方法,采用的即是顺序访问的方法,因此在LinkedList里,使用iterator较快

一个Java字符串中到底有多少个字符?

依照Java的文档, Java中的字符内部是以UTF-16编码方式表示的,最小值是 \u0000 (0),最大值是\uffff(65535),也就是一个字符以2个字节来表示,难道Java最多只能表示 65535个字符

java 中 IO 流分为几种?

按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。

BIO、NIO、AIO 有什么区别?

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
  • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

泛型在编译期和运行期的作用

https://www.cnblogs.com/archermeng/p/7537024.html

泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。

泛型值存在于java的编译期,编译后生成字节码文件泛型是被擦除的。

深拷贝和浅拷贝区别是什么?

浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign())

深拷贝会开辟新的内存,是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)

浅拷贝会出现的事故:浅拷贝造成的内存被重复释放;

如何实现对象克隆?

注意:对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。

有两种方式:

  • 实现Cloneable接口并重写Object类中的clone()方法;
  • 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,

Java集合

  • 集合分为两大块:java.util包下的非线程安全集合和java.util.concurrent下的线程安全集合。

List
在这里插入图片描述
在这里插入图片描述

集合对比HashSetTreeSetLinkedHashSetHashMapTreeMapLinkedHashMapHashTableLinkedListVectorArrayList
结构集合集合键值对集合HashMap+双向链表键值对双向链表数组数组
是否有序否,字典顺序
线程安全
可以为空一个键为null,多个值为null否,是
默认容量1616111010
加载因子扩容0.75,扩容1倍0.75,扩容1倍0.75,扩容1.5倍1倍1.5倍

List、Set、Map的用法和区别

List和Set是Collection的子类 ,Collection接口中定义了以下方法:

boolean add(E e) //向集合中添加一个元素,若添加元素后集合发生了变化就返回true,若没有发生变化,就返回false。(optional operation).boolean addAll(Collection<? extends E> c) //添加给定集合c中的所有元素到该集合中(optional operation).void clear() //(optional operation).boolean contains(Object o) //判断该集合中是否包含指定对象boolean containsAll(Collection<?> c)boolean equals(Object o)int hashCode()boolean isEmpty()Iterator<E> iterator()boolean remove(Object o) //移除给定对象的一个实例(有的具体集合类型允许重复元素) (optional operation).boolean removeAll(Collection<?> c) //(optional operation).int size()Object[] toArray()<T> T[] toArray(T[] a)
List:
  1. 可以允许重复的对象

  2. 可以插入多个null元素

  3. 是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序

  4. 常用的实现类有ArrayList,LinkedList和Vector。ArrayList提供了使用索引的随意访问,底层结构是数组,优查询劣增删,而LinkedList经常用于添加或删除元素的场合,底层结构是链表

Set:

https://www.jianshu.com/p/d6cff3517688

在这里插入图片描述

  1. 不允许重复的对象,set里面存放的是对象的引用,在判断重复元素的时候,Set集合会调用hashCode()和equal()方法来实现。

  2. 无序容器,你无法保证每个元素的存储顺序,TreeSet通过Conparator或者Comparable维护了一个排序顺序

  3. 只允许一个null元素

  4. Set常用的实现类是HashSet,LinkedHashSet以及TreeSet。最流行的是基于HashMap实现的HashSet,TreeSet还实现了SortedSet接口,因此TreeSet是一个根据其conpare()和compareTo()的进行排序的有序容器。

    HashSet是哈希表结构,主要利用HashMap的key来存储元素,计算插入元素的hashCode来获取元素在集合中的位置;

    TreeSet是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序;

Map:
  1. Map不是collection的子接口或者实现类,Map是一个接口

  2. Map的每个Entry都持有俩个对象,一个键一个值,可能会持有相同的值对象但键对象必须是唯一的。

  3. TreeMap也通过Comparator或者Comparable维护了一个排序顺序

  4. Map里你可以拥有任意个null,但只能有一个null键

  5. 常用的实现类HashMap,LinkedHashMap,Hashtable,TreeMap

    总结:

    如何选择使用list,set或者map?

1、如果经常使用索引来对元素进行访问,那么使用List,如果你已经知道索引了的话,那么List的实现类比如ArrayList可以提供更快速的访问,如果经常添加删除元素,那么选择LinkedList

2、如果想让容器中的元素能够按照他们插入的次序进行有序存储,那么还是List,因为List是一个有序容器

3、如果想要保证插入数据的唯一性,可以选择一个Set的实现类

4、如果你以键和值的形式进行数据存储那么Map是你正确的选择

HashTable,HashMap区别

  1. HashTable的方法是同步的,HashMap未经同步,所以在多线程场合要手动同步HashMap这个区别就像Vector和ArrayList一样。
  2. HashTable不允许null值,key和value都不可以,HashMap允许null值,key和value都可以。HashMap允许 key值只能由一个null值,因为hashmap如果key值相同,新的key, value将替代旧的。
  3. HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。
  4. HashTable使用Enumeration,HashMap使用Iterator。
  5. HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。Hashtable 初始容量是11 ,扩容 方式为2N+1;
  6. 哈希值的使用不同,HashTable直接使用对象的hashCode。
  7. hashtable:使用一把锁处理并发问题,当有多个线程访问时,需要多个线程竞争一把锁,导致阻塞。

TreeMap

TreeMap继承于AbstractMap,实现了Map, Cloneable, NavigableMap, Serializable接口。
(1)TreeMap 继承于AbstractMap,而AbstractMap实现了Map接口,并实现了Map接口中定义的方法,减少了其子类继承的复杂度;
(2)TreeMap 实现了Map接口,成为Map框架中的一员,可以包含着key-value形式的元素;
(3)TreeMap 实现了NavigableMap接口,意味着拥有了更强的元素搜索能力;
(4)TreeMap 实现了Cloneable接口,实现了clone()方法,可以被克隆;
(5)TreeMap 实现了Java.io.Serializable接口,支持序列化操作;
TreeMap具有如下特点:
不允许出现重复的key;
可以插入null键,null值;
可以对元素进行排序;
无序集合(插入和遍历顺序不一致);

  1. 注意到SortedMap是接口,它的实现类是TreeMap
  2. 使用TreeMap时,放入的Key必须实现Comparable接口。StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。
  3. 如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法
  4. 要严格按照compare()规范实现比较逻辑,否则,TreeMap将不能正常工作。

HashMap的底层原理

https://blog.csdn.net/mbshqqb/article/details/79799009

https://blog.csdn.net/vking_wang/article/details/14166593

https://blog.csdn.net/ym123456677/article/details/78860719

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。

扩容机制,在什么条件下扩容?

hahsmap默认桶的大小是16,扩容方式为2N(2的幂次方).

如果bucket(桶)满了(超过load factor*current capacity),就要resize。

16 * 0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍

load factor为0.75,为了最大程度避免哈希冲突

current capacity为当前数组大小。

HashMap:初始化容量 = 元素个数 / 加载因子(加载因子默认值:0.75),相除的结果向上取整且为2的N次方,默认值16;

比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

为什么扩容是2的次幂?为什么要先高16位异或低16位再取模运算?

降低hash冲突的几率

为什么加载因子是0.75

加载因子是表示Hash表中元素的填满的程度;
加载因子越大,填满的元素越多,空间利用率越高,冲突的机会加大了,同时也增加了查询时间成本;
反之,加载因子越小,填满的元素越少,虽然可以减少查询时间成,冲突的机会减小,但空间浪费多了,同时提高了rehash操作的次数

提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小

并发编程环境下问题

如果多线程一边size(),一边put()会发生什么?

  • (1)多线程扩容,引起的死循环问题
  • (2)多线程put的时候可能导致元素丢失
  • (3)put非null元素后get出来的却是null

在jdk1.8中,死循环问题已经解决。其他两个问题还是存在。比如ConcurrentHashmap,Hashtable(效率低)等线程安全等集合类。

HashMap1.8中改动

https://blog.csdn.net/lnn112233/article/details/82968748

  1. 由数组+链表的结构改为数组+链表+红黑树。
  2. 优化了高位运算的hash算法:h^(h>>>16)
  3. 扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。

在java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点很多时,查询时间是O(n)。
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储);若桶中元素小于等于6时,树结构还原成链表形式。

最后一条是重点,因为最后一条的变动,hashmap在1.8中,通过栈封闭的链表替换,解决了多线程会引起链表死循环问题。

在JDK1.8之前,多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环。

在JDK1.8之前之后,也会死循环:多线程下操作同一对象时,对象内部属性的不一致性导致的。

在JDK1.8之前的版本中,HashMap的底层实现是数组+链表。当调用HashMap的put方法添加元素时,如果新元素的hash值或key在原Map中不存在,会检查容量size有没有超过设定的threshold,如果超过则需要进行扩容,扩容的容量是原数组的两倍,扩容就是新建Entry数组,并将原Map中元素重新计算hash值,然后存到新数组中。当调用get方法获取该位置的元素时就会发生死循环。

为什么hashmap的在链表元素数量超过8时改为红黑树?

当链表长度大于或等于阈值(默认为 8)的时候,如果同时还满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树。

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要,这样做效率高。

红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,意思是10为底8的对数。链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

HashMap计算添加元素的位置时,使用的位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!

什么是桶排序?

桶排序(Bucket Sort)是迄今为止最快的一种排序法,其时间复杂度仅为Ο(n),也就是线性复杂度!

hash冲突你还知道哪些解决办法?
  1. 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
  2. 再哈希法
  3. 链地址法
  4. 建立一个公共溢出区

Java中hashmap的解决办法就是采用的链地址法:

假设有元素x要放进数组,那么遍历数组,假设都是1号位置有元素,那么此元素位置放在哪里呢?是放在1-1位置吗?答案是否定的。x元素放入1号位置。有同学要问1号位置不是有元素吗?1号位置里的元素不会被覆盖吗?答案是:如果放入直接放入x元素,那么1号位置的元素必将被覆盖。导致1号元素位置元素丢失。那么,我们在把x元素放入1号位置之前把1号位置原有元素保存到1-1位置,然后再把x元素放入1号位置。这就完美解决了。那么,1-2,1-3位置的元素也都是这样产生的。这就是链地址法的简单讲述。

知道1.8 hashmap中put元素的过程是什么样么?

对key的hashCode()做hash运算,计算index;

如果没碰撞直接放到bucket(桶)里;

如果碰撞了,以链表的形式存在buckets后;

如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(JDK1.8中的改动);

如果节点已经存在就替换old value(保证key的唯一性)

如果bucket满了(超过load factor*current capacity),就要resize。

打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。

知道1.8 hashmap中get元素的过程是什么样么?

先定位到数组元素,再遍历该元素处的链表。

对key的hashCode()做hash运算,计算index;

如果在bucket里的第一个节点里直接命中,则直接返回;

如果有冲突,则通过key.equals(k)去查找对应的Entry;

若为树,则在树中通过key.equals(k)查找,O(logn);

若为链表,则在链表中通过key.equals(k)查找,O(n)。

1.8中 通过hash定位桶,然后根据该桶的存储结构决定是遍历红黑树还是遍历链表

1 table[i]的首个元素是否和key一样,如果相同则返回该value

2 如果不同,先判断首元素是否是红黑树节点,如果是则去红黑树中查找;反之去链表中查

如果两个键的hashcode相同,你如何获取值对象

找到bucket位置之后,会调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象。

如何线程安全的使用HashMap

这个无非就是以下三种方式:

  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map
Hashmap扩容时判断是否要重新hash的规则

hash寻址算法是 index =(n - 1) & hash

Java7扩容时,遍历每个节点,并重新hash获得当前数组的位置并添加到链表中;

Java8进一步做了优化,将元素的hash和旧数组的大小(大小为2次幂)做与运算,为0则表示数组位置不变,不为0则表示需要移位,新位置为原先位置+旧数组的小大(新数组大小为旧数组翻倍),并将当前链表拆分为两个链表,一个链表放到原先位置,一个链路放到新位置,效率比Java7高。

ConcurrentHashMap

https://www.cnblogs.com/heqiyoujing/p/10928423.html

ConcurrentHashMap是Java1.5中引用的一个线程安全的支持高并发的HashMap集合类。

1.7中ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。

首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

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

当问到我们有关于ConcurrentHashMap的工作原理以及实现时,可以从以下几个方面说:

  • ConcurrentHashMap的优点,即HashMap和HashTable的缺点。
  • ConcurrentHashMap两个静态内部类:HsahEntry和segment
  • ConcurrentHashMap含有16个segment
  • ConcurrentHashMap的put方法:根据hash值找到对应的segment,segment是分段锁。
  • ConcurrentHashMap的get方法:count>0,hash找到HashEntry,hash相等并且key相同,若取value为null,加锁重新获取。
  • ConcurrentHashMap的remove方法:加锁,每删除一个元素就将那之前的元素克隆一边。因为设置为第一次next之后不能再改变。
  • ConcurrentHashMap的size()方法:2次不锁住segment方式统计各个segment的大小,若count发生变化,采用加锁方式统计。modCount变量,在put,remove和clean方法里操作元素,modcount加1.
ConcurrentHashMap在jdk1.7和jdk1.8主要区别:

https://blog.csdn.net/qq_41884976/article/details/89532816

(1):实现线程安全的方式
hashMap是线程不安全的,
hashTable是线程安全的,实现线程安全的机制是使用Synchronized关键字修饰方法。
ConcurrentHashMap
<JDK1.7>ReentrantLock+Segment+HashEntry
ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
<jdk1.8>synchronized+CAS+HashEntry+红黑树
使用的是优化的synchronized 关键字 和 cas操作了维护并发。
(2):底层数据结构:
hashMap同hashTable;都是使用数组 + 链表结构
ConcurrentHashMap
<jdk1.7> :使用 Segment数组 + HashEntry数组 + 链表
<jdk1.8> :使用 Node数组+链表+ 红黑树
(3) : 效率
hashMap只能单线程操作,效率低下
hashTable使用的是synchronized方法锁,若一个线程抢夺了锁,其他线程只能等到持锁线程操作完成之后才能抢锁操作
《1.7》ConcurrentHashMap 使用的分段锁,如果一个线程占用一段,别的线程可以操作别的部分,
《1.8》简化结构,put和get不用二次哈希,一把锁只锁住一个链表或者一棵树,并发效率更加提升。

cas是什么?

一句话:对内存执行读-改-写操作,比较并更新;CAS是Java乐观锁的一种实现机制

Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值;一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

CAS底层原理?

1.自旋锁

2.UnSafe类

CAS 的缺点:

1.循环时间长开销很大。

2.只能保证一个共享变量的原子操作。

3.会有ABA问题。

4.由于CAS失败后会继续重试,导致一致占用着CPU。

ABA问题解决

原理:添加一个标记来记录更改

1:AtomicMarkableReference 利用一个boolean类型的标记来记录,只能记录它改变过,不能记录改变的次数
2:AtomicStampedReference 利用一个int类型的标记来记录,它能够记录改变的次数。

ConcurrentHashMap get() 和 size()是否要加锁?如何加锁?

1.7中 ConcurrentHashMap采用了分段锁的形式,每一段为一个Segment类,它内部类似HashMap的结构,内部有一个Entry数组,数组的每个元素是一个链表。同时Segment类继承自ReentrantLock。`

1.8 中放弃了Segment这种分段锁的形式,而是采用了CAS+Synchronized的方式来保证并发操作的,get()操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。

JDK1.8中的ConcurrentHashMap在执行put()方法
  • 第一步通过key进行hash。
  • 第二步判断是否需要初始化数据结构。
  • 第三步根据key定位到当前Node,如果当前位置为空,则可以写入数据,利用CAS机制尝试写入数据,如果写入失败,说明存在竞争,将会通过自旋来保证成功。
  • 第四步如果当前的hashcode值等于MOVED则需要进行扩容(扩容时也使用了CAS来保证了线程安全)。
  • 第五步如果上面四步都不满足,那么则通过synchronized阻塞锁将数据写入。
  • 第六步如果数据量大于TREEIFY_THRESHOLD时需要转换成红黑树(默认为8)。

1.7版本put:

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

1.8版本put:

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

JDK1.8的ConcurrentHashMap的get()方法就还是比较简单
  1. 根据key的hashcode寻址到具体的桶上。
  2. 如果是红黑树则按照红黑树的方式去查找数据。
  3. 如果是链表就按照遍历链表的方式去查找数据。

HashSet

HashSet实现Set接口,底层由HashMap(后面讲解)来实现,为哈希表结构,新增元素相当于HashMap的key,value默认为一个固定的Object。在我看来,HashSet相当于一个阉割版的HashMap;

hashset基本上就是使用hashmap的方法再次实现了一遍而已,只不过value全都是同一个object,让你以为相同元素没有插入,事实上只是value替换成和原来相同的值而已。

而当两个hashcode相同但key不相等的entry插入时,仍然会连成一个链表,长度超过8时依然会和hashmap一样扩展成红黑树。

当有元素插入的时候,会计算元素的hashCode值,将元素插入到哈希表对应的位置中来;

它继承于AbstractSet,实现了Set, Cloneable, Serializable接口。

(1)HashSet继承AbstractSet类,获得了Set接口大部分的实现,减少了实现此接口所需的工作,实际上是又继承了AbstractCollection类;

(2)HashSet实现了Set接口,获取Set接口的方法,可以自定义具体实现,也可以继承AbstractSet类中的实现;

(3)HashSet实现Cloneable,得到了clone()方法,可以实现克隆功能;

(4)HashSet实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。

具有如下特点:

  • 不允许出现重复因素;
  • 允许插入Null值;
  • 元素无序(添加顺序和遍历顺序不一致);
  • 线程不安全,若2个线程同时操作HashSet,必须通过代码实现同步;

LinkedHashMap

https://blog.csdn.net/justloveyou_/article/details/71713781
在这里插入图片描述

基本原理

本质上,LinkedHashMap = HashMap + 双向链表,也就是说,HashMap和双向链表合二为一即是LinkedHashMap。

LinkedHashMap中的Entry增加了两个指针 beforeafter,它们分别用于维护双向链接列表。特别需要注意的是,next用于维护HashMap各个桶中Entry的连接顺序,before、after用于维护Entry插入的先后顺序的

LinkedHashMap重写了HashMap 的迭代器,它使用其维护的双向链表进行迭代输出。

哪两种有序

LinkedHashMap的默认实现是按插入顺序排序

  1. 按插入顺序排序(当 accessOrder = false)
  2. 按最近获取的数据倒序(当 accessOrder = true)

什么是LRU

https://blog.csdn.net/qq_26440803/article/details/83795122

LRU 最近最少使用替换,利用默认的排序方式可以实现。

LRU的设计原理就是,当数据在最近一段时间经常被访问,那么它在以后也会经常被访问。这就意味着,如果经常访问的数据,我们需要然其能够快速命中,而不常访问的数据,我们在容量超出限制内,要将其淘汰。

常见的页面置换算法还有如下几种:

  • LRU 最近最久未使用
  • FIFO 先进先出置换算法 类似队列
  • OPT 最佳置换算法 (理想中存在的)
  • NRU Clock置换算法
  • LFU 最少使用置换算法
  • PBA 页面缓冲算法
LRU缓存实现
  1. 使用linkedHashmap实现:定义LRULinkedHashMap继承LinkedHashMap,并重写removeEldestEntry()方法,这个方法返回boolean值,返回true代表需要删除最老的节点,要注意的就是创建实例对象的时候需要传入size和accessOrder参数(accessOrder= ture)。
  2. 对于这种类似序列的结构我们一般可以选择链表或者是数组来构建。

差异对比:

数组 查询比较快,但是对于增删来说是一个不是一个好的选择
链表 查询比较慢,但是对于增删来说十分方便O(1)时间复杂度内搞定
有没有办法既能够让其搜索快,又能够快速进行增删操作。
我们可以选择链表+hash表,hash表的搜索可以达到0(1)时间复杂度,这样就完美的解决我们搜索时间慢的问题了

查询:

  • 根据键值查询hashmap,若命中,则返回节点,否则返回null。
  • 从双向链表中删除命中的节点,将其重新插入到表头。
  • 所有操作的复杂度均为O(1)。

插入:

  • 将新的节点关联到Hashmap
  • 如果Cache满了,删除双向链表的尾节点,同时删除Hashmap对应的记录
  • 将新的节点插入到双向链表中头部

更新:

  • 和查询相似

删除:

  • 从双向链表和Hashmap中同时删除对应的记录。

ArrayList的扩容机制

在这里插入图片描述
是使用数组方式存储数据,默认长度为 10,但其实并不是在初始化的时候就创建了 DEFAULT_CAPACITY=10 的数组,而是在往里边 add 第一个数据的时候会扩容到 10,既然知道了默认的长度为 10 ,那说明后续一旦写入到第9个元素的时候就会扩容为 10*1.5=15。这一步为数组复制,也就是要重新开辟一块新的内存空间存放这 15 个数组。一旦我们频繁且数量巨大的进行写入时就会导致许多的数组复制,这个效率是极低的

扩容大小主要是第四行

		int newCapacity = oldCapacity + (oldCapacity >> 1);

这句代码的意思是:新的容量是旧容量加上旧容量值右移一位得到的

按二进制形式把所有的数字向右移动对应位移位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1。

10>>1 相当于 10/2 ; 10+5=15

但如果我们提前预知了可能会写入多少条数据时就可以提前避免这个问题。比如我们往里边写入 1000W 条数据,在初始化的时候就给定数组长度与用默认 10 的长度之间性能是差距巨大的。

查找快,增删慢

常用方法:

boolean add(E e) //添加一个元素到数组末尾void add(int index, E element) //添加一个元素到指定位置void clear()boolean contains(Object o)void ensureCapacity(int minCapacity) //确保ArrayList至少能容纳参数指定数目的对象,若有需要会增加ArrayList实例的容量。E get(int index) //返回指定位置的元素int indexOf(Object o)boolean isEmpty()Iterator<E> iterator()ListIterator<E> listIterator()E remove(int index)boolean remove(Object o)E set(int index, E element)int size()
①为什么ArrayList 是线性不安全的?
②替代措施及解决方案?

第一种方案:

使用Vertor集合

第二种方案:

使用Collections.synchronizedList。它会自动将我们的list方法进行改变,最后返回给我们一个加锁了List

第三种方案:

使用JUC中的CopyOnWriteArrayList类进行替换。具体详情可参考JUC框架

Vector

ArrayList和Vector都是使用数组方式存储数据,次数组元素大于实际存储的数据以便添加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通过性能上校ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项即可,所以插入速度较快。

Vector和ArrayList类似,Vector中的方法是线程安全的,使用起来不灵活,效率较差,ArrayList是线程不安全的,

使用时可以用Collections工具类中的synchronizedXxx()方法转换成线程安全的,比较灵活,或者使用加锁的方式也可实现线程安全

LinkedList

https://blog.csdn.net/u011240877/article/details/52876543
在这里插入图片描述
继承自 AbstractSequentialList 接口,同时了还实现了 Deque, Queue 接口。

虽说都是 List 的容器,但本质实现却完全不同。LinkedList 是由链表组成,每个节点又有头尾两个节点分别引用了前后两个节点;因此它也是一个双向链表。所以理论上来说它的写入非常高效,将不会有 ArrayList 中效率极低的数组复制,每次只需要移动指针即可。

增删快,查找慢

方法:

void addFirst(E element); void addLast(E element); E getFirst(); E getLast(); E removeFirst(); E removeLast();

add方法:

boolean add(E e) //把元素e添加到链表末尾 void add(int index, E element) //在指定索引处添加元素

总结:

  1. 再使用 ArrayList 时如果能提前预测到数据量大小,比较大时一定要指定其长度。
  2. 尽可能避免使用 add(index,e) api,会导致复制数组,降低效率。
  3. 再额外提一点,我们常用的另一个 Map 容器 HashMap 也是推荐要初始化长度从而避免扩容。
  4. List中的subList()方法作用是返回一个List集合的其中一部分视图,示例:List(对象的集合).subList(int fromIndex, int toIndex);调用list.subList()方法返回的新的List集合,其内部引用的还是原来的list,所以两个list的内部元素都是同一个元素。即:对返回的list集合操作就是对原来的集合操作,此时操作需要谨慎。一般来说,使用list.subList()方法后,不会再对原来的list进行操作了。

Java多线程

什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位

什么是线程安全?

线程安全是指保证多线程环境下共享的、可修改的状态的正确性。

保证线程安全的两个办法:

  1. 封装:将对象的内部状态隐藏、保护起来。
  2. 不可变:final变量产生了某种程度地不可变(immutable)效果,可以用于保护只读数据。

线程安全需要保证几个基本特性:

  1. 原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现。
  2. 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
  3. 有序性:保证线程内串行语义,避免指令重排。

线程安全的类有哪些,平时有使用么,用来解决什么问题

  1. 通过synchronized 关键字给方法加上内置锁来实现线程安全(

    StringBuffer:线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。

    Vector:Vector 类可以实现可增长的对象数组。

    Hashtable:此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。)

  2. java.util.concurrent下的线程安全集合(ConcurrentHashMap)

  3. ThreadPoolExecutor也是使用了ReentrantLock显式加锁同步

并行和并发有什么区别?

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。

并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

线程和进程的区别?

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

守护线程是什么?

守护线程拥有自动结束本身生命周期的特性,而非守护线程不具有这个特色。

synchronized

https://blog.csdn.net/weixin_40255793/article/details/80786249

首先那些说看过synchronized源码的基本都是大聪明,synchronized根本点不进去,想弄懂它的实现原理,我们只能通过看编译好的字节码文件。

关键字synchronized可以用来修饰方法和代码块,修饰方法也就相当于将方法放在代码块中。synchronized修饰实例方法时(对象锁),相当于synchronized (this) {}。修饰静态方法时(类锁,也叫静态锁),相当于synchronized (ClassName.class) {}。

原理:对象有锁计数器,一个任务可以多次获得对象的锁,每获取一次,计数加1,每释放一次,计数减1,当计数为0时,其他任务才能获取该对象的锁(可重入性)。

锁有个专门的名字:对象监视器

基于对象的监视器(ObjectMonitor),我们在字节码文件里面可以看到,在同步方法执行前后,有两个指令,方法前monitorenter,方法后monitorexit;

synchronized属于独占式悲观锁通过JVM隐式实现,只允许同一时刻只有一个线程操作资源

Java中每个对象都隐式包含一个monitor(监视器)对象
加锁的过程其实就是竞争monitor的过程,当线程进入字节码monitiorenter指令后
线程将持有monitor对象,执行monitorexit时释放monitor对象
当其他线程没有拿到monitor对象时,则需要阻塞等待获取该对象。

Synchronized有哪些缺点?

只有一个condition与锁相关联,这个condition是什么?就是synchronized对针对的对象锁。
synchronized无法中断一个正在等待获得锁的线程,也即多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。这种情况对于大量的竞争线程会造成性能的下降等后果。

lock

Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),它包含以下方法

//尝试获取锁,获取成功则返回,否则阻塞当前线程void lock(); //尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 void lockInterruptibly() throws InterruptedException; //尝试获取锁,获取锁成功则返回true,否则返回false boolean tryLock(); //尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 boolean tryLock(long time, TimeUnit unit)    throws InterruptedException; //释放锁void unlock(); //返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量Condition newCondition();

Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。

原理:

synchronized与lock对比

类别synchronizedlock
存在层次Java的关键字,在jvm层面上是一个接口
锁的释放1、以获取锁的线程执行完同步代码,释放锁;
2、线程执行发生异常,jvm会让线程释放锁。
必须在finally中释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。Lock有多种获取锁的方式,如lock、tryLock
唤醒只能随机唤醒一个或者全部唤醒可以指定唤醒哪些线程
锁状态无法判断,只能阻塞可以判断;tryLock();tryLock(long time, TimeUnit unit);可避免死锁。
锁类型可重入,非公平,不可中断可重入,可公平(两者皆可);可中断(lockInterruptibly()😉
锁的对象
功能功能单一API丰富;tryLock();tryLock(long time, TimeUnit unit);可避免死锁。
AQS(AbstractQueuedSynchronizer)

Lock之所以能实现线程安全的锁,主要的核心是AQS,AQS提供了一个FIFO(先入先出)队列,可以看做是一个用来实现锁以及其他需要同步功能的框架。AQS的使用依靠继承来完成,子类通过继承自AQS并实现所需的方法来管理同步状态。例如常见的ReentrantLock,CountDownLatch等。

从使用上来说,AQS的功能可以分为两种:独占和共享。

独占锁模式下,每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现的互斥锁。

共享锁模式下,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。

synchronized与ReentrantLock的区别?

https://zhuanlan.zhihu.com/p/126085068

  1. 底层实现:synchronized 是JVM层面的锁,是Java关键字,通过对象锁计数器来完成,对象只有在同步块或同步方法中才能调用wait/notify方法,synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;

    ReentrantLock(可重入锁) 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁,通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

  2. 锁的对象:synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

  3. **是否可手动释放:**synchronized 不需要用户去手动释放锁;ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

  4. 是否可中断:synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

  5. 是否公平锁:ynchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

  6. 锁是否可绑定条件Condition:synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

公平锁和非公平锁的区别

公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。
锁的实现方式是基于如下几点:
表结点Node和状态state的volatile关键字。
sum.misc.Unsafe.compareAndSet的原子操作(见附录)。

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

ReentrantLock 重入锁

https://blog.csdn.net/ITkampong/article/details/106056843

ReentrantLock的实现是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的。 其可重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。

ReentrantLock是Lock的默认实现方式之一,是基于AQS(Abstrack Queued Synchronizer,队列同步器) 实现的默认是通过非公平锁实现的,它的内部有一个state的状态字段用于表示锁是否被占用,如果0则表示锁未被占用,此时线程就可以吧state改为1,并成功获得锁而其他未获得锁的线程只能去排队等待获取资源

这俩个都实现了锁的功能,具备互斥性和不可见性。

JDK1.6之后synchronized性能略低于ReentrantLock

区别:

synchronized是JVM隐式实现的,而ReentrantLock是Java语言提供的API
ReentrantLock可设置为公平锁,而synchronized却不行
ReentrantLock只能修饰代码块,而Synchronized可以用于修饰方法、修饰代码块等
ReentrantLock需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用而synchronized无需手动释放锁
ReentrantLock可以知道是否成功获得了锁,而synchronized缺不行

ReentrantLock锁都不会使得线程中断,除非开发者自己设置了中断位。
ReentrantLock获取锁里面有看似自旋的代码,但是它不是自旋锁。
ReentrantLock公平与非公平锁都是属于排它锁

ReentrantReadWriteLock 重入读写锁

  • 如果一个线程已经占用了读锁,其他线程可以马上获得读锁,但需要等待才能获取写锁。
  • 如果一个线程已经占用了写锁,其他线程要获取读锁或写锁都需要等待。

当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

线程同步的几种方式

  1. synchronized修饰
  2. volatile实现同步(只能保证可见性,不能保证原子性)
  3. 使用局部变量ThreadLocal
  4. 使用原子类(AtomicInteger、AtomicBoolean……)
  5. 使用Lock
  6. 使用容器类(BlockingQueue、ConcurrentHashMap

自旋锁、阻塞锁、重入锁、偏向锁、轻量锁和重量锁

https://blog.csdn.net/qq_25827845/article/details/74139773

1、自旋锁:

采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区

优缺点分析:

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

2、阻塞锁:

阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。

优缺点分析:

阻塞锁的优势在于,阻塞的线程不会占用cpu时间,不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于自旋锁。

3、重入锁:

Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另

优缺点分析:

可重入锁的最大优点就是可以避免死锁。缺点是必须手动开启和释放锁。

java并发和线程同步,同步机制,锁具体解释一下

高并发相关的问题,concurrent相关包

常用的线程池,threadlocal有什么用,多线程并发解决办法

线程池是如何创建的?需要几个参数?分别是什么含义?

乐观锁和悲观锁的区别

乐观锁,每次操作时不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
悲观锁是会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁可以使用volatile+CAS原语实现,带参数版本来避免ABA问题,在读取和替换的时候进行判定版本是否一致
悲观锁可以使用synchronize的以及Lock

乐观锁和悲观锁的实现。(数据库、Java)

CAS syncronized实现有什么区别。

violate关键字。

Java 虚拟机

JVM GC的优缺点。

假设一个场景,要求 stop the world时间非常短,你会怎么设计垃圾回收机制?

有没有用过JVM相关工具?

算法

海量数据top K算法,讲一下思路。

设计模式

策略模式和XX模式的区别。(这里因为没有看过其他设计模式,当时问这个题目的时候脑袋都是懵的。)

MySQL

数据库索引结构

事务隔离级别、锁、索引的数据结构、聚簇索引和非聚簇索引、最左匹配原则、查询优化(explain等命令)

Mysql(innondb 下同) 有哪几种事务隔离级别?

不同事务隔离级别分别会加哪些锁?

mysql的行锁、表锁、间隙锁、意向锁分别是做什么的?

说说什么是最左匹配?

如何优化慢查询?

mysql索引为什么用的是b+ tree而不是b tree、红黑树

分库分表如何选择分表键

分库分表的情况下,查询时一般是如何做排序的?

Spring 相关

bean的生命周期、循环依赖问题、spring cloud(如项目中有用过)、AOP的实现、spring事务传播

常见问题

java动态代理和cglib动态代理的区别(经常结合spring一起问所以就放这里了)

spring中bean的生命周期是怎样的?

属性注入和构造器注入哪种会有循环依赖的问题?

redis

、redis持久化、redis过期淘汰机制、redis分布式集群的常见形式、缓存击穿、缓存雪崩、缓存一致性问题

redis工作模型

分布式锁

http://blog.itpub.net/29715045/viewspace-2650976/

Redisson原理分析
在这里插入图片描述

Redis锁实现防重复提交和并发问题

一.redis命令讲解:
setnx()命令:
setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。

该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。

get()命令:
get(key) 获取key的值,如果存在,则返回;如果不存在,则返回nil;
getset()命令:
这个命令主要有两个参数 getset(key, newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:

  1. getset(key, “value1”) 返回nil 此时key的值会被设置为value1
  2. getset(key, “value2”) 返回value1 此时key的值会被设置为value2
  3. 依次类推!
    二.具体的使用步骤如下:
  4. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
  5. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
  6. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
  7. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
  8. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理

redis性能为什么高?

单线程的redis如何利用多核cpu机器?

redis有3种部署方式:

单机模式
master-slave + sentinel选举模式
redis cluster模式

redis的缓存淘汰策略?

redis如何持久化数据?

redis有哪几种数据结构?

redis集群有哪几种形式?

有海量key和value都比较小的数据,在redis中如何存储才更省内存?

如何保证redis和DB中的数据一致性?

如何解决缓存穿透和缓存雪崩?

如何用redis实现分布式锁?

  • edis你们都用来做什么?
  • redis的持久化机制?
  • 怎么样保证redis的高可用?

http 1.0 和 http 2.0的区别?

说下https 的请求过程。

说说ssl四次握手的过程。

在java 7 和 java 8中GC的区别。

多个task是在一个进程中运行吗?

数据库建索引有哪些考虑?

之前保存文件分片序号的时候会出现脏读的情况,如何防止脏读?事务隔离是怎么做的?

304状态码有什么含义?服务端是如何实现的?

消息队列

  • 你们项目中用过MQ,平时都用MQ来做什么?
  • 你处理过MQ得幂等问题,当时是怎么做的?
  • MQ的可靠性怎么保证?

分布式事务解决方案tx-lcn和seata简单对比分析

tx-lcn是本地事务协调,本身并不会产生事务,LCN 核心采用3PC+TCC补偿机制
seata 是两阶段提交事务,第一阶段解析业务sql并生成对应快照,第二阶段是提交/回滚,并删除快照

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值