一、Java基础及集合
1、Java语言的了解,然后讲一下Java语言的特点
面向对象(封装,继承,多态)
跨平台性(Java虚拟机实现平台无关性)
支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的)
支持多线程(多线程机制使应用程序在同一时间并行执行多项任)
健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等)
安全性
2、和C++相比较?
都是面向对象的语言,都支持封装、继承和多态
Java不提供指针来直接访问内存,程序内存更加安全
Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
Java有自动内存管理机制,不需要程序员手动释放无用内存
3、Java面向对象的三大特征?
抽象:将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
继承:是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
子类拥有父类非 private 的属性和方法。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以重写父类的方法。
多态:就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
方法重载实现的是编译时的多态性,根据参数列表的不同来区分不同的函数,通过编译之后会变成两个不同的函数。
而方法重写实现的是运行时的多态性,通过动态绑定来实现的。
运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
方法重写(子类继承父类并重写父类中已有的或抽象的方法)
父类引用子类对象(这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)
多态的实现:
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要父类引用指向子类对象。
重载(Overload)和重写(Override)的区别
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不能重写。
3、介绍一下hashcode和equals?
hashCode() 的作用是获取哈希码,也称为散列码;这个哈希码的作用是确定该对象在哈希表中的索引位置。Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了 equals 的次数,相应就大大提高了执行速度。
hashCode()与equals()的相关规定
如果两个对象相等,则hashcode一定也是相同的
两个对象相等,对两个对象分别调用equals方法都返回true
两个对象有相同的hashcode值,它们也不一定是相等的
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
4、hashmap和hashtable
区别 | HashMap | HashTable |
---|---|---|
接口 | 都实现了 Map、Cloneable、Serializable 接口 | |
父类 | AbstractMap 类 | Dictionary 类 |
底层数据结构 | JDK1.7数组 + 链表;JDK1.8数组+链表+红黑树 | 数组 + 链表 |
键值对 | 只允许一个键为null,多个null值为空 | 不允许空的 key 和 value 值 |
线程安全性 | 线程不安全 | 线程安全 |
扩容机制 | 初始长度为16,每次扩容变为原来的两倍;若给定初始长度,会将其扩充为2的倍数 | 初始长度是11,每次扩容变为之前的 2n+1,若给定长度,则使用给定长度 |
效率 | 高 | 慢,方法大多是Synchronized的,而ConcurrentHashMap使用分段锁,效率比HashTable高 |
计算hash方式 | 先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值。 | Hashtable通过计算key的hashCode()来得到hash值就为最终hash值。 |
HashMap1.7底层实现:
变量size,它记录HashMap的底层数组中已用槽的数量;
变量threshold,它是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75。
HashMap内存储数据的Entry数组默认是16,有自己的扩容机制。
HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容
扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。
加载因子:如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容),对空间造成严重浪费。如果在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值。
HashMap 1.8 的底层结构
1.如果冲突数量小于8,则是以链表方式解决冲突。
2.在进行添加元素时,当一个桶中存储元素的数量 > 8 时,会自动转换为红黑树
3.在进行删除元素时,如果一个桶中存储元素数量 < 6 后,会自动转换为链表
JDK1.7 中,HashMap 采用位桶 + 链表的实现,即使用链表来处理冲突,同一 hash 值的链表都存储在一个数组中。但是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。
所以,与 JDK 1.7 相比,JDK 1.8 在底层结构方面做了一些改变,当每个桶中元素大于 8 的时候,会转变为红黑树,目的就是优化查询效率。
HashMap 的 put 过程
首先会使用 hash 方法计算对象的哈希码,根据哈希码来确定在 bucket 中存放的位置,如果 bucket 中没有 Node 节点则直接进行 put,如果对应 bucket 已经有 Node 节点,会对链表长度进行分析,判断长度是否大于 8,如果链表长度小于 8 ,在 JDK1.7 前会使用头插法,在 JDK1.8 之后更改为尾插法。如果链表长度大于 8 会进行树化操作,把链表转换为红黑树,在红黑树上进行存储。
put时,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()
与key.hashCode()>>>16
进行异或操作, hash 函数作用:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash
,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
HashMap的扩容操作?
①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
②.每次扩展的时候,都是扩展2倍;
③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
HashMap 为啥线程不安全?
HashMap 不是一个线程安全的容器,不安全性体现在多线程并发对 HashMap 进行 put 操作上。如果有两个线程 A 和 B ,首先 A 希望插入一个键值对到 HashMap 中,在决定好桶的位置进行 put 时,此时 A 的时间片正好用完了,轮到 B 运行,B 运行后执行和 A 一样的操作,只不过 B 成功把键值对插入进去了。如果 A 和 B 插入的位置(桶)是一样的,那么线程 A 继续执行后就会覆盖 B 的记录,造成了数据不一致问题。
HashMap 是如何处理哈希碰撞的
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所如果只是单纯的用hashCode取余来获取对应的bucket,相当于参与运算的只有hashCode的低位,高位没有起到任何作用,这将会大大增加哈希碰撞的概率,所以还需要对hashCode作一定的优化。
hash()函数
让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
在JDK 1.8中的hash()函数如下:与自己右移16位进行异或运算(高位保持不变,低位和高位异或操作)
HashMap有效解决哈希冲突
- 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
- 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
- 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
选择?
线程安全:ConcurrentHashMap>HashTable
HashMap线程不安全
HashMap 和 HashSet 的区别?
HashSet 继承于 AbstractSet 接口,实现了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不允许集合中出现重复的值。HashSet 底层其实就是 HashMap,所有对 HashSet 的操作其实就是对 HashMap 的操作。所以 HashSet 也不保证集合的顺序。
对比 | HashMap | ConcurrentHashMap |
---|---|---|
线程安全 | 线程不安全 | 对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,较HashTable锁的粒度更精细 |
null值 | 只允许一个键为空,多个值为空 | 键值都不允许为空 |
ConcurrentHashMap 和 Hashtable 的区别?—主要体现在实现线程安全的方式不同
ConcurrentHashMap的锁粒度更精细
对比 | ConcurrentHashMap | Hashtable |
---|---|---|
底层数据结构 | JDK1.7,采用 分段的数组+链表 实现,JDK1.8 采用数组+链表/红黑二叉树 | 数组+链表 |
实现线程安全的方式 | JDK1.7,分段锁对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment);JDK1.8 摒弃Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。 | 同一把锁:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态 |
ConcurrentHashMap 底层具体实现原理
JDK1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现;
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
JDK1.8
在JDK1.8中,采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
6、String、StringBuilder、StringBuffer的区别,线程安全问题
区别 | String | StringBuilder | StringBuffer |
---|---|---|---|
可变性 | private final char value[],不可变 | 继承AbstractStringBuilder类,char[] value,可变 | 可变 |
线程安全性 | 线程安全 | StringBuilder没有对方法加同步锁,非线程安全 | AbstractStringBuilder定义了一些字符串的基本操作,StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,线程安全 |
性能 | 对String 类型进行改变时,都会生成一个新的String对象,然后将指针指向新的String 对象,慢 | 对对象本身进行操作,而不是生成新的对象并改变对象引用,快 | 快 |
选择 | 操作少量的数据 | 单线程操作字符串缓冲区 下操作大量数据 | 多线程操作字符串缓冲区 下操作大量数据 |
7、Java集合 ArrayList LInkedList HashMap 底层数据结构 扩容
单列集合Collection | List | Set |
---|---|---|
有序性 | 有序 | 无序 |
重复性 | 可重复 | 不可重复 |
null值 | 可多个null值 | 只能有一个null值 |
实现类 | ArrayList、LinkedList、Vector | HashSet、LinkedHashSet 以及 TreeSet |
多列集合Map:键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。
实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
底层数据结构:
集合 | 底层数据结构 |
---|---|
ArrayList | 数组 |
LinkedList | 双向链表 |
HashSet | HashMap |
LinkedHashSet | LinkedHashMap |
TreeSet | 红黑树 |
HashMap | JDK1.7是数组+链表;JDK1.8是数组+链表+红黑树 |
LinkedHashMap | 底层仍然是数组和链表或红黑树,在此基础上,增加一条双向链表,可以保持键值对的插入访问顺序 |
HashTable(线程安全) | 数组+链表 |
TreeMap | 红黑树 |
线程安全的集合?
vector:就比ArrayList多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
statck:堆栈类,先进后出。
HashTable:就比HashMap多了个线程安全。
enumeration:枚举,相当于迭代器。
Java集合的快速失败机制 “fail-fast”?
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
迭代器 Iterator与foreach增强循环
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。只能单向遍历。
迭代器和foreach增强循环删除元素对比分析:
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
迭代器允许调用者在迭代过程中使用 Iterator.remove() 方法移除元素。
但是在foreach增强循环时,删除元素会报ConcurrentModificationException 异常。
for(Integer i : list){
list.remove(i)
}
1)在使用For-Each快速遍历时,ArrayList内部创建了一个内部迭代器iterator,使用的是hasNext和next()方法来判断和取下一个元素。
2)ArrayList里还保存了一个变量modCount,用来记录List修改的次数,而iterator保存了一个expectedModCount来表示期望的修改次数,在每个操作前都会判断两者值是否一样,不一样则会抛出异常;
3)在foreach循环中调用remove()方法后,会走到fastRemove()方法,该方法不是iterator中的方法,而是ArrayList中的方法,在该方法中modCount++; 而iterator中的expectedModCount并没有改变;
4)再次遍历时,会先调用内部类iteator中的hasNext(),再调用next(),在调用next()方法时,会对modCount和expectedModCount进行比较,此时两者不一致,就抛出了ConcurrentModificationException异常。
对比 | ArrayList | LinkedList |
---|---|---|
数据结构实现 | 动态数组、 | 双向链表 |
随机访问效率 | 快 | 慢,线性的数据存储方式,所以需要移动指针从前往后依次查找。 |
增删查找 | 查找快,增删慢 | 查找慢,增删快 |
内存空间占用 | 少 | 多,LinkedList 的节点除存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素 |
线程安全 | 线程不安全 | 线程不安全 |
初始化 | 初始化参数等于0,则将数组初始化为一个空数组;不等于0,将数组初始化为一个容量为10的数组 | |
扩容时机 | 当数组的大小大于初始容量时,进行扩容,新的容量为旧的容量的1.5倍 | Linked底层为双向链表,没有初始化大小,也没有扩容的机制 |
扩容方式 | 会以新的容量建一个原数组的拷贝,修改原数组,指向这个新数组,原数组被抛弃,会被GC回收 |
对比 | ArrayList | Vector |
---|---|---|
线程安全 | 非线程安全 | 使用了Synchronized 来实现线程同步,线程安全 |
扩容 | 1.5倍 | 2倍 |
性能 | 好 | 差 |
说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,HashSet 不允许重复的值。
HashSet是如何保证数据不可重复的?
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
8、java访问修饰符的作用范围
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
public : 对所有类可见。使用对象:类、接口、变量、方法。
二、异常
1、Error与Exception
Error | 运行时异常RuntimeException: | 编译时异常 |
---|---|---|
程序中无法处理的错误,表示运行应用程序中出现了严重的错误 | 程序本身可以捕获并且可以处理的异常,Java 编译器不会检查它 | Java 编译器会检查它 |
Virtual MachineError(虚拟机运行错误):OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误;NoClassDefFoundError(类定义错误) | NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。 | ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),必须手动在代码里添加捕获语句来处理该异常 |
NoClassDefFoundError 和 ClassNotFoundException 区别?
不同 | NoClassDefFoundError | ClassNotFoundException |
---|---|---|
类型 | Error | 编译时异常 |
原因 | JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义 | 1、ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;2、某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它 |
try-catch-finally 中哪个部分可以省略?
try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。如果只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略。
2 Java常见异常有哪些
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException:强制类型转换异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
三、并发编程
1、并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
并发编程三要素(线程的安全性问题体现在):
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因:
线程切换带来的原子性问题
缓存导致的可见性问题(线程工作缓存和主存)
编译优化带来的有序性问题(指令重排序)
解决办法:
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题。
2、进程与线程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
3、形成死锁的四个必要条件是什么
互斥条件: 线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
如何避免线程死锁?
破坏互斥条件:没有办法破坏,因为临界资源需要互斥访问。
破坏请求与保持条件:一次性申请所有的资源。
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
4、可以使用哪些方法创建多个线程?
(1)继承 Thread 类:
定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法;
创建自定义的线程子类对象;调用子类实例的start()方法来启动线程。
(2)实现 Runnable 接口:
定义Runnable接口实现类,并重写run()方法;
创建实现类实例myRunnable,以myRunnable作为参数创建Thread对象,该Thread对象才是真正的线程对象
调用线程对象的start()方法。
(3)实现 Callable 接口
创建实现Callable接口的实现类myCallable
以myCallable为参数创建FutureTask对象
将FutureTask作为参数创建Thread对象
调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
}
}
public class CallableTest {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
}
}
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中.
(4)使用 Executors 工具类创建线程池
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
runnable 和 callable异同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
Callable | Runnable |
---|---|
call方法有返回值,泛型,和Future、FutureTask配合可以用来获取异步执行的结果 | run 方法无返回值 |
call 方法允许抛出异常,可以获取异常信息 | run 方法只能抛出运行时异常,且无法捕获处理 |
5、线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
6、与线程同步以及线程调度相关的方法。
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常,不释放锁;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。
Java的线程调度算法是抢占式算法。可能会到导致饥饿。
sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
(1)Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
(2)一个线程完全可以持有很多锁,如果是Thread类的方法,线程放弃锁不知道要放弃哪个锁。
综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。
为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
线程的 sleep()方法和 yield()方法有什么区别?
(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
Java中线程通信协作的最常见的两种方式:
1.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
2.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
5、多线程中有哪些锁。
(1)公平锁与非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
(2)可重入锁
自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
ReentrantLock、synchronized均支持
(3)独享锁与共享锁
独享锁是指该锁一次只能被一个线程所持有; synchronized、ReentrantLock 是独享锁。
共享锁是指该锁可被多个线程所持有。 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
(4)互斥锁和读写锁
互斥锁在Java中的具体实现就是ReentrantLock;读写锁在Java中的具体实现就是ReadWriteLock。
(5)乐观锁和悲观锁
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。 synchronized 关键字的实现也是悲观锁。
(6)分段锁
对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)可重入锁。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
(7)偏向锁/轻量级锁/重量级锁
通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
(8)自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
6、线程同步和线程互斥
当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
线程互斥是指对于共享的进程系统资源,单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
实现线程同步的方法
同步代码方法:sychronized 关键字修饰的方法
同步代码块:sychronized 关键字修饰的代码块
使用volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
使用重入锁实现线程同步:reentrantlock类是可重入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
7、在 Java 程序中怎么保证多线程的运行安全?
方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized。
方法三:使用手动锁 ReentrantLock。
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
8、synchronized 和 Lock 有什么区别?synchronized 和 ReentrantLock 区别是什么?synchronized 和 volatile 的区别是什么?
synchronized 和 Lock 有什么区别?
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。
相同点:两者都是可重入锁
主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
synchronized 和 volatile 的区别是什么?
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
4、线程池了解吗,线程池工作的原理?线程的生命周期?
如果你提交任务时,线程池队列已满,这时会发生什么
这里区分一下:
(1)如果使用的是无界队列 LinkedBlockingQueue,可以继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
(2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
ThreadPoolExecutor 饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满时,ThreadPoolTaskExecutor 定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:??调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
线程池状态:
运行状态(RUNNING):此状态下,线程池可以接受新的任务,也可以处理阻塞队列中的任务。执行shutdown()方法可进入待关闭(SHUTDOWN)状态,执行shutdownNow()方法可进入停止(STOP)状态。
待关闭状态(SHUTDOWN):此状态下,线程池不再接受新的任务,继续处理阻塞队列中的任务。当阻塞队列中的任务为空,且工作线程数为0的时候,进入整理(TIDYING)状态。
停止状态(STOP):此状态下,线程池不接受新任务,也不处理阻塞队列中的任务,反而会尝试结束执行中的任务。当工作线程数为0时,进入整理(TIDYING)状态。
整理状态(TIDYING):此状态下,所有任务都已经执行完毕,且没有工作线程。执行terminated()方法进入终止(TERMINATED)状态。
终止状态(TERMINATED):此状态下,线程池完全终止,并完成了所有资源的释放。
ThreadPoolExecutor构造函数重要参数分析
线程状态和工作线程的数量
首先线程池是有状态的,在不同的状态下,线程池的行为是不一样的。
然后线程池肯定是需要线程去执行具体的任务,所以在线程池中就封装了一个内部类Worker作为工作线程,每个Worker中都维持着一个Thread。
ThreadPoolExecutor中用一个AtomicInteger型的变量就保存了这两个属性的值,那就是ctl。
ctl是一个原子操作类型(AtomicInteger)的变量。ctl的高3位用来表示线程池的状态(runState),低29位用来表示工作线程的个数(workerCnt)
核心线程和最大线程数
corePoolSize
:核心线程数,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize
:线程池中允许存在的工作线程的最大数量
工作线程数在0-最大线程数之间。
创建线程的工厂
threadFactory
缓存任务的阻塞队列workQueue
当线程池接受到一个任务时,如果工作线程数没有达到corePoolSize,那么就会新建一个线程,并绑定该任务,直到工作线程的数量达到corePoolSize前都不会重用之前创建的线程。
当工作线程数达到corePoolSize,接收到新任务时,会将任务存放在一个阻塞队列(workQueue)中等待核心线程去执行。为什么不直接创建更多的线程来执行新任务呢?原因是核心线程中很可能已经有线程执行完自己的任务了,或者有其他线程马上就能处理完当前的任务,并且接下来就能投入到新的任务中去,所以阻塞队列是一种缓冲机制,给核心线程一个机会让他们充分发挥自己的能力。另外一个值得考虑的原因是,创建线程毕竟是代价昂贵的,不可能一有任务要执行就去创建一个新的线程。
所以我们需要为线程池配备一个阻塞队列,用来临时缓存任务,这些任务将等待工作线程来执行。
非核心线程存活的时间keepAliveTime
线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
当工作线程数达到corePoolSize时,线程池会将新接收到的任务放在阻塞队列中,而阻塞队列又分为两种情况:一种是有界的队列,一种是无界的队列。
如果是无界队列,那么当核心线程都在忙时,所有新提交的任务都会被存放在该无界队列中,这时最大线程数将变得没有意义,因为阻塞队列不会存在被装满的情况。
如果是有界队列,那么当阻塞队列中装满了等待执行的任务,这时再有新任务提交时,线程池就需要创建新的临时线程来处理。
但是创建的临时线程是有存活时间的,当阻塞队列中的任务被执行完毕,并且又没有那么多新任务被提交时,临时线程就需要被回收销毁,而在被回收销毁之前等待的这段时间,就是非核心线程的存活时间,也就是keepAliveTime属性。
核心线程跟创建的先后没有关系,而是跟工作线程的个数有关,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是非核心线程,都有被回收的可能。
一个线程执行完一个任务后,会去阻塞队列里面取新的任务,在取到任务之前,它就是一个闲置的线程。
取任务的方法有两种,一种是通过take()方法一直阻塞直到取出任务,另一种是通过poll(keepAliveTime, timeUnit)方法在一定时间内取出任务或者超时,如果超时这个线程就会被回收,请注意核心线程一般不会被回收。
每一个线程在取任务的时候,线程池会比较当前的工作线程个数与核心线程数。
1.如果工作线程数小于当前的核心线程数,则使用第一种方法取任务,也就是没有超时回收,这时所有的工作线程都是核心线程,它们不会被回收。
2.如果工作线程数大于核心线程数,则使用第二种方法取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。
拒绝策略handler
队列满的情况,工作线程的数据已经达到最大线程数的时候。此时既没有空余的队列空间来存放该任务,也无法创建新的线程来执行该任务,所以这时我们就需要有一种拒绝策略,即handler。
1、默认的拒绝策略:抛出异常。
2.直接丢弃该任务
3.使用调用者线程执行该任务
4.丢弃任务队列中的最老的一个任务,然后提交该任务
线程池工作流程
5、Atomic原子类AtomicInteger了解
实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。
当多个线程修改原子类时,采用CAS机制检查预测原值和内存地址处的数值是否相等,如果相等,则将内存地址的值修改为新值,如果其他线程在此阶段已经修改了值,则会不断循环。
6、AQS底层实现
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS 对资源的共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 。
自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。只需要实现共享资源 state 的获取与释放方式即可。
四、JVM优化
1、深拷贝、浅拷贝
浅拷贝:
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址 ,因此如果改变了这个地址中的值,就会影响到另一个对象.
浅拷贝特点
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
(2) 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。
深拷贝
在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。
深拷贝特点
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
(2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
(3) 深拷贝相比于浅拷贝速度较慢并且花销较大。
2、处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
3、对象的访问定位
Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
句柄访问
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针
如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
4、Java会存在内存泄漏吗?请简单描述
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
Java垃圾回收算法是可达性分析,不是引用计数器,引用计数器会出现循环引用而没有回收的情况,但是在Java中不会出现这种情况。
5、说一下JMM内存模型
JMM定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
工作内存与主内存交互
java虚拟机中主内存和工作内存交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。
lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线程独占这个变量
unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
原子性:synchronized
可见性:synchronized、volatile
有序性:synchronized、volatile
happens-before:规定对共享变量的写操作对其他线程的读操作可见
6、说一下在垃圾回收过程中都有哪些回收算法?
标记-清除、标记-整理、复制、分代回收算法
7、垃圾回收器【CMS、G1】
CMS:初始标记,并发标记,重新标记,并发处理
8、类加载器有哪些,类加载过程。
启动类加载器、拓展类加载器、应用系统类加载器、自定义类加载器
类加载:加载、链接(验证、准备、解析)、初始化。
9、JVM内存结构
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:堆、方法区
10、OOM异常怎么排查,如何调优
最常见的OOM情况有以下三种:
位置 | 堆内存溢出 | 方法区溢出 | 虚拟机栈溢出 |
---|---|---|---|
分析 | 内存泄露或者堆的大小设置不当 | CGlib等反射机制导致大量的class信息 | 死循环或者深度递归调用 |
解决 | 内存泄露:内存监控软件查找程序中的泄露代码。堆大小:-Xms,-Xmx **参数修改。 | XX:PermSize=64m -XX:MaxPermSize=256m 修改方法区大小 | -Xss 修改栈大小 |
排查手段
一般手段是:先通过内存映像工具对Dump出来的堆转储快照进行分析,先分清楚到底是出现了内存泄漏还是内存溢出。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。这样就能够找到泄漏的对象是通过怎么样的路径与GC Roots相关联的导致垃圾回收机制无法将其回收。掌握了泄漏对象的类信息和GC Roots引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果不存在泄漏,那么就是内存中的对象确实必须存活着,那么此时就需要通过虚拟机的堆参数( -Xmx和-Xms)来适当调大参数;从代码上检查是否存在某些对象存活时间过长、持有时间过长的情况,尝试减少运行时内存的消耗。
常用的 JVM 调优的参数都有哪些?
-Xms2g
:初始化推大小为 2g;
-Xmx2g
:堆最大内存为 2g;
-XX:NewRatio=4
:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8
:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC
:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC
:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC
:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC
:开启打印 gc 信息;
-XX:+PrintGCDetails
:打印 gc 详细信息
11、JVM堆内存默认比例
新生代:老年代=1:2;
Eden:From:To=8:1:1
12、类加载机制
三、框架
1、Spring框架中AOP,IOC分别介绍一下。
IOC:控制反转,由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
控制反转(IoC)有什么作用
管理对象的创建和依赖关系的维护。
解耦,由容器去维护具体的对象
托管了类的产生过程
Spring 中的 IoC 的实现原理就是工厂模式加反射机制。
控制反转IoC主要实现方式有两种:依赖注入和依赖查找
依赖注入:组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系
依赖注入分为接口注入(Interface Injection),Setter方法注入(Setter Injection)和构造器注入(Constructor Injection)三种方式
2、Spring 事务传播机制
Spring支持两种类型的事务管理:
编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。
Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
说一下Spring的事务传播行为
spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
spring 的事务隔离?
spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:
ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
ISOLATION_READ_UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
ISOLATION_READ_COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server
的默认级别;ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL
的默认级别;ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
3、AOP面向切面编程是通过什么实现的?
Spring AOP and AspectJ AOP 有什么区别?AOP 有哪些实现方式?
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
JDK动态代理和CGLIB动态代理的区别
Spring AOP中的动态代理:JDK动态代理和CGLIB动态代理:
JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
13、动态代理讲一下。
spring MVC ,servlet生命周期
Linux
16、Linux操作系统,简单讲下你用过的指令。
17、创建文件如何操作,删除文件如何操作,在文件中查找想要的信息如何操作。
Redis
18、Redis了解吗?介绍一下。
redis分布式锁,redis持久化操作?rdb底层如何实现的?持久化操作如何不影响性能
19、微服务了解吗,介绍一下微服务是做什么的?
20、说一下在在浏览器中输入地址到主页显示的整个过程?
redis项目中的使用场景,缓存穿透、雪崩,redis持久化机制
计算机网络
21、Http协议?
22、对于状态码你有哪些了解?
tcp 和三次握手。
计算机网络五层协议哪五层
TCP和UDP区别
数据库
23、数据库存储引擎是用的什么数据结构,使用这种数据结构的优势在哪里?
24、数据库索引具有什么优势以及劣势?假如通过一个非主键索引去寻找相关信息,是如何进行寻找的。
数据库事务的四大原则,隔离级别,mvcc原理
myibatis #和$区别,缓存,和JDBC区别,如何获取自增主键
数据库乐观锁悲观锁
mysql的mvcc怎么实现的?底层数据结构是什么?
tomcat服务器
如何避免回表操作?
数据结构与算法
二叉树的遍历方式以及对应的时间复杂度
设计模式
2.设计模式了解哪些?讲一讲你知道的设计模式
分布式
分布式事务和分布式锁
零拷贝,浅拷贝