Java面试准备资料一

166 阅读10分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

1. Java同步机制

1.1 Java做到线程同步方法

  • 在需要同步的方法的方法签名中加上synchronized关键字(锁方法)

  • 使用synchronized关键字对需要进行同步的代码块进行同步 (锁代码块)

  • 使用java.util.concurrent.lock包中Lock对象(JDK1.8)---JDK 1.5出现

  • 使用volatile关键字(不能替代synchronized)

    a. volatile关键字为域变量的访问提供了一种免锁机制 b. 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新 c. 因此每次使用该域就要重新计算,而不是使用寄存器中的值 d. volatile不会提供任何原子操作(因此不能替换synchronized),它也不能用来修饰final类型的变量

1.2 synchronized使用时需要注意的一些地方:

被synchronized关键字修饰的代码块在被线程执行之前,首先要拿到被同步对象的锁,并且一个对象仅仅是只有一个锁,比如上面被synchronized代码,首先那个方法需要拿到当前对象的锁,如果当前的锁已经被其它线程拿走了,那么还没抢到锁的线程将从可运行状态转变为阻塞状态,只有当拿到锁的线程执行完同步块的代码后,就释放锁,让给别的线程的、这样就可以保证数据的完整性!

1.3 关于Lock对象和synchronized关键字的选择:

(1)最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 (2)如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。 (3)如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。

1.4 volatile与synchronized的区别:

参考链接:blog.csdn.net/suifeng3051…

全面了解推荐链接:blog.csdn.net/suifeng3051… JMMJava内存模型

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

volatile就是基于内存屏障实现

1.5 happens-before

参考链接:www.cnblogs.com/chenssy/p/6…

从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系,这两个操作即可以在同一个线程,也可以在不同的线程中。

  • happens-before原则定义
  1. 如果一个操作happens-before另一个操作,那么第一个操作的结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
  • happens-before原则规则
  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

happens-before与JMM的关系图(摘自《Java并发编程的艺术》)

happens-before

2. HashMap (Java 8系列)

2.1 Map家族

img

  • HashMap:根据键的hashCode值存储数据。非线程安全,可以用synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
  • Hashtable:遗留类,线程安全,但是不建议使用
  • LinkedHashMap:保存了记录的插入顺序
  • TreeMap:实现了SortedMap接口,能够把它保存的记录根据键排序,默认是升序排序

2.2 HashMap存储结构

数组+链表+红黑树

img

当put一个键值对时,首先获取key值的hashCode值,在通过hash算法(高位运算和取模运算)来定位该剪值对对对应的数组下标,然后再找链表或红黑树值(具体见下面)。

在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函数就是对下面几个字段进行初始化,源码如下:

int threshold;             // 所能容纳的key-value对极限 
final float loadFactor;    // 负载因子
int modCount;  
int size;  
  • Load factor为负载因子(默认值是0.75)

  • threshold是HashMap所能容纳的最大数据量的Node(键值对)个数,threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

  • modCount字段主要用来记录HashMap内部结构发生变化的次数

  • size:HashMap中实际存在的键值对数量

哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。

2.3 HashMap的put方法

img

(具体见链接)

3. ConcurrentHashMap 1.7和1.8的区别

出现场景

在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug,于是出现了ConcurrentHashMap

1. 数据结构

  • 1.7:Segment+HashEntry数组+链表

img

ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个SegmentHashEntry数组的大小cap,并初始化Segment数组的第一个元素;

其中ssize大小为2的幂次方默认为16cap大小也是2的幂次方最小值为2,最终结果根据初始化容量initialCapacity进行计算

因为Segment继承了ReentrantLock,所有segment是线程安全的

  • 1.8:Node数组+链表/红黑树+CAS+synchronized

img

移除Segment,使锁的粒度更小

2. put()

  • 1.7:先定位到Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。

  • 1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,再到first节点后进行判断:

    1. 为空则CAS插入
    2. 为-1则说明在扩容,则跟着一起扩容
    3. 两者都不是,则加锁put

3. get()

基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁

4. size()

  • 1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和
  • 1.8:用baseCount(用volatile修饰)来存储当前的节点个数,这就涉及到baseCount并发环境下修改的问题

5. resize()

  • 1.7:跟HashMap步骤一样,不过是搬到单线程中执行,避免了HashMap在1.7中扩容是死循环的问题,保证线程安全
  • 1.8:支持并发扩容,HashMap扩容在1.8中由头插改成尾插(为了避免死循环),ConcurrentHashMap也一样,迁移也是从尾部开始,扩容前在统的头部防止一个hash值为-1的节点,这样别的线程访问是就能判断是否该桶已经被其他线程处理过了

4. ArrayList和LinkedList区别

参考链接:blog.csdn.net/eson_15/art…

1. List概括

img

2. ArrayList和LinkedList区别

  1. ArrayList实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构(双向链表,它同样可以被当做栈,队列或双端队列来使用);
  2. 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;
  3. 对于添加和删除操作add和remove,一般大家都会说LinkedList要比ArrayList快,因为ArrayList要移动数据。但是实际情况并非这样,对于添加或删除,LinkedList和ArrayList并不能明确说明谁快谁慢
  • 从源码可以看出,ArrayList想要get(int index)元素时,直接返回index位置上的元素,而LinkedList需要通过for循环进行查找,虽然LinkedList已经在查找方法上做了优化,比如index < size / 2,则从左边开始查找,反之从右边开始查找,但是还是比ArrayList要慢。这点是毋庸置疑的。
  • ArrayList想要在指定位置插入或删除元素时,主要耗时的是System.arraycopy动作,会移动index后面所有的元素;LinkedList主耗时的是要先通过for循环找到index,然后直接插入或删除。(这就导致了两者并非一定谁快谁慢)

所以当插入的数据量很小时,两者区别不太大,当插入的数据量大时,大约在容量的1/10之前,LinkedList会优于ArrayList,在其后就劣与ArrayList,且越靠近后面越差。所以个人觉得,一般首选用ArrayList,由于LinkedList可以实现栈、队列以及双端队列等数据结构,所以当特定需要时候,使用LinkedList。当然咯,数据量小的时候,两者差不多,视具体情况去选择使用;当数据量大的时候,如果只需要在靠前的部分插入或删除数据,那也可以选用LinkedList,反之选择ArrayList反而效率更高。

5. Java线程安全的集合

1. Vector

Vector和ArrayList类似,是长度可变的数组,但是Vector是线程安全的。它给几乎所有的pubic方法都加了synchronized关键字。由于加锁导致性能降低,所以Vector已经被弃用。

2. HashTable

HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了

3. ConcurrentHashMap

  • 在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
  • JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率

4. CopyOnWriteArrayList和CopyOnWriteArraySet

它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

5. 其他

ConcurrentSkipListMapConcurrentSkipListSetConcurrentLinkedQueueConcurrentLinkedDeque等,没有ConcurrentArrayList