本文是由我的笔记整理而成,总结了面试中常见的知识点,同时这些知识点也是很多 Java 开发者容易搞混弄错的。
第一部分:面向对象基础知识
final 在 java 中有什么作用?
final修饰类,意味着该类不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被 abstract 声明,又被 final 声明。
final修饰方法,当使用 final 修饰方法时,这个方法无法被子类重写。但是,该方法仍然可以被继承。
final修饰变量,必须在声明时给出变量的初始值,而在以后的引用中只能读取。
String 属于基础的数据类型吗?
不是。
Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
String 类使用 final 修饰,无法被继承。
String 和 StringBuilder、StringBuffer 的区别?
String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String 对象的生成。
StringBuffer:跟 String 类似,但是值可以被修改,使用 synchronized 来保证线程安全。
StringBuilder:StringBuffer 的非线程安全版本,没有使用 synchronized,具有更高的性能,推荐优先使用。
String s = new String(“xyz”) 创建了几个字符串对象?
一个或两个。如果字符串常量池已经有“xyz”,则是一个;否则,两个。
当字符创常量池没有 “xyz”,此时会创建如下两个对象:
一个是字符串字面量 “xyz” 所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,此时该实例也是在堆中,字符串常量池只放引用。
另一个是通过 new String() 创建并初始化的,内容与”xyz”相同的实例,也是在堆中。
String s = “xyz” 和 String s = new String(“xyz”) 区别?
两个语句都会先去字符串常量池中检查是否已经存在 “xyz”,如果有则直接使用,如果没有则会在常量池中创建 “xyz” 对象。
另外,String s = new String(“xyz”) 还会通过 new String() 在堆里创建一个内容与 “xyz” 相同的对象实例。
所以前者其实理解为被后者的所包含。
== 和 equals 的区别是什么?
== 运算符,用于比较基础类型变量和引用类型变量。
对于基础类型变量,比较的变量保存的值是否相同,类型不一定要相同。
对于引用类型变量,比较的是两个对象的地址是否相同。equals 在 Object 方法中其实等同于 ==,但是在实际的使用中,equals 通常被重写用于比较两个对象的值是否相同。
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
不对。
hashCode() 和 equals() 之间的关系如下:
当有 a.equals(b) == true 时,则 a.hashCode() == b.hashCode() 必然成立,
反过来,当 a.hashCode() == b.hashCode() 时,a.equals(b) 不一定为 true。
重写 equals 方法需要重写 hashCode 方法吗?
是的。
JDK规定:如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。
重写 hashCode 方法用重写 equals 方法吗?
不用。
想弄明白这个问题首先需要知道 equals方法和 equals方法的含义。
equals方法的含义,即“两个对象相等”,hashCode 方法一般用于在 Map 、HashSet 集合类型中计算对象所处的下标时使用,那么理所当然“如果两个对象相等则它们应该位于相同的下标”,所以重写 equals 应该重写 hashcode 。反之,重写 hashCode 方法并没有涉及对象是否相等的问题,所以重写hashCode 不用重写 equals.
throw 和 throws 的区别?
throws 用在方法声明后面,表示该方法可能抛出异常,由该方法的调用者来处理。
throw 是在代码中向外抛异常的,抛出的是一个异常实例。
Java 是值传递吗?
值传递。
Java 中只有值传递,对于对象参数,值的内容是对象的引用。
重载(Overload)和重写(Override)的区别?
重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。
重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
第二部分:类加载相关
1. 什么是类加载?
类加载就是把编译后的 .class 文件加载到内存,然后 JVM 才可以对其进行校验、解析、初始化等一些列操作来生成 Java 可直接使用的类型。每个类被加载进入内存之后,系统都会为该类生成一个 Class 对象。
2. 类加载分为几个阶段?
按照 Java 虚拟机规范,从 Class 文件到加载到内存中的类,到类卸载出内存位置,它的整个生命周期包括如下七个阶段:

3. 什么是类加载器?
类加载器是一个用来加载类文件的类。
Java 源代码通过 javac 编译器编译成类文件。然后 JVM 来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。
Java 有三种默认使用的类加载器:
- Bootstrap类加载器
- Extension类加载器
- System类加载器(或者叫作Application类加载器)
Extension类加载器将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader实现。
Bootstrap类加载器负责加载 rt.jar 中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出NUllPointerException异常。Bootstrap加载器被称为初始类加载器。
System 类加载器(又叫作 Application 类加载器)。它负责从classpath环境变量中加载某些应用相关的类。
总结一下三种类加载器加载类文件的地方:
- Bootstrap类加载器 – JRE/lib/rt.jar
- Extension类加载器 – JRE/lib/ext或者java.ext.dirs指向的目录
- Application类加载器 – CLASSPATH环境变量, 由-classpath或-cp选项定义,或者是JAR中的Manifest的classpath属性定义.
4. 什么时候需要类加载?
- 遇到 new 关键字的时候
- 使用反射生成 Class 对象的时候如 Class.forName(…)
- 初始化一个类时
- JVM 启动时指定了 main 方法时。
5. 类加载器的机制/工作原理?
Java类加载器的工作基于三个机制:委托、可见性和单一性。
- 委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。
- 可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
- 单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。正确理解类加载器能够帮你解决 NoClassDefFoundError 和java.lang.ClassNotFoundException,因为它们和类的加载相关。
委托机制当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作 Abc.class,首先加载这个类的请求由Application 类加载器委托给它的父类加载器 Extension 类加载器,然后再委托给Bootstrap类加载器。Bootstrap 类加载器会先看看 rt.jar 中有没有这个类,因为并没有这个类,所以这个请求由回到 Extension 类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被 Extension 类加载器找到了,那么它将被加载,而 Application 类加载器不会加载这个类;而如果这个类没有被 Extension 类加载器找到,那么再由 Application 类加载器从classpath中寻找。记住 classpath 定义的是类文件的加载目录,而 PATH 是定义的是可执行程序如 javac,java 等的执行路径。
可见性机制根据可见性机制,子类加载器可以看到父类加载器加载的类,而反之则不行。所以下面的例子中,当Abc.class已经被Application类加载器加载过了,然后如果想要使用Extension类加载器加载这个类,将会抛出java.lang.ClassNotFoundException异常。
单一性机制根据这个机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写违反委托和单一性机制的类加载器是可能的,但这样做并不可取。你写自己的类加载器的时候应该严格遵守这三条机制。
6. 如何显式的加载类?
Java提供了显式加载类的API:Class.forName(classname) 和 Class.forName(classname, initialized, classloader)。
7. 简要描述 ClassLoader 加载类的过程
ClassLoader使用的是双亲委托模型来搜索类的,每个 ClassLoader 实例(比如扩展类加载器、系统类加载器)都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它 ClassLoader 实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器 Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常【重要!】。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的 Class 实例对象。
8. 为什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载。
9. JVM 在搜索类的时候,又是如何判定两个class是相同的呢?
JVM 在判定两个 class 是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。就算两个class是同一份 class 字节码,如果被两个不同的 ClassLoader 实例所加载,JVM 也会认为它们是两个不同class。
比如网络上的一个 Java 类 org.classloader.simple.NetClassLoaderSimple,javac 编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA 和 ClassLoaderB 这两个类加载器并读取了NetClassLoaderSimple.class 文件,并分别定义出了 java.lang.Class 实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。
第三部分:集合相关
1. HashMap 知识点
1.1 HashMap 实现原理?加载因子?扩容过程?
HashMap 底层是使用“数组+链表”的形式实现的。
默认容量16,默认加载因子 0.75。threshold(阈值) = 容量 * 加载引子。当元素容量超过 threshold 便会触发扩容。HashMap扩容一次扩容1倍。HashMap 默认最大容量MAXIMUM_CAPACITY= 1 << 30; 这个值是10亿多点,小于Integer最大值20多亿,所以你可以指定HashMap容量大于MAXIMUM_CAPACITY。
在HashMap的构造器中,你指定了一个容量值,然后内部数组的大小并不一定是这个容量,因为HashMap内部数组的length必须是2的次方。所以HashMap的构造器会做处理,即:
int capacity = 1;
while(capacity < initialCapacity)
capacity <<= 1; 【即如果你指定了的容量 initialCapacity 等于8,则 capacity 等于8;如果initialCapacity等于9,则capacity等于16】==》所以这里的capacity是指HashMap的真实容量。
- 一般来说,threshold 等于 capacity 乘以加载因子。
- HashMap扩容一次扩容1倍。resize(2 *table.length);
- HashMap扩容会使用新容量创建一个新数组,把原数组内容 for 循环 copy 到新数组。扩容的时候需要重新计算哈希值。
1.2 HashMap 在多线程操作的时候会产生哪些问题?
- 会产生脏数据的情况。
- 多线程操作可能会出现某个线程被hang住的情况。
- 对HashMap中的元素进行rehash过程,可能会进行两次。
- resize扩容,可能会进行两次,导致其中一次的操作被覆盖掉。
1.3 HashMap 怎么处理哈希冲突?
往 HashMap 中 put 一个 K/V 时,如果 key 出现碰撞(碰撞的意思是计算得到的 Hash 值相同,需要放到同一个 bucket 中),以链表的方式链接到后面。
2. CopyOnWriteArrayList
CopyOnWriteArrayList 顾名思义,“写时复制”。
任何对 CopyOnWriteArrayList 的修改都会导致整个数组被复制。CopyOnWriteArrayList 总结:
set元素的时候会加可重入锁,也就是说对于CopyOnWriteArrayList来说,同一时刻只能有一个线程set元素进行写。在写操作没结束之前,读操作读的都是旧数组里面的数据。===》》CopyOnWriteArrayList进行写的时候加锁,读的时候不加锁!!!
3. BlockingQueue
BlockingQueue 的实现都是线程安全的,但是批量的集合操作如 addAll, containsAll , retainAll 和 removeAll 因为不是原子操作所以不是线程安全的。
3.1 阻塞队列的原理是什么?
如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。==>正因为阻塞队列有这个“睡眠”和“唤醒”的动作,所以阻塞队列的实现类(比如ArrayBlockingQueue、LinkedBlockingQueue)中,都得有Condition。

4. ArrayBlockingQueue
ArrayBlockingQueue 使用数组实现的阻塞队列,它实现并发同步的原理是,读操作和写操作都需要获取到 AQS 独占锁(即ReentrantLock)才能进行操作。ArrayBlockingQueue 读和写,用的是一把锁。
ArrayBlokckingQueue是数组实现的阻塞队列,但这个阻塞队列并不是所有方法都是阻塞的,比如往队列里面添加元素,只有put方法会阻塞,阻塞时使用的是可重入锁的await方法,而offer、add方法并不会阻塞。===》》但不论是否阻塞,每个方法都加了可重入锁,是线程安全的。(即阻塞队列都是线程安全的)。ArrayBlokckingQueue是一个循环数组,当put的时候队列满了会设置putIndex=0重新从下表等于0的位置put。ArrayBlockingQueue读和写,用的是一把锁。而LinkedBlockingQueue读和写,用的两把锁。ArrayBlokckingQueue为什么读写使用一把锁?我觉得还是因为数组的入队和出队时间复杂度低,不像列表需要额外维护节点对象。所以当入队和出队并发执行时,阻塞时间很短。如果使用双锁的话,会带来额外的设计复杂性。
5. LinkedBlockingQueue
LinkedBlockingQueue 是用链表实现的阻塞队列,它其实也是有界的,大小是Integer.MAX_VALUE(21亿多)。

6. ConcurrentHashMap
ConcurrentHashMap 的大部分操作和 HashMap 是相同的,例如初始化,扩容和链表向红黑树的转变等。
但是,在 ConcurrentHashMap 中,大量使用了 U.compareAndSwapXXX 的方法,这个方法是利用一个 CAS 算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。
同时,在ConcurrentHashMap中还定义了三个原子操作,用于对指定位置的节点进行操作。这三种原子操作被广泛的使用在ConcurrentHashMap的get和put等方法中,正是这些原子操作保证了ConcurrentHashMap的线程安全。
实现原理
ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是 ConcurrentHashMap 的内部结构图:

从图中可以看到,ConcurrentHashMap内部分为很多个Segment,每一个Segment拥有一把锁,然后每个Segment(继承ReentrantLock):
|
1 |
static final class Segment <K,V> extends ReentrantLock implements Serializable |
Segment继承了ReentrantLock,表明每个segment【本身】都可以当做一个锁。(ReentrantLock前文已经提到,不了解的话就把当做synchronized的替代者吧)这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。Segment下面包含很多个HashEntry列表数组。对于一个key,需要经过三次(为什么要hash三次下文会详细讲解)hash操作,才能最终定位这个元素的位置,这三次hash分别为:
1,对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
2,将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
3,将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图可以看出之间的关系。
第四部分:线程池相关
1. Executors 创建线程池的几个方法
- newSingleThreadExecutor
|
1 2 3 4 5 6 |
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); |
创建一个单线程的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。【注意这个方法调用的ThreadPoolExecutor里初始线程数量是1,最大线程数量也是1,也就是说 newSingleThreadExecutor 创建的线程池里只有一个线程。==》创建一个线程池但是里面只有一个线程目的是什么呢?是让任务顺序执行。】
- newFixedThreadPool
|
1 2 3 4 5 |
new ThreadPoolExecutor(n Threads, n Threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); |
创建固定大小的线程池。可控制线程最大并发数,超出的线程会在队列中等待。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。【这种线程池适用于我们事先知道自己的业务量多少和对系统的压力多少, 因为这种线程池的任务队列是LinkedBlockingQueue, 可以任务是无限大小的队列, 当任务量很多的时候会对系统造成很大的压力】
- newCachedThreadPool
|
1 2 3 4 5 |
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); |
创建一个可缓存的线程池。如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool
|
1 2 3 4 5 |
super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue()); |
继续调用:
|
1 2 3 4 5 6 7 |
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); |
创建一个大小无限的线程池。支持定时及周期性任务执行。此线程池支持定时以及周期性执行任务的需求。
2. 线程池核心参数
在介绍使用 Executors 创建线程池的方法前,先介绍一下 ThreadPoolExecutor ,因为这些创建线程池的静态方法都是返回ThreadPoolExecutor对象,和我们手动创建 ThreadPoolExecutor 对象的区别就是我们不需要自己传构造函数的参数。
ThreadPoolExecutor 的构造函数共有四个,但最终调用的都是同一个:
|
1 2 3 4 5 6 7 8 |
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) |
构造函数参数说明:
- corePoolSize:线程池核心线程数量 ===》》【当核心线程未满时新来任务会创建核心线程,当corePoolSize满了以后,任务会进入阻塞队列,只有当阻塞队列满了以后,才会创建非核心线程,当最大线程数与队列均满了以后,才会执行拒绝策略。】
- maximumPoolSize :线程池最大数量
- keepAliveTime:空闲线程存活时间 ===》》这个参数非常重要。【当池中线程数大于核心线程数时,该时间为余下线程(存活线程总数-核心线程数)的最大空闲存活时间】
- unit => 时间单位
- workQueue:线程池所使用的缓冲队列
- threadFactory:线程池创建线程使用的工厂【线程池中的线程就是在这创建的】
- handler:线程池对拒绝任务的处理策略 ===》》 拒绝策略也很重要, 默认的拒绝策略是抛异常, 但是有的业务你需要需要处理这些异常的, 因为否则会出现数据问题, 所以默认的拒绝策略就不行。
执行逻辑说明:
判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务,若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中,若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务,若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关。
3. 线程状态
参照 Thread 类内部源码状态定义:java.lang.Thread.State
- New:尚未启动的线程的线程状态。【刚new的线程,还没有调用 start 方法】
- Runnable:可运行/运行中线程的状态,java没对“可运行/运行中”的线程状态做细分,所以一个线程调用了start方法,就是Runnable状态,但线程真正的状态可能是等待CPU调度、或者正在执行中。
- Blocked:线程阻塞等待监视器锁定的线程状态。处于synchronized同步代码块或方法中被阻塞。【阻塞状态,即获取锁而不可得的状态,官网文档上说该状态是等待监视器锁定的状态。】
- Waiting:等待线程的线程状态。下列不带超时的方式:无参的Object.wait、无参的Thread.join、无参的LockSupport.park=》官方文档就是说被这三个方法调用的时候会处于Waiting状态【waiting状态,线程自己释放CPU和锁】
- Timed Waiting:具有指定等待时间的等待线程的线程状态。下列超时的方式:Thread.sleep、带参的Object.wait、带参的Thread.join、LockSupport.parkNanos、LockSupport.parkUntil=》官方文档就是说被这五个方法调用的时候会处于Timed Waiting状态。
- Terminated:线程终止状态。
以上是 Thread 类内部源码定义的线程的6个状态。按照这个定义,其实6种状态描述的非常清晰,但是很多技术博客、文章都将“Blocked阻塞状态、Waiting状态、Timed Waiting状态”混为一谈了,把Waiting状态、Timed Waiting状态都称为“阻塞状态”。
如上,其实可以看到,同一种状态,可能是由不同的函数导致的。比如:
- Waiting状态:可能是wait()方法、join方法导致的。
- Timed Waiting状态:可能是sleep、wait(毫秒)等导致的,但sleep方法不会导致当前线程释放锁,而wait方法会。
- Blocked阻塞状态:只有在“获取锁而不可得”时,才称为阻塞状态。
关于“一个线程暂停执行了”,我看网上有多种描述,比如阻塞、等待、睡眠、挂起,等等,非常乱且易混淆。我在此重新对这些名词梳理下。一个线程执行中暂停执行了,有下面这几种情况:
- 阻塞状态(获取锁而不可得,使线程暂停执行,
该线程不会释放已有的锁和CPU==》错,阻塞状态的线程就没有获取到锁啊)。 - 等待状态(调用无参的wait、join、park等函数,使线程暂停执行,释放锁和CPU)。
- 限时等待状态(调用sleep函数、有参的wait、join等5个方法,使线程暂停执行,具体释放不释放锁,不同函数不同情况。比如sleep函数不释放锁和CPU=》“不释放CPU”这个描述不准确,如果不释放CPU则其他线程就获取不到CPU了,准确说是不释放锁,但睡眠时间一过就可以获取CPU执行)
- 线程挂起(调用suspend函数,suspend()使得线程进入暂停执行(不会释放锁),并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。这两个是过期方法。)
第五部分:线程安全
1. ThreadLocal 原理
ThreadLocal 的原理,见我的文章 ThreadLocal 原理分析 。
2. volatile
参笔记:volatile 详解
3. AtomicInteger
AtomicInteger 里面其实使用的 CAS 原理,最终调用的 unSafe 的 CAS 方法。
4. 什么是可重入锁?synchroniized ? ReentrantLock ?
可重入锁,指锁可以被单个线程多次获取。Synchronized和ReentrantLock都是可重入锁。

ReentrantLock 和 synchronized 都是可重入锁。而 LockSupport.park 则是不可重入的,连续调用两次 LockSupport.park 则这个线程就会阻塞了。
park/unpark 模型真正解耦了线程之间的同步,线程之间不再需要一个 Object 或者其它变量来存储状态,不再需要关心对方的状态。Synchronized 的阻塞无法被中断,而 ReentrantLock 则提供了可中断的阻塞。
synchronized 关键字是不能继承的,也就是说,基类的方法 synchronized f(){} 在继承类中并不自动是 synchronized f(){},而是变成了f(){}。
5. 什么是死锁?怎么避免死锁的发生?
当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件(即多个线程不能同时使用同一个资源)
- 持有并等待条件(当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1 )
- 不可剥夺条件(当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取。)
- 环路等待条件(在死锁发生的时候,两个线程获取资源的顺序构成了环形链)
怎么检测死锁?
jstack 是 java 虚拟机自带的一种堆栈跟踪工具。jstack 用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息。jstack 主要用于生成 java 虚拟机当前时刻的线程快照,线程快照是当前 java 虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过 jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
怎么避免死锁的发生?
因为已经知道死锁发生必须要满足的四个条件,所以只要能打破任何其中一个以上的条件即可。
怎么防止死锁,怎么没了?
怎么防止死锁,这块怎么不写了