主要是总结Java面试当中经常问到的Java知识点,总结到一篇博客当中,并给出参考解答或者参考链接。
java知识点
- Hashmap 源码级掌握,扩容,红黑树,最小树化容量,hash冲突解决,有些面试官会提出发自灵魂的审问,比如为什么是红黑树,别的树不可以吗;为什么8的时候树化,4不可以吗
Java集合数据结构 建议参考 java面试常问数据结构
- JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,即 数组+链表+红黑树 的结构。
为什么要这样设计呢?好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢- 扩容:参考 HashMap扩容原理
- 为什么8的时候树化?
理想情况下使用随机的哈希码,容器中节点分布在hash桶中的频率遵循泊松分布,照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。- hashmap为什么用红黑二叉树而不用B+树
我们在学数据库索引的时候知道底层用B+树做索引存储,了解到B+树查询效率比红黑二叉树更好,但是为什么hashmap底层要用红黑二叉树存储,而不用B+树存储呢,其实原因很简单
B+树在数据库中被应用的原因就是B+树比B树更加“矮胖”,每个结点能存储的关键字更多。所以B+树更能应对大量数据的情况。
如果HashMap用B+树代替红黑树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面。这个时候遍历效率就退化成了链表。
- concureentHashMap,段锁,如何分段,和hashmap在hash上的区别,性能,等等
- 背景:由于hashMap在处理高并发的问题时,会出现因put操作,扩容混乱时出现的链路环,有可能造成下一次操作的死锁。为了解决这个问题提出了:concurrentHashMap。
- ConcurrentHashMap是在JDK 1.5上添加的并发收集类,用于替代基于哈希的同步映射实现,例如Hashtable和HashMap。他们的同步对手提供更好的性能和可扩展性,风险很小。
- ConcurrentHashMap的实现:
(1)JDK1.7
- JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
- 其使用的是分段锁(使用的是可重入锁),首先将数据分为一段一段的存储,然后给每一段数据Segment配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
- 用 volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性!
(2)JDK1.8
- JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。
- 将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
- 如何分段
如何分段是指对分Segment的操作,在ConcurrentHashMap中,分的segment的个数是2的次幂。使得整个哈希表相当于一个二级哈希表。在并发过程中,segment之间相互独立,读写操作互不影响。- concurrentHashMap和hashmap在hash上的区别
(1)hashmap的hash: hashcode = key的二进制; index = hashcode & (n-1)
(2)concurrentHashMap的hash: 先定位到segment的index位置,再定位到segment中的位置
参考 concurrentHashMap
- HashTable ,同步锁,这块可能会问你synchronized关键字 1.6之后提升了什么,怎么提升的这些
Java集合数据结构 建议参考 java面试常问数据结构
- HashTable是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。
- synchronized关键字以及1.6的锁优化 参考下面的 synchronized 的实现原理以及锁优化?部分。
- ArrayList 优势,扩容,什么时候用 , LinkedList 优势,什么时候用,和arraylist的区别 等等
Java集合数据结构 建议参考 java面试常问数据结构
- ArrayList 底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。
ArrayList底层采用的是数组,因此它的优缺点也跟数组相关,其随机访问性强,查找速度快,时间复杂度为O(1);但是在数组起始位置处,插入数据和删除数据效率低。插入数据时,待插入位置的的元素和它后面的所有元素都需要向后搬移;删除数据时,待删除位置后面的所有元素都需要向前搬移
- LinkedList底层是双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)。
同样,LinkedList底层采用链表,因此其优缺点也跟链表的优缺点有关,任意位置插入元素和删除元素的速度快,时间复杂度为O(1),内存利用率高,不会浪费内存,链表的空间大小不固定,可以动态拓展;但是随机访问效率低,时间复杂度为0(N);
- 基本类型和包装类型的区别,涉及自动装箱和拆箱,怎么做的,原理
- Java有8种基本类型:大致分为3类:字符,布尔,数值类型
- 包装类型:Java是一个面向对象的语言,基本类型并不具有对象的性质,为了与其他对象“接轨”就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
- 基本类型和包装类型的区别
a 基本类型存放在堆栈中, 包装类型的是实例放在堆中
b 基本类型的创建不需要new关键字,包装类型需要new
c 基本类型的初始值为0/false…,包装类型的初始值是null
d 和ArrayList LinkList 这类集合类使用的时候,只能使用包装类型- 自动装箱和拆箱怎么做
装箱就是自动将基本数据类型转换为包装器类型;
拆箱就是自动将包装器类型转换为基本数据类型- 自动装箱和拆箱原理
自动装箱:调用包装类型的函数 valueOf(),进行转换
自动拆箱:通过调用intValue() doubleValuse(),进行转换
- String ,StringBuffer,StringBuilder哪个是安全的
推荐参考 String、StringBuffer、StringBuilder
- StringBuffer的方法是同步的,synchronized修饰符进行修饰,证明Stringbuffer是线程安全的。
- 字符串编码的区别
常用的字符编码:
Unicode: 适用于所有语言
ASCII:适用于英文
GB2312:适用于中文
- 什么是泛型,怎么用泛型
- 早期Java是使用 Object 来实现通用、不同类型的处理,有这么两个缺点:
(1)每次使用时都需要强制转换成想要的类型
(2)在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全- 泛型 ,顾名思义就是 广泛的数据类型,也就是说什么数据类型都可以。
(1)如果数据类型不确定,可以使用泛型方法的方式,达到简化代码、提高代码重用性的目的。
(2)泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,使代码可以应用于多种数据
(3)参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。- 泛型的通配符
通配符:传入的类型有一个指定的范围,从而可以进行一些特定的操作。泛型中有三种通配符形式:
1.<?>无限制通配符
2.<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
3.<? super E> super 关键字声明了类型的下界,表示参数化类型可能是指定类型,或者是此类型的父类。- 泛型的实现
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。
当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这就是 类型擦除。- 擦除的实现原理
Java 编译器在编译期间擦除了泛型的信息,那运行中怎么保证添加、取出的类型就是擦除前声明的呢?
Java 编辑器会将泛型代码中的类型完全擦除,使其变成原始类型。当然,这时的代码类型和我们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,可以保证类型安全。总之泛型就是一个语法糖,它运行时没有存储任何类型信息。
- static能不能修饰threadLocal,为什么,这道题我当时一听到其实挺懵逼的
- ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。- static可以用来修饰threadlocal。经过static修饰的变量,在类加载时候就完成了内存的分配和初始化,在内存中只有一个副本,被所有对象共享。
- Comparable和Comparator接口是干什么的,其区别
- Comparable & Comparator 都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序
- Comparable接口。Comparable接口包含一个内部函数compareTo(),因此但凡是继承了接口的类,都需要实现这个方法。
Comparable对这个接口的每一个类对象都强加了一个整体排序。这个排序被称为类的自然排序,类的这个compareTo()就被称为其自然比较方法。- Comparator是一个比较器接口,被比较的类不需要直接实现它。
- Comparable和Comparator接口的区别
1 comparalbe在java.lang ,comparator在java.util下
2 comparable实现通过在集合内部实现该结构的compareTo()方法实现的排序
comparator是在集合外部实现的排序,是一个专用的比较器。当这个对象不支持自比较或者自比较函数不满足要求时,可以写一个比较器来完成两个对象之间大小的比较
- 多态的原理是什么,感觉这个很容易被问到
- Java多态的实现机制是父类或接口定义的引用变量可以指向子类或实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实现对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
- 多态的机制。
本质上多态分两种:编译时多态(又称静态多态);运行时多态(又称动态多态)
重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行时运行的时候调用的是确定的方法。
通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。- 运行时多态(以下简称多态)的机制。
多态通常有两种实现方法:子类继承父类(extends);类实现接口(implements)
- 接口和抽象类,
相同点:
1、都不能被实例化。
2、接口的实现类和抽象类的子类只有全部实现了接口或者抽象类中的方法后才可以被实例化。
不同点:
1、接口只能定义抽象方法不能实现方法,抽象类既可以定义抽象方法,也可以实现方法。
2、单继承,多实现。接口可以实现多个,只能继承一个抽象类。
3、接口强调的是功能,抽象类强调的是所属关系。
4、接口中的所有成员变量 为public static final, 静态不可修改,当然必须初始化。接口中的所有方法都是public abstract 公开抽象的。而且不能有构造方法。抽象类就比较自由了,和普通的类差不多,可以有抽象方法也可以没有,可以有正常的方法,也可以没有。
- 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么
- fail-fast机制在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception。
fail-fast会在以下两种情况下抛出ConcurrentModificationException
(1)单线程环境。集合被创建后,在遍历它的过程中修改了结构。
(2)多线程环境。当一个线程在遍历这个集合,而另一个线程对这个集合的结构进行了修改。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。- fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出Concurrent Modification Exception
fail-safe机制有两个问题
(1)需要复制集合,产生大量的无效对象,开销大
(2)无法保证读取的数据是目前原始数据结构中的数据。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
- synchronized 的实现原理以及锁优化?
实现原理 :synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,基于进入和退出Monitor对象实现,无论是显示同步(同步代码块)还是隐式同步(同步方法)都是如此,同时它还可以保证共享变量的内存可见性。
synchronized的作用:保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized实现同步的基础:java中的每一个对象都可以作为锁。
synchronized所存储位置:synchronized使用的锁对象是存储在Java对象头里的标记字段里。
synchronized的3种使用方式:修饰实例方法:作用于当前实例加锁;修饰静态方法:作用于当前类对象加锁;修饰代码块:指定加锁对象,对给定对象加锁;
锁优化:JDK1.6对synchronized锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
参考JDK1.6版本关于synchronized的优化
- volatile 的实现原理?
- volatile是Java提供的一种轻量级的同步机制。相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差,而且其使用也更容易出错。
- volatile关键字的作用是保证变量在多线程之间的可见性和禁止指令重排序。
- volatile变量的特性
1.保证可见性,不保证原子性
(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
(2)这个写会操作会导致其他线程中的volatile变量缓存无效。
(3)volatile在原子性这块能保证的只是对单个volatile变量的读/写具有原子性,但是对于类似volatile++这样的复合操作就无能为力了。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。- 实现原理: volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
参考 Java面试官最爱问的volatile关键字
- Java 的信号灯?
- Semaphore是Java1.5之后提供的一种同步工具,Semaphore可以维护访问自身线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而release() 释放一个许可。
- 简单理解就是Semaphore(信号量)是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。
- Semaphore采用了CAS来实现,尽量避免锁的使用,提高了性能
- synchronized 在静态方法和普通方法的区别?
- synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。
- 这个问题涉及到synchronized的三种使用方式 ,可参考 synchronized 的实现原理以及锁优化 问题。
- 怎么实现所有线程在等待某个事件的发生才会去执行?
参考 Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
java里面实现这个有两个办法,countdownlatch和cyclicbarrier。
- cyclicbarrier可以重复使用,它允许一组线程相互等待,直到达到某个公共屏障点。cyclicbarrier不会阻塞主线程,只会阻塞子线程。
cyclicbarrier内部使用Lock,每一个子线程执行await,计数减一,当最后一个子线程的计数为0时,会执行cyclicbarrier构造函数中的Runable参数的run方法- countdownlatch不可以重复使用,会阻塞主线程。主线程调用await方法,主线程阻塞。子线程调用countdown方法,触发计数。countdownlatch内部是实现了AQS,初始化的时候,new CountDownLatch(n);将AQS的state设置为n。
- CAS?CAS 有什么缺陷,如何解决?
- JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。J.U.C提供的atomic包中的类,使用的是乐观锁,用到的机制就是CAS(Compare and Swap),CAS是一种实现并发算法时常用到的技术。
- CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
参考 Java CAS同步机制 原理详解- 缺陷:
- 循环时间长开销很大。getAndAddInt方法执行时,只有完成覆盖才会调出循环,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
- ABA问题。如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题,Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
- synchronized 和 lock 有什么区别?
- 来源:lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
- 异常是否释放锁:synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,发生异常时候会自动释放占有的锁,因此不会出现死锁;而Lock则必须要用户去手动释放锁,必须手动unlock来释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
- 是否响应中断。lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
- 是否知道获取锁。Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
- synchronized只支持非公平锁,而Lock支持公平和非公平锁。
- synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。
参考 synchronized 和Lock区别
- Hashtable 是怎么加锁的 ?
- Hashtable ht = Hashtable.Synchronized(new Hashtable());
- HashTable是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。
- List,Map,Set接口在取元素时,各有什么特点
Java集合数据结构底层可参考 Java集合面试常问数据结构
- List与Set都是单列元素的集合,它们有一个功共同的父接口Collection。
- Set里面不允许有重复的元素。
存元素:add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true;当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。
取元素:由于Set是无序的,没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
- List表示有先后顺序的集合。
存元素:多次调用add(Object)方法时,每次加入的对象按先来后到的顺序排序,也可以插队,即调用add(int index,Object)方法,就可以指定当前对象在集合中的存放位置。
取元素:
方法1:Iterator接口取得所有,逐一遍历各个元素
方法2:调用get(index i)来明确说明取第几个。
- Map保持键值对映射。
映射关系可以是一对一或者多对一。存元素:存放用put方法:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。
取元素:
用get(Object key)方法根据key获得相应的value。
也可以获得所有的key的集合,还可以获得所有的value的集合,
还可以获得key和value组合成的Map.Entry对象的集合。
- 如何线程安全的实现一个计数器
- 利用Java的关键字volatile。
- 利用volatile去修饰变量可以保证其内存可见性,直接从内存中读,但是不能保证正确性,因为多线程环境下存在同时读取,同时做写操作。因此我们还得限制,一次只有一个线程操作变量,可以利用synchronized关键字去保证。
public class AtomicCounter { private volatile int count = 0; public synchronized void increment() { count++; } public int getCount() { return count; } }
- Java的JUC当中已经提供一个安全计数器的实现AtomicInteger。AtomicInteger本质上就是利用自旋锁+CAS原子操作实现的。参考 AtomicInteger原理分析
- 生产者消费者模式
- 生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
- 实现:在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。
(1)Object的wait() / notify()方法
(2)Lock和Condition的await() / signal()方法
(3)BlockingQueue阻塞队列方法
(4)PipedInputStream / PipedOutputStream
代码参考
生产者消费者模式的实现(java实现)
生产者消费者模式的简单实现
- 单例模式,饿汉式,懒汉式,线程安全的做法,两次判断instance是否为空,每次判断的作用是什么。
- 单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
- 实现思路与步骤
1).将该类的构造方法定义为私有方法,就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
2).在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。- 饿汉式和懒汉式的区别就在于是否在类装载时就完成唯一对象的实例化。
具体实现参考 java设计模式(一)创建型模式之 单例模式(饿汉式,懒汉式,线程安全,双重检查)
- 两次判断instance是否为空。
第一层是为了判断如果instance存在直接返回,第二层在synchronized内判断是为了解决当一个线程进入,另外排队等待锁的线程在第一个线程释放锁之后重复创建instance对象。- volatile修饰instance的作用:禁止指令重排序、内存可见性
- 线程池,这个还是很重要的,在生产中用的挺多,四个线程池类型,其参数,参数的理解很重要,corepoolSize怎么设置,maxpoolsize怎么设置,keep-alive各种的
- 背景:如果每个请求都创建一个线程去处理,处理完又马上释放线程,当线程并发数量很大时服务器的资源很快就会被耗尽,且频繁地创建销毁线程带来了额外的开销。
- 线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。使用线程池可以限制系统中执行线程的数量,同时可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
参考 关于线程池的五种实现方式,七大参数,四种拒绝策略
- cyclicbarrier 和countdownlatch的区别,个人理解 赛马和点火箭
- CountdownLatch阻塞主线程,等所有子线程完结了再继续下去。cyslicbarrier阻塞一组线程,直至某个状态之后再全部同时执行,并且所有线程都被释放后,还能通过reset来重用。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点)。
- CyclicBarrier只能唤醒一个任务;CountDownLatch可以唤醒多个任务;
- CyccliBarrier可以重用,CountDownLatch不可重用,当计数值为0时,CountDownLatch就不可再用了。
参考 CyclicBarrier与CountdownLatch区别和相似之处的个人理解
- 线程回调,这块 被问过让我设计一个RPC,怎么实现,其实用到了回调这块的东西
参考 Java回调机制
- 回调是一种双向的调用方式, 其实而言, 回调也有同步和异步之分;
- 回调的思想是:
类A的a()方法调用类B的b()方法
类B的b()方法执行完毕主动调用类A的callback()方法- 通俗而言: 就是A类中调用B类中的某个方法C, 然后B类中反过来调用A类中的方法D, D这个方法就叫回调方法。
- 经典的回调方式:
class A实现接口CallBack callback——背景1
class A中包含一个class B的引用b ——背景2
class B有一个参数为callback的方法f(CallBack callback) ——背景3
A的对象a调用B的方法 f(CallBack callback) ——A类调用B类的某个方法 C
然后b就可以在f(CallBack callback)方法中调用A的方法 ——B类调用A类的某个方法D
- sleep、wait、yeild三者的区别
- sleep和wait的区别 [参考 sleep和wait的区别]
(1)属于不同的两个类,sleep()方法是线程类(Thread)的静态方法,wait()方法是Object类里的方法。
(2)sleep()方法不会释放锁,wait()方法释放对象锁。
(3)sleep()方法可以在任何地方使用,wait()方法则只能在同步方法或同步块中使用。
(4)sleep()必须捕获异常,wait()方法、notify()方法和notiftAll()方法不需要捕获异常。
(5)sleep()使线程进入阻塞状态(线程睡眠),wait()方法使线程进入等待队列(线程挂起),也就是阻塞类别不同。
(6) 它们都可以被interrupted方法中断。
- sleep 和yeild方法有什么区别 参考 线程的sleep()方法和yield()方法有什么区别?
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞( blocked)状态, 而执行yield()方法后转入就绪( ready)状态;
yield()方法调用后线程处于RUNNABLE状态,而sleep()方法调用后线程处于TIME_WAITING状态,所以yield()方法调用后线程只是暂时的将调度权让给别人,但立刻可以回到竞争线程锁的状态;而sleep()方法调用后线程处于阻塞状态。
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU 调度相关)具有更好的可移植性。
- 乐观锁和悲观锁的使用场景
java高并发锁的知识 推荐参考 Java高并发之锁总结、常见的面试问题
- 乐观锁:乐观锁在处理是比较乐观的,每次去读数据都认为其它事务没有在写数据,就不上锁,只在提交数据的时候判断其它事务是否改过这个数据,如果改过就rollback。
乐观锁相当于一种检测冲突的手段。Java中的非阻塞同步都是采用这种乐观的并发策略,乐观锁在Java中是通过使用无锁编程来实现,最常使用的CAS操作。- 悲观锁:对共享数据进行访问时,悲观锁总是认为一定会有其他线程修改数据。如果不加锁,肯定会出问题。
悲观锁相当于一种避免冲突的手段。Java中同步互斥都是采用这种悲观的并发策略,synchronized关键字和Lock接口的实现类都是悲观锁。- 适用场景:
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
- 悲观锁的常见实现方式:lock synchronized ReentrantLock
- Lock接口实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。其常见的实现类有
ReentrantLock、ReentrantReadWriteLock等;- 这些同步方式加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
- 区别:
(1)实现: lock由jdk支持,依靠AQS实现锁的获取和释放,synchronized由JVM支持,依靠进入和退出monitor对象,实现同步方法或同步代码块;导致Lock需要用lock()和unlock()去显式的获取、释放锁,若忘记手工是否锁容易造成死锁,一般会将unlock()放到finally代码块里;而synchronized的加锁解锁是由JVM隐式完成。
(2)性能: 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,synchronized的优化其实就是借鉴了ReenTrantLock中的CAS技术,都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
(3)特性:lock 支持非阻塞获取(tryLock)、中断获取(lockinterruptibly)、超时获取(带参数的tryLock),而synchronized不支持。
(4)线程间的协作: synchronized依靠Object的监视器方法wait()、notify()/notifyAll(),实现等待/通知模式;lock接口的实现类,依靠绑定的condition对象的await()、signal()/signalAll(),实现等待/通知模式。
(5)公平锁和非公平锁:Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
- 乐观锁:CAS MVCC
- CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。实现非阻塞同步的方案称为“无锁编程算法”( Non-blocking algorithm)。
乐观锁基本都是基于 CAS(Compare and swap)算法来实现的。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。整个CAS操作是一个原子操作,是不可分割的。
- 多版本并发控制(Multiversion concurrency control, MCC 或 MVCC),是数据库管理系统常用的一种并发控制,也用于程序设计语言实现事务内存。 参考 乐观锁、悲观锁和MVCC
(1) MVCC是为了解决读写锁造成的多个、长时间的读操作饿死写操作问题,也就是解决读写冲突的问题。总的来说,MVCC的出现就是数据库不满用悲观锁去解决读-写冲突问题,因性能不高而提出的解决方案。
(2) MVCC的实现,是通过保存数据在某个时间点的快照来实现的。每个事务读到的数据项都是一个历史快照,被称为快照读,不同于当前读的是快照读读到的数据可能不是最新的,但是快照隔离能使得在整个事务看到的数据都是它启动时的数据状态。而写操作不覆盖已有数据项,而是创建一个新的版本,直至所在事务提交时才变为可见。
- 读写锁的实现方式,16位int的前八位和后八位分别作为读锁和写锁的标志位
- 背景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
- 读写锁ReentrantReadWriteLock:针对这种场景,JAVA提供了ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
- ReadWriteLock的实现原理
在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
(1)公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
(2)可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁
(3)可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。- 推荐参考 Java 并发编程(八):读写锁 ReentrantReadWriteLock
- 死锁的条件,怎么解除死锁,怎么观测死锁。
- 产生死锁的主要原因
(1)因为系统资源不足
(2)进程运行推进的顺序不合适
(3)资源分配不当等
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁,其次进程运行推进顺序与速度的不同也可能产生死锁
- 产生死锁的四个必要条件:
(1)互斥条件:一个资源每次只能被一个进程使用
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能前行剥夺
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁- 死锁的检测
可以使用 jstack或者pstack 和 gdb 工具对死锁程序进行分析。- 死锁的预防与避免:
(1)在系统设计,进度调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源,此外,也要防止进程在处于等待状态的情况下占用资源,因此,对资源的分配要给予合理的规划
(2)一个非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁和释放锁,当所有的线程都按顺序获取和释放锁时就不会发生死锁- 死锁的解除:
- 第一种方法:进程终止,简单地终止一个或多个进程以打破循环等待。
- 撤销所有死锁线程
- 一次终止一个线程直到取消死循环为止
- 第二种方法:抢占资源,从一个或多个死锁进程中抢占一个或多个资源。
- 选择一个牺牲品
- 回滚
- 饥饿(在代价因素中加上回滚次数,回滚的越多则越不可能被作为牺牲品)
- java 反射的原理,怎么确定类,怎么调方法
- 什么是反射?
(1)在运行状态中,对于任意一个类,都能够知道这个类的属性和方法。
(2)对于任意一个对象,都能够调用它的任何方法和属性。
这种动态获取信息以及动态调用对象的方法的功能称为JAVA的反射。 这是一个动态相关机制——Reflection(反射),用在Java身上指的是可以于运行时加载、探知、使用编译期间完全未知的classes。- 反射能做什么?
(1)在运行时判断任意一个对象所属的类;
(2)在运行时构造任意一个类的对象;
(3)在运行时判断任意一个类所具有的成员变量和方法;
(4)在运行时调用任意一个对象的方法;
(5)生成动态代理。- 反射的原理
Java有个Object 类,是所有Java 类的继承根源,其内声明了数个应该在所有Java 类中被改写的方法:hashCode()、equals()、clone()、toString()、getClass()等。
其中getClass()返回一个Class 对象而这个Class 类十分特殊。它和一般类一样继承自Object,当一个class被加载,或当加载器(class loader)的defineClass()被JVM调用,JVM 便自动产生一个Class 对象。
当我们获取到这个Class对象之后,我们就可以调用这个Class的方法直接对class字节文件进行操作,获取到类的属性、构造器、普通方法的详细信息和对其进行操作- 具体可参考 java反射机制的原理与使用
- 同步,异步,阻塞,非阻塞 在深信服的面试中遇到过,最好再找一些应用场景加以理解
- 同步、异步:针对客户端。
同步:客户端请求后等待返回。应用程序执行一个系统调用,在系统调用没有完成,应用程序会一直阻塞。
异步:客户端请求发出后,不用等待返回结果,执行下一步动作,当系统调用返回时,通过状态、通知来通知调用者,或通过回调函数处理这个调用。- 阻塞、非阻塞:针对服务器。
阻塞调用:是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
非阻塞调用:指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。- 推荐参考 Java之阻塞和非阻塞以及同步和异步的区别