六、Java底层-JVM
1.谈谈你对java的理解
- 平台无关性一次编译到处运行
- GC 垃圾回收机制
- 语言特性 泛型反射lanbuda表达式
- 面相对象 并发
- 类库 IO
- 异常处理
2.平台无关性如何实现
Compile Once ,Run Any Where(平台无关性) 如何实现?
编译时
运行时

java源码,首先被编译成字节码,再由不同平台的Jvm进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
如何查看字节码:Javap 命令
为什么JVM不直接将源码解析成机器码去执行?
- 准备工作:每次执行都需要各种检查(语法,语义)
- 兼容性:也可以将别的语言解析成字节码
3.JVM如何加载.class文件
java虚拟机

- Class Loader:依据特定格式,加载class文件到内存
- Execution Engine:对命令进行解析
- Native Interface : 融合不同开发语言的原生库为Java所用
- Runtime Data Area:JVM内存空间结构模型
4.谈谈反射
java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
写一个反射的例子
package com.interview.javabasic.reflect;
public class Robot {
private String name;
public void sayHi(String helloSentence){
System.out.println(helloSentence + " " + name);
}
private String throwHello(String tag){
return "Hello " + tag;
}
static {
System.out.println("Hello Robot");
}
}
package com.interview.javabasic.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
Class rc = Class.forName("com.interview.javabasic.reflect.Robot"); //获取这个类
Robot r = (Robot) rc.newInstance();//创建RC实例
System.out.println("Class name is " + rc.getName());//输出类全限定名
Method getHello = rc.getDeclaredMethod("throwHello", String.class); //getDeclaredMethod获取私有方法throwHello方法
getHello.setAccessible(true);//因为方法是私有的,这里要设置为true
Object str = getHello.invoke(r, "Bob");//给方法传参
System.out.println("getHello result is " + str);
Method sayHi = rc.getMethod("sayHi", String.class);//getMethod 能获取public上的方法也能获取继承类的方法
sayHi.invoke(r, "Welcome");//传参
Field name = rc.getDeclaredField("name");//给类中的参数赋值
name.setAccessible(true);
name.set(r, "Alice");
sayHi.invoke(r, "Welcome");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}
5.谈谈ClassLoader
类从编译到执行的过程?
- 编译器将Robot.java源文件边翼卫Robot.class字节码文件
- ClassLoader将字节码转换为JVM中的Class Robot 对象
- JVM利用Class Robot 对象实例化为Robot对象
谈谈ClassLoader ?
ClassLoader在java中有着非常重要的作用,它主要工作在Class撞在的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
ClassLoader的种类
- BootStrapClassLoader:C++编写,加载核心库Java.*
- ExtClassLoader : java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录
自定义ClassLoader的实现?
关键函数:
package com.interview.javabasic.reflect;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
//用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
package com.interview.javabasic.reflect;
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("/Users/baidu/Desktop/", "myClassLoader");
Class c = m.loadClass("Wali");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
System.out.println(c.getClassLoader().getParent().getParent());
System.out.println(c.getClassLoader().getParent().getParent().getParent());
c.newInstance();
}
}
6.谈谈类加载器的双亲委派机制

- 避免多份同样字节码的加载
7.loadClass和forName的区别
类的加载方式
- 隐式加载 : new
- 显示加载:loadClass,forName 等
loadClass和forName的区别?
类的装载过程

- Class.forName得到的class是已经初始化完成的
- ClassLoader.loadClass得到的class是还没有连接的
package com.interview.javabasic.reflect;
public class LoadDifference {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader cl = Robot.class.getClassLoader();
Class r = Class.forName("com.interview.javabasic.reflect.Robot");
Class.forName("com.mysql.jdbc.Driver");
}
}
8.你了解Java的内存模型嘛?
内存简介

- 32位处理器:2的23次方的可寻址范围
- 64位处理器:2的64的次方可寻址范围
地址空间的划分
- 内核空间:计算机硬件 联网等逻辑
- 用户空间:java可用

JVM架构图

- Class Loader:依据特定格式,加载class文件到内存
- Execution Engine:对命令进行解析
- Native Interface : 融合不同开发语言的原生库为Java所用
- Runtime Data Area:JVM内存空间结构模型
JVM内存模型 -- JDK8

- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:MetaSpace、Java堆
程序计数器(program counter register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取吓一跳需要执行的字节码指令
- 和线程是一对一的关系,即“线程私有”
- 对Java方法技术,如果是Native方法则计数器的值为Undefined
- 由于只是记录行号,所以不会发生内存泄露的问题
Java虚拟机栈(Stack)
- Jva方法执行的内存模型
- 包含多个栈帧

局部变量表和操作数栈
局部变量表:包含方法执行过程中的所有变量

操作数栈:入栈、出栈、复制、交换、产生消费变量
package com.interview.javabasic.jvm.model;
public class ByteCodeSample {
public static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}
}
执行add(1,2)

package com.interview.javabasic.jvm.model;
public class Fibonacci {
//F(0)=0,F(1)=1,当n>=2的时候,F(n) = F(n-1) + F(n-2),
//F(2)=F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1+1 = 2
//F(0)-F(N) 依次为 0,1,1,2,3,5,8,13,21,34...
public static int fibonacci(int n){
if(n == 0) {return 0;}
if(n == 1) {return 1;}
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
System.out.println(fibonacci(1000000));
}
}
递归为什么会引发java.long.StackOverflowError异常?

- 递归过深,栈帧数超出虚拟栈深度
虚拟机栈过多会引发java.lang.OutOfMemoryError异常

本地栈方法
- 与虚拟机栈相似,主要作用于标注了native的方法
元空间(MateSpace)与永久代(PermGen)的区别
元空间使用本地内存,而永久代使用的是jvm内存
java.lang.OutOfMemoryError:PermGen space
MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难易确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便HotSport与其他JVM如Jrockit的集成
Java堆(Heap)
- 对象实例的分配区域

- GC管理的主要区域

JVM三大性能调优参数 -Xms -Xmx -Xss 的含义
java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值
Java内存模型中堆和栈的区别 --- 内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
Java内存模型中堆和栈的区别
- 联系:引用对象,数组时,栈里定义变量保存堆中目标的首地址

- 管理方式:栈自动释放,堆需要GC
- 空间大小:堆比栈小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
元空间、堆、线程独占部分间的联系 —— 内存角度


不同JDK版本之间的intern()方法的区别—— JDK6 VS JDK6+
String s = new String("a");
s.intern();
JDK6:当调用intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则将此字符串对象添加到字符串常量池中,并返回该字符串对象的引用。
JDK6+:当调用intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返会池子中的该字符串的引用。菲欧则如果该字符串对象已经存在于Java堆中,则将堆中堆此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
package com.interview.javabasic.jvm.model;
import java.util.Random;
public class PermGenErrTest {
public static void main(String[] args) {
for(int i=0; i <= 1000; i++){
//将返回的随机字符串添加到字符串常量池中
getRandomString(1000000).intern();
}
System.out.println("Mission Complete!");
}
//返回指定长度的随机字符串
private static String getRandomString(int length) {
//字符串源
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for ( int i = 0; i < length; i++){
int number = random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
}
//JDK6中报错 6+没事
package com.interview.javabasic.jvm.model;
public class InternDifference {
public static void main(String[] args) {
String s = new String("a");
s.intern();
String s2 = "a";
System.out.println(s == s2);
String s3 = new String("a") + new String("a");
s3.intern();
String s4 = "aa";
System.out.println(s3 == s4);
}
}
jdk:1.8
false
true
jdk:1.6
false
false

七、Java底层 - GC
1.Java垃圾回收机制
对象被判断为累计的标准
- 没有被其他对象所引用的时候
判定对象是否为垃圾的算法
- 引用计数算法
- 可达性分析算法
引用计数算法
判断对象的引用数量
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例,可以被当做垃圾收集的
- 优点:执行效率高,程序执行收影响较小
- 缺点:无法检测出循环引用的情况,导致内存泄露
package com.interview.javabasic.jvm.gc;
public class MyObject {
public MyObject childNode;
}
package com.interview.javabasic.jvm.gc;
public class ReferenceCounterProblem {
public static void main(String[] args) { //循环引用
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.childNode = object2;
object2.childNode = object1;
}
}
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收

可以作为GC Root的对象
- 虚拟机种引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
2.垃圾回收算法
谈谈你了解的垃圾回收算法?
标记- 清除算法(Mark and Sweep)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存

- 碎片化
复制算法(Copying)
- 分为对象面和空闲面
- 对象在对相面上创建
- 存活的对象被从对象面复制到空闲面
- 将对象面所有对象内存清除

- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景
标记-整理算法(Compacting)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:移动所有的存活对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收

- 避免内存的不连续性
- 不用设置两块内存互换
- 适用于存活率高的场景
分代收集算法(Generational Collector)
垃圾回收算法的组合拳
按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
目的:提高JVM的垃圾回收效率
Jdk6,jdk7

jdk8及其以后的版本

GC的分类
- Minor GC
- Full GC
年轻代:尽可能快速的地收集掉那些生命周期短的对象
- Eden 区
- 两个Survivor区

年轻代
年轻代垃圾回收的过程演示

对象如何晋升到老年代
- 经历一定Minor次数依然存活的对象
- Survivor区中存放不下的对象
- 新生成的大对象(-XX:+PretenuerSizeThreshold)
常用的调优参数
- -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
- -XX:NewRatio:老年代和年轻代内存大小的比例
- -XX:MaxTenuringThreshold:对象从年轻代晋升到老生代进过GC次数的最大阈值
老年代:存放生命周期较长的对象

- 标记-清理算法
- 标记-整理算法
老年代
- Full GC和Major GC
- Full GC比Major GC慢,但执行频率低
触发Full GC 的条件
- 老年代空间不足
- 永久代空间不足
- CMS GC时出现promotion failed ,concurrent mode failure
- Minor GC 晋升到老年代的平均大小大于老年代的剩余空间
- 调用System.gc();
- 使用RMI来进行RPC或者管理的JDK应用,每小时执行一次Full GC
3.分代收集算法(Gennerational Collector)
Stop - the - World
- JVM由于要执行GC而停止了应用程序的执行
- 任何一种GC算法中都会发生
- 多数GC优化通过减少Stop - the - world发生的时间来提高程序性能
Safepoint
- 分析过程中对象引用关系不会发生变化的点
- 产生SafePoint的地方:方法调用;循环跳转;异常跳转等
- 安全点数量得适中
常见的垃圾收集器
Jvm的运行模式
- server
- Client
C:\Users\Administrator>java -version
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
垃圾收集器之间的联系

年轻代常见的垃圾收集器
Serial收集器(-XX:+UseSerialGC)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器

ParNew收集器(-XX:+UseParNewGC,复制算法)
多线程收集,其余的行为,特点和Serial收集器一样
单核执行效率不如Serial,在多核下执行才有优势

Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
- 吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 多线程垃圾回收
- 比起关注用户线程停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server模式下默认的年轻代收集器

老年代常见的垃圾收集器
Serial Old收集器(-XX:+UseSerialOldGC,标记整理算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器

Parallel Old收集器(-XX:+useParallelOldGC,标记整理算法)
- 多线程,吞吐量优先

CMS收集器(-XX:+UseConcMarkSweepGC,标记清除算法)
- 初始标记:stop - the - world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年前代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集齐的数据结构

G1搜集器(-XX:+UserG1GC,复制+标记-整理算法)
Garbage First收集器的特点
并发和并行
分代收集
空间整合
可预测停顿
将整个java堆内存划分成多个大小相等Region
年轻代和老年代不在物理隔离

垃圾收集器之间的兼容联系

4.GC相关的面试题
Object的finalize()方法的作用是否与C++的构析函数作用相同
- 与C++的构析函数不同,析构函数调用确定,而它的是不确定的
- 将未被引用的对象放置于F-Queue队列
- 方法执行随时可能会被终止
- 给予对象最后一次重生的机会
package com.interview.javabasic.jvm.gc;
import java.lang.ref.ReferenceQueue;
public class Finalization {
public static Finalization finalization;
@Override
protected void finalize(){
System.out.println("Finalized");
finalization = this; //重生
}
public static void main(String[] args) {
Finalization f = new Finalization();
System.out.println("First print: " + f);
f = null;
System.gc();
try {// 休息一段时间,让上面的垃圾回收线程执行完成
Thread.currentThread().sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Second print: " + f);
System.out.println(f.finalization);
}
}
Java中的强引用,软引用,弱引用,虚引用有什么用?
强引用
- 最普遍的引用:Object obj = new Object();
- 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
- 通过将对象设置为null来弱化引用,使其被回收
软引用
- 对象处在有用但费必须的状态
- 只有当内存空间不足时,GC会回收该引用对象的内存
- 可以用来实现高速缓存
String str =new String(“abc”);//强引用
SoftReference<String> softRef=new SoftReference<String>(str);//软引用
弱引用
- 非必须的对象,比软引用更弱一些
- GC时会被回收
- 被回收的概率也不大,因为GC线程优先级比较低
- 适用于引用偶尔被使用且不影响垃圾收集的对象
String str=new String("abc");
WeakReference<String> abcWeakRef=new WeakReference<String>(str);
虚引用
- 不会决定对象的生命周期
- 任何时候都可能被垃圾收集器回收
- 跟踪对象被垃圾收集器回收的活动,起哨兵作用
- 必须和引用对鞋ReferenceQueue联合使用
String str=new String("abc");
ReferenceQueue queue=new ReferenceQueue();
PhantomReference ref =new PhantomReference(str,queue);
强引用 > 软引用 > 弱引用 > 虚引用

类层次结构
引用队列(Rreference Queue)
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
- 存储关联的且被GC的软引用,弱引用以及虚引用
package com.interview.javabasic.jvm.gc;
public class NormalObject {
public String name;
public NormalObject(String name){
this.name = name;
}
@Override
protected void finalize(){
System.out.println("Finalizing obj " + name);
}
}
package com.interview.javabasic.jvm.gc;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class NormalObjectWeakReference extends WeakReference<NormalObject> {
public String name;
public NormalObjectWeakReference(NormalObject normalObject, ReferenceQueue<NormalObject> rq) {
super(normalObject, rq);
this.name = normalObject.name;
}
@Override
protected void finalize(){
System.out.println("Finalizing NormalObjectWeakReference " + name);
}
}
package com.interview.javabasic.jvm.gc;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
public class ReferenceQueueTest {
private static ReferenceQueue<NormalObject> rq = new ReferenceQueue<NormalObject>();
private static void checkQueue(){
Reference<NormalObject> ref = null;
while ((ref = (Reference<NormalObject>)rq.poll()) != null){
if (ref != null){
System.out.println("In queue: " + ((NormalObjectWeakReference)(ref)).name);
System.out.println("reference object:" + ref.get());
}
}
}
public static void main(String[] args) {
ArrayList<WeakReference<NormalObject>> weakList = new ArrayList<WeakReference<NormalObject>>();
for (int i =0; i < 3 ; i++){
weakList.add(new NormalObjectWeakReference(new NormalObject("Weak " + i),rq));
System.out.println("Created weak:" + weakList.get(i));
}
System.out.println("first time");
checkQueue();
System.gc();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("second time");
checkQueue();
}
}
Created weak:com.interview.javabasic.jvm.gc.NormalObjectWeakReference@4554617c
Created weak:com.interview.javabasic.jvm.gc.NormalObjectWeakReference@74a14482
Created weak:com.interview.javabasic.jvm.gc.NormalObjectWeakReference@1540e19d
first time
Finalizing obj Weak 2
Finalizing obj Weak 1
Finalizing obj Weak 0
second time
In queue: Weak 0
reference object:null
In queue: Weak 2
reference object:null
In queue: Weak 1
reference object:null
八、Java多线程并发
1.进程和线程的区别
进程和线程的由来

进程和线程的区别?
进程是资源分配的最小单位,线程是CPU调度的最小单位
- 所有于进程相关的资源,都被记录在PCB中
- 进程是抢占处理机的调度单位;线程属于某个进程,共享资源

- 线程只由堆栈寄存器、程序计数器和TCB组成

总结
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程程序健壮
- 进程的切换比线程的切换开销大
Java进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个程序会产生一个进程,进程包含至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
package com.interview.javabasic.thread;
public class CurrentThreadDemo {
public static void main(String[] args) {
System.out.println("Current Thread: " + Thread.currentThread().getName()); //获取当前线程名字
}
}
2.Thread中的start和run方法的区别
package com.interview.javabasic.thread;
public class ThreadTest {
private static void attack() {
System.out.println("Fight");
System.out.println("Current Thread is : " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
public void run(){
attack();
}
};
System.out.println("current main thread is : " + Thread.currentThread().getName());
t.run(); //main
t.start(); //thread-0
}
}

- 调用start()方法会创建一个新的子线程并启动
- run()方法只是Thread的一个普通方法的调用
3.Thread和Runnable是什么关系?
package com.interview.javabasic.thread;
public class MyThread extends Thread {
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
System.out.println("Thread start : " + this.name + ",i= " + i);
}
}
}
package com.interview.javabasic.thread;
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt1 = new MyThread("Thread1");
MyThread mt2 = new MyThread("Thread2");
MyThread mt3 = new MyThread("Thread3");
mt1.start();
mt2.start();
mt3.start();
}
}
package com.interview.javabasic.thread;
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
System.out.println("Thread start : " + this.name + ",i= " + i);
}
}
}
package com.interview.javabasic.thread;
public class RunnableDemo {
public static void main(String[] args) throws InterruptedException {
MyRunnable mr1 = new MyRunnable("Runnable1");
MyRunnable mr2 = new MyRunnable("Runnable2");
MyRunnable mr3 = new MyRunnable("Runnable3");
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
Thread t3 = new Thread(mr3);
t1.start();
t2.start();
t3.start();
}
}
- Thread是实现了Runnable接口的类,使得run支持多线程
- 因类的单一继承原则,推荐多使用Runnable接口
4.如何给run()方法传参?
实现方式主要有三种
- 构造函数传参
- 成员变量传参
- 回调函数传参
如何实现处理线程的返回值?
实现方式主要有三种
- 主线程等待法
package com.interview.javabasic.thread;
public class CycleWait implements Runnable{
private String value;
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
}
public static void main(String[] args) throws InterruptedException {
CycleWait cw = new CycleWait();
Thread t = new Thread(cw);
t.start();
while (cw.value == null){
Thread.currentThread().sleep(100);
}
System.out.println("value : " + cw.value);
}
}
- 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
package com.interview.javabasic.thread;
public class CycleWait implements Runnable{
private String value;
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
}
public static void main(String[] args) throws InterruptedException {
CycleWait cw = new CycleWait();
Thread t = new Thread(cw);
t.start();
t.join();
System.out.println("value : " + cw.value);
}
}
- 通过Callable接口实现:通过FutureTask 或者 线程池获取
FutureTask
package com.interview.javabasic.thread;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception{
String value="test";
System.out.println("Ready to work");
Thread.currentThread().sleep(5000);
System.out.println("task done");
return value;
}
}
package com.interview.javabasic.thread;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<String>(new MyCallable());
new Thread(task).start();
if(!task.isDone()){
System.out.println("task has not finished, please wait!");
}
System.out.println("task return: " + task.get());
}
}
task has not finished, please wait!
Ready to work
task done
task return: test
线程池
package com.interview.javabasic.thread;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
Future<String> future = newCachedThreadPool.submit(new MyCallable());
if(!future.isDone()){
System.out.println("task has not finished, please wait!");
}
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
newCachedThreadPool.shutdown();
}
}
}
task has not finished, please wait!
Ready to work
task done
test
5.线程的状态

六个状态
新建(New):创建后尚未启动的线程的状态
运行(Runnable):包含Running和Ready
无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
限期等待(Timed Waiting):在一定时间后会由系统能够自动唤醒

- 阻塞(Blocked):等待获取排他锁
- 结束(Terminated):已终止线程状态,线程已经结束执行
package com.interview.javabasic.thread;
public class ThreadTest {
private static void attack() {
System.out.println("Fight");
System.out.println("Current Thread is : " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
public void run(){
attack();
}
};
System.out.println("current main thread is : " + Thread.currentThread().getName());
t.run(); //main
t.start(); //thread-0
t.join();
t.start();
}
}
current main thread is : main
Fight
Current Thread is : main
Fight
Current Thread is : Thread-0
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.interview.javabasic.thread.ThreadTest.main(ThreadTest.java:19)
6.sleep和wait的区别
基本差别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以再任何地方使用
- wait方法只能在synchronized方法或者synchronized块中使用
最主要的本质区别
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
package com.interview.javabasic.thread;
public class WaitSleepDemo {
public static void main(String[] args) {
final Object lock = new Object(); // 创建一个共享对象作为锁
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 A 正在等待获取锁");
synchronized (lock){ // 同步块,获取锁
try {
System.out.println("线程 A 获取到锁");
Thread.sleep(20); // 线程 A 睡眠 20 毫秒
System.out.println("线程 A 执行 wait 方法");
lock.wait(); // 线程 A 进入等待状态
System.out.println("线程 A 完成");
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
try{
Thread.sleep(10); // 主线程睡眠 10 毫秒,让线程 A 先执行
} catch (InterruptedException e){
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 B 正在等待获取锁");
synchronized (lock){ // 同步块,获取锁
try {
System.out.println("线程 B 获取到锁");
System.out.println("线程 B 睡眠 10 毫秒");
Thread.sleep(10); // 线程 B 睡眠 10 毫秒
lock.notifyAll(); // 唤醒所有等待中的线程
Thread.yield(); // 让出 CPU
Thread.sleep(2000); // 线程 B 睡眠 2000 毫秒
System.out.println("线程 B 完成");
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}
}
线程 A 正在等待获取锁
线程 A 获取到锁
线程 B 正在等待获取锁
线程 A 执行 wait 方法
线程 B 获取到锁
线程 B 睡眠 10 毫秒
线程 B 完成
线程 A 完成
Process finished with exit code 0
7.notify和notifyAll的区别
两个概念
- 锁池EntryList
- 等待池WaitSet
锁池
假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个Synchronized方法或者块,由于B,C线程在进入对象的Synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池
假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象锁。
notify和notifyAll的区别
- notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。
- notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
package com.interview.javabasic.thread;
import java.util.logging.Level;
import java.util.logging.Logger;
public class NotificationDemo {
private volatile boolean go = false; // 使用 volatile 关键字保证多线程可见性
public static void main(String args[]) throws InterruptedException {
final NotificationDemo test = new NotificationDemo();
Runnable waitTask = new Runnable(){
@Override
public void run(){
try {
test.shouldGo(); // 调用等待方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行完毕");
}
};
Runnable notifyTask = new Runnable(){
@Override
public void run(){
test.go(); // 调用通知方法
System.out.println(Thread.currentThread().getName() + " 执行完毕");
}
};
Thread t1 = new Thread(waitTask, "WT1"); // 将等待
Thread t2 = new Thread(waitTask, "WT2"); // 将等待
Thread t3 = new Thread(waitTask, "WT3"); // 将等待
Thread t4 = new Thread(notifyTask,"NT1"); // 将通知
// 启动所有等待线程
t1.start();
t2.start();
t3.start();
// 暂停以确保所有等待线程成功启动
Thread.sleep(200);
// 启动通知线程
t4.start();
}
/*
* wait 和 notify 方法只能在同步方法或同步块中调用
*/
private synchronized void shouldGo() throws InterruptedException {
while(go != true){
System.out.println(Thread.currentThread()
+ " 准备在此对象上等待");
wait(); // 释放锁,并在唤醒时重新获取锁
System.out.println(Thread.currentThread() + " 被唤醒");
}
go = false; // 重置条件
}
/*
* shouldGo() 和 go() 都是在当前对象上使用 "this" 关键字进行锁定
*/
private synchronized void go() {
while (go == false){
System.out.println(Thread.currentThread()
+ " 准备通知所有或一个正在等待此对象的线程");
go = true; // 将条件设置为 true,以唤醒等待的线程
//notify(); // 仅唤醒等待的线程 WT1, WT2, WT3 中的一个
notifyAll(); // 唤醒所有等待的线程 WT1, WT2, WT3
}
}
}
8.yield函数
概念
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
package com.interview.javabasic.thread;
public class YieldDemo {
public static void main(String[] args) {
Runnable yieldTask = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
if (i == 5) {
Thread.yield(); // 当 i 等于 5 时,当前线程让出 CPU
}
}
}
};
Thread t1 = new Thread(yieldTask, "A");
Thread t2 = new Thread(yieldTask, "B");
t1.start();
t2.start();
}
}
9.如何中断线程
已经被抛弃的方法
- 通过调用stop()方法停止线程
- 通过调用suspend()和resume()方法
目前使用的方法
- 调用interrup(),通知线程应该中断了
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行不受影响
- 需要被调用的线程配合中断
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
package com.interview.javabasic.thread;
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Runnable interruptTask = new Runnable() {
@Override
public void run() {
int i = 0;
try {
//在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(100); // 休眠100ms
i++;
System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") loop " + i);
}
} catch (InterruptedException e) {
//在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)
System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") catch InterruptedException.");
}
}
};
Thread t1 = new Thread(interruptTask, "t1");
System.out.println(t1.getName() +" ("+t1.getState()+") is new.");
t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started.");
// 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");
// 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
}
}
t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (TERMINATED) is interrupted now.
Process finished with exit code 0
10.前述方法以及线程状态总结
线程状态以及状态之间的转换

九、线程并发Synchronized
1.Synchronized
线程安全问题的主要诱因
- 存在共享数据(也称临界资源)
- 存在多条线程共同操作这些共享数据
解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
互斥锁的特性
- 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
- Synchronized锁的不是代码,锁的都是对象
根据获取锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法
- 同步代码块(Synchronized(this),Synchronized(类实例对象)),锁是小括号()中的实例对象。
- 同步非静态方法(Synchronized method),锁是当前对象的实例对象。
package com.interview.javabasic.thread;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")) {
async();
} else if (threadName.startsWith("B")) {
syncObjectBlock1();
} else if (threadName.startsWith("C")) {
syncObjectMethod1();
} else if (threadName.startsWith("D")) {
syncClassBlock1();
} else if (threadName.startsWith("E")) {
syncClassMethod1();
}
}
/**
* 异步方法
*/
private void async() {
try {
System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 方法中有 synchronized(this|object) {} 同步代码块
*/
private void syncObjectBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized 修饰非静态方法
*/
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void syncClassBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized static void syncClassMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.interview.javabasic.thread;
public class SyncDemo {
public static void main(String... args) {
SyncThread syncThread = new SyncThread();
Thread A_thread1 = new Thread(syncThread, "A_thread1");
Thread A_thread2 = new Thread(syncThread, "A_thread2");
Thread B_thread1 = new Thread(syncThread, "B_thread1");
Thread B_thread2 = new Thread(syncThread, "B_thread2");
Thread C_thread1 = new Thread(syncThread, "C_thread1");
Thread C_thread2 = new Thread(syncThread, "C_thread2");
Thread D_thread1 = new Thread(syncThread, "D_thread1");
Thread D_thread2 = new Thread(syncThread, "D_thread2");
Thread E_thread1 = new Thread(syncThread, "E_thread1");
Thread E_thread2 = new Thread(syncThread, "E_thread2");
A_thread1.start();
A_thread2.start();
B_thread1.start();
B_thread2.start();
C_thread1.start();
C_thread2.start();
D_thread1.start();
D_thread2.start();
E_thread1.start();
E_thread2.start();
}
}
对象锁和类锁的总结
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块。
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞。
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞。
- 若锁住的是同一个对象,一个线程在访问对象的同步同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然。
- 同一个类的不同对象的对象锁互不干扰。
- 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只能有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的。
- 类锁和对象锁互不干扰。
2.Synchronized的底层原理
实现Synchronized的基础
- Java对象头
- Monitor
对象在内存中的布局
- 对象头
- 实例填充
- 对齐填充
对象头的结构

Mark Word

Monitor:每个Java对象天生自带了一把看不见的锁
Monitor:在JVM中通过C++实现
Monitor锁的竞争、获取与释放

package com.interview.javabasic.thread;
public class SyncBlockAndMethod {
public void syncsTask() {
//同步代码库
synchronized (this) {
System.out.println("Hello");
synchronized (this){
System.out.println("World");
}
}
}
public synchronized void syncTask() {
System.out.println("Hello Again");
}
}
什么是重入
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。
为什么会对Synchronized嗤之以鼻
- 早起版本中,Synchronized属于重量级锁,依赖于Mutex Lock实现
- 线程之间的切换需要从用户态转换到核心态,开销较大
Java6以后,Synchronized性能得到了很大提升
- Adaptive Spinning
- Lock Eliminate
- Lock Coarsening
- LightWeight Locking
- Biased Locking
自旋锁与自适应自旋锁
自旋锁
- 许多情况下,共享数据的锁定状态持续时间段,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU
- 缺点:若锁别其他线程长时间占用,会带来许多性能上的开销
- PreBlockSpin 参数可以更改
自适应自旋锁
- 自旋的次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除
更彻底的优化
- JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
package com.interview.javabasic.thread;
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
}
锁粗化
另一种极端
- 通过扩大加锁范围,避免反复加锁和解锁
package com.interview.javabasic.thread;
public class CoarseSync {
public static String copyString100Times(String target){
int i = 0;
StringBuffer sb = new StringBuffer();
while (i<100){
sb.append(target);
}
return sb.toString();
}
}
Synchronized的四种状态
- 无锁、偏向锁、轻量级锁、重量级锁
锁膨胀方向:无锁 - 偏向锁 - 轻量级锁 - 重量级锁
偏向锁:减少同一线程获取锁的代价
- 大多数情况下,锁不存在多线程竞争,总是有同一线程多次获得
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁的结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
- 不适用锁竞争比较激烈的多线程场合
CAS 无锁的算法
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁征用的时候,偏向锁就会升级为轻量级锁。
适用的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01“状态),虚拟机首先将在当前线程的帧中建立一个名为锁记录(锁记录)的空间,用于存储锁对象目前的标记字的拷贝,官方称之为替换标记字。这时候线程堆栈与对象头的状态如图所示。

(2)拷贝对象头中的标记字复制到锁记录中。 (3)拷贝成功后,虚拟机将使用中科院操作尝试将对象的标记字更新为指向锁记录的指针,并将锁记录里的所有者指针指向对象标记字。如果更新成功,则执行步骤(4),否则执行步骤(5)。 (4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁并且对象标记字的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
(5)如果这个更新操作失败了,虚拟机首先会检查对象的MarkWord 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为"10",MarkWord 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋咱们前面讲过,就是为了不让线程阻塞,而采用循环去获取锁的过程。
咱们再来讲讲解锁的过程 (1)通过CAS操作尝试把线程中复制的替换标记字对象替换当前的标记字。 (2)如果替换成功,整个同步过程就完成了。 (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

锁的内存语义
当线程释放锁时,java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时,Java内存模型会把该线程对应的本地内存置位无效,从而使得被监视器保护的临界区代码必须从中内存中读取共享变量。

偏向锁、轻量级锁、重量级锁的汇总

3.Synchronized和ReentrantLock的区别
ReentrantLock(再入锁)
- 位于java.util.concurrent.locks包
- 和CountDownlatch、FutureTask、Semaphore一样基于AQS实现
- 能够实现比Synchronized更细粒度的控制,如控制fairness
- 调用lock()之后,必须调用unlock()释放锁
- 性能未必比Synchronized高,并且也是可重入的
ReentrantLock公平性的设置
- ReentarantLock fairLock =new ReentrantLock(true);
- 参数为true时,倾向于将锁赋予等待时间最久的线程
- 公平锁:获取锁的顺序按先后调用Lock方法的顺序(慎用)
- 非公平锁:抢占的顺序不一定,看运气
- Synchronized是非公平锁
package com.interview.javabasic.thread;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo implements Runnable{
private static ReentrantLock lock = new ReentrantLock(false);
@Override
public void run(){
while (true){
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
Thread.sleep(1000);
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo rtld = new ReentrantLockDemo();
Thread thread1 = new Thread(rtld);
Thread thread2 = new Thread(rtld);
thread1.start();
thread2.start();
}
}
ReentrantLock将锁对象化
- 判断是否有线程,或者某个特定线程,再排队等待获取锁
- 带超时的获取锁的尝试
- 感知有没有成功获取锁
是否能将wait、notify、notifyAll对象化
- Java.util.concurrent.locks.Condition
总结:
- Synchronized是关键字,ReentrantLock是类
- ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活地实现多路通知
- 机制:sync操作Mark Word,lock调用Unsafe类的park()方法
4.jmm内存的可见性
什么是Java内存模型中的happens-before
Java内存模型JMM
Java内存模型(即Java Menory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范,通过这组规范定义了程序中各个变量(包含实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM中的主内存
- 存储Java实例对象
- 包含成员变量、类信息、常量、静态变量等
- 属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存
- 存储当前方法的所有本地变量信息,本地变量对其他线程不可见
- 字节码行号指示器,Native方法信息
- 属于线程私有区域,不存在线程安全问题
JMM与Java内存区域划分是不同的概念层次
- JMM描述的是一组规则,围绕原子性,有序性,可见性展开
- 相似点:存在共享数据区域和私有数据区域。
主内存与工作内存的数据存储类型以及操作方式归纳
- 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
- 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
- 成员变量、static变量、类信息均会被存储在主内存中
- 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存
JMM如何解决可见性问题

指令重排序需要满足的条件
- 在单线程环境下不能改变程序运行结果
- 存在数据依赖关系的不允许重排序
无法通过happens - before 原则推导出来的,才能进行指令的重排序
A操作的结果需要对B操作可见,则A与B存在happens - before关系
happens-before的八大原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:-个unLock操作先行发生于后面对同一个锁的lock操作;3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
happens - before的概念
- 如果两个操作不满足上述任意一个happens - before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;
- 如果操作A happens - before 操作B,那么操作A在内存上所做的操作对B操作都是可见的。
volatile:JVM提供的轻量级同步机制
- 保证被volatile修饰的共享变量对所有线程总是可见的
- 禁止指令的重排序优化
volatile的可见性
volatile变量为何立即可见?
- 当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
- 当读取一个volatile变量时,JMM会把该线程对应的工作内存置位无效。
volatile如何禁止重排优化
内存屏障(Memory Barrier)
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
单例双重检测实现


volatile和Synchronized的区别
- volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别volatile仅能实现变量的修改可见性,不能保证原子性;而
- synchronized则可以保证变量修改的可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
5.CAS(Compare and Swap)
一种高效实现线程安全性的方法
- 支持原子更新操作,适用于计数器,序列发生器等场景
- 属于乐观锁机制,号称lock-free
- CAS操作失败时由开发者决定是继续尝试,还是执行别的操作
CAS思想
- 包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
package com.interview.javabasic.thread;
public class CASCase {
public volatile int value;
public synchronized void add() {
value++;
}
}
CAS多数情况下对开发者来说是透明的
- JUC的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。
- Unsafe类虽然提供CAS服务,但因能够操纵任意内存地址读写而有隐患
- Java9以后,可以使用Variable Handle API来替代Unsafe
缺点
- 若循环时间长,则开销很大
- 只能保证一个共享变量的原子操作
- ABA问题 解决:AtomicStampedReference
6.Java线程池
利用Executors创建不同的线程池满足不同场景的需求
- newFixedThreadPool(int nThreads)指定工作线程数量的线程池
- newCachedThreadPool()
处理大量短时间工作任务的线程池
- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;(2)如果线程闲置的时间超过阈值,则会被终止并移出缓存;
- 系统长时间闲置的时候,不会消耗什么资源
- newSingleThreadExecutor()创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它
- newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
- newWorkStealingPool()内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序
Fork/Join框架 Work - Stealing 算法:某个线程从其他队列里窃取任务来执行
- 把大人物分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架。

为什么要使用线程池
- 降低资源消耗
- 提高线程的可管理性
Executor的框架

J.U.C的三个Executor接口
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
- ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善
- ScheduledExecutorService:支持Future和定期执行任务
ThreadPoolExecutor

ThreadPoolExecu
- corePoolSize:核心线程数量
- maximumPoolSize:线程不够用时能够创建的最大线程数
- workQueue:任务等待队列
- keepAliveTime:抢占的顺序不一定,看运行
- ThreadFactory:创建新线程,Executors.defaultThreadFactory()
- handler:线程池的饱和策略
- AbortPolicy:直接排除异常,这是默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
- 实现RejectedExecutionHandler接口的自定义handler
新任务提交execute执行后的判断
- 如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
- 如果线程池中的线程数量大于等于 corePoolSize 且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
- 如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
- 如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过hander所指定的策略来处理任务
流程图

线程池的状态
- RUNNING:能接收新提交的任务,并且也能处理阻塞队列中的任务
- SHUTDOWN:不在接收新提交的任务,但可以处理存量任务
- STOP:不再接收新提交的任务,也不处理存量任务
- TIDYING:所有的任务都已终止
- TERMINATED:terminated()方法执行完后进入该状态
状态转换图

工作线程的生命周期

线程池的大小如何选定?
- CPU密集型:线程数=按照核数或者核数+1设定
- I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)
十、Java常用类库与技巧
1.Java异常体系
String、StringBuffer、StringBuilder的区别
Java异常以及常用工具类体系
异常处理机制主要回答了三个问题
- what:异常类型回答了什么被抛出
- where:异常堆栈跟踪回答了在那抛出
- Why:异常信息回答了为什么被抛出
Java的异常体系

从概念角度解析Java的异常处理机制
Error:程序无法处理的系统错误,编译器不做检查
Exception:程序可以处理的异常,捕获后可能恢复
总结:前者是程序无法处理的错误,后者是可以处理的异常
RuntimeException:不可预知的,程序应当自行避免
非RuntimeException:可预知的,从编译器校验的异常
从责任角度看
- Error属于JVM需要负担的责任
- RuntimeException是程序应该负担的责任
- Checked Exception可检查异常是Java编译器应该负担的责任
常见的Error以及Exception
RuntimeExecution
- NullPointerExecution - 空指针异常
- ClassCastException - 类型强制转换异常
- IllegalArgumentExecution - 传递非法参数异常
- IndexOutOfBoundsException - 下标越界异常
- NumberFormatException - 数字格式异常
非RuntimeExecution
- ClassNotFoundException - 找不到指定class异常
- IOException - IO操作异常
Error
- NoClassDefFoundError - 找不到class定义的异常
- StackOverflowError - 深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError - 内存溢出异常
2.Java的异常处理机制
- 抛出异常:创建异常对象,交由运行时系统处理
- 捕获异常:寻找合适的异常处理器处理异常,否则终止运行
java异常的处理原则
- 具体明确:抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因;
- 提早抛出:应尽可能早点发现并抛出异常,便于精确定位问题;
- 延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。
高效主流的异常处理框架
在用户看来,应用系统发生的所有异常都是应用系统内部的异常
- 设计一个通用的继承自RuntimeException的异常来统一处理
- 其余异常都同意转译为上述异常AppException
- 在Catch之后,抛出上述异常的子类,并提供足以定位的信息
- 由前端接收AppException做统一处理

Java异常处理消耗性能的地方
- try -catch 块影响JVM的优化
- 异常对象实例需要保存栈快照等信息,开销较大
3.Java集合框架
工作中消失而面试缺长存的算法与数据结构
- 优秀的算法和数据结构被封装到了Java的集合框架中
数据结构考点
- 数组和链表的区别
- 链表的操作,如反转,链表环路检测,双向立案别,循环链表相关操作
- 队列,栈的应用
- 二叉树的遍历方式及其递归和非递归的实现
- 红黑树的旋转
算法考点
- 内部排序:如递归排序,交换排序(冒泡、快速),选择排序,插入排序,
- 外部排序:应掌握如何利用有限的内存配合海量的外部存储来处理超大数据集,写不出来也要有相关的思路
考点扩展
- 那些排序是不稳定的,稳定意味着什么
- 不同数据集,各种排序最好或最差的情况
- 如何优化算法
Java集合框架

集合之List和Set

4.集合之Map

HashMap、HashTable、ConccurentHashMap
HashMap(Java8以前):数组+链表


HashMap:put方法的逻辑
- 如果HashMap未被初始化过,则初始化
- 如果Key求Hash值,然后再计算下标
- 如果没有碰撞,直接放入桶中
- 如果碰撞了,以链表的方式链接到后面
- 如果链表长度超过阈值,就把链表转成红黑树
- 如果链表长度低于6,就把红黑树转回链表
- 如果节点已经存在就替换旧值
- 如果桶满了(容量16*加载因子0.75),就需要resize(扩容两倍后重排)
HashMap:如何有效减少碰撞
- 扰动函数:促使元素位置分布均匀,减少碰撞几率
- 使用final对象,并采用合适的equals()和hashCode()方法
HashMap:从获取hash到散列的过程

HashMap:扩容的问题
- 多线程环境下,调整大小会存在条件竞争,容易造成死锁
- rehashing是一个比较耗时的过程
HashMap知识点回顾
- 成员变量:数据结构,树化阈值
- 构造函数:延迟创建
- put和get流程
- 哈希算法,扩容、性能
5.ConcurrentHashMap
如何优化Hashtable?
- 通过锁细粒度化,将整锁拆解成多个锁进行优化
早期的ConcurrentHashMap:通过分段锁Segment来实现

当前的ConcurrentHashMap:CAS+Synchronized使锁更细化

ConcurrentHashMap:put方法的逻辑
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环
- 检查到内容正在扩容米酒帮助它一块扩容
- 如果f!=null,则使用Synchronized锁住f元素(链表/红黑二叉树的头元素)
- 如果是Node(链表结构),则执行链表的添加操作
- 如果是TreeNode(树形结构)则执行树添加操作
- 判断链表长度已经达到临界值8,当然这个8是默认值,大家也可以做调整,当节点数超过这个值就需要把立案别转换为树结构。
ConcurrentHashMap总结:比起segment,锁拆的更细
- 首先使用无锁操作CAS插入头节点,失败则循环重试
- 若头节点已存在,则尝试获取头节点的同步锁,再进行操作
ConcurrentHashMap:别的需要注意的点
- size()方法和mappingCount()方法的异同,两者计算是否准确?
- 多线程环境下如何进行扩容
HashMap、HashTable、ConccurentHashMap三者的区别?
- HashMap线程不安全,数组+链表+红黑树
- Hashtable线程安全,锁住整个对象,数据+链表
- ConccurentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
- HashMap的key,value均可以为null,而其他的两个类不支持
6.J.U.C包的梳理
java.util.concurrent:提供了并发编程的解决方案
- CAS是java.util.concurrent.atomic包的基础
- AQS是 java.util.concurrent.locks包以及一些常用类比如Semophore,ReentrantLock等类的基础
J.U.C包的分类
- 线程执行器executor
- 锁locks
- 原子变量类atomic
- 并发工具类tools
- 并发集合collections

并发工具类
- 闭锁CountDownLatch
- 栅栏CyclicBarrier
- 信号量Semaphore
- 交换器 Exchanger
CountDownLatch:让主线程等待一组事件发生后继续执行
- 事件是指CountDownLatch里的CountDown()方法

- 所有线程到达栅栏处,可以触发执行另外一个预先设置的线程

Semaphore:控制某个资源可以不饿同时访问的线程个数

Exchanger:两个线程到达同步点后,相互交换数据

BlockingQueue:提供可阻塞的入队和出队操作

主要用于生产者-消费者模式,在多线程场景时生产者线程在队列尾部添加元素,而消费者线程则在队列头部消费元素,通过这种方式能够达到将任务的生产和消费进行隔离的目的。
BlockingQueue
- ArrayBlockingQueue:一个数组结构组成的有界阻塞队列;
- LinkedBlockingQueue:一个由链表结构组成的有界/无界阻塞队列;
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列;
- DealyQueue:一个使用优先级队列实现的无界阻塞队列;
- SynchronousQueue:一个不存储元素的阻塞队列
- LinkedTransferQueue:一个号有链表结构组成的无界阻塞队列
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列;
7.Java的IO机制
BIO、NIO、AIO的主要区别
Block-IO:InputStream和OutputStream,Reader和Writer

NonBlock-IO:构建多路复用的、同步非阻塞的IO操作

NIO的核心
- Channels
- Buffers
- Selectors

NIO - channels
- FileChannel
- TransferTo:把FileChannel重点数据拷贝到灵位一个Channel
- TransferFrom:把灵位一个Channel中的数据拷贝到FileChannel
- 避免了两次用户态和内核态间的上下文切换,即“零拷贝”,效率较高
- DatagramChannel
- SocketChannel
- ServerSocketChannel
NIO-Buffers
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- MappedByteBuffer
NIO-Selector

IO多路复用:调用系统级别的select/poll/epoll

select、poll、epoll的区别
支持一个进程所能打开的最大连接数

FD剧增后带来的IO效率问题

消息传递方式

Asynchronous IO:基于事件和回调机制

AIO如何进一步加工处理结果
- 基于回调:实现CompletionHandler接口,调用是触发回调函数
- 返回Future:通过isDone()查看是否准备好,通过get()等待返回数据
BIO、NIO、AIO对比

十一、Java框架 - Spring
1.Spring家族介绍
如何选择框架?
- 对应的开发者社区是否有名、是否活跃
- 框架的模块是否不断迭代
2.Spring - IOC原理
你了解SpringIOC嘛?
IOC(Incersion of Controol):控制反转
- Spring Core最核心的部分
- 需要先了解依赖注入(Dependency Inversion)
DI举例:设计行李箱
轮子 依赖 底盘 依赖 箱体 依赖 行李箱
上层建筑依赖下层建筑

依赖注入

含义:把底层类作为参数传递给上层类,实现上层对下层的"控制"
IOC、DI、DL的关系

依赖注入的方式
- Setter
- Interface
- Constructor
- Annotation注解
依赖倒置原则、IOC、DI、IOC容器的关系

IOC容器的优势
- 避免在各处使用new来创建类,并且可以做到统一维护
- 创建实例的时候不需要了解其中细节

3.Spring IOC 的应用
你了解Spring的IOC吗?

Spring IOC支持的功能
- 依赖注入
- 依赖检查
- 自动装配
- 支持集合
- 指定初始化方法和销毁方法
- 支持回调方法
Spring IOC容器的核心接口
- BeanFactory
- ApplicationContext
BeanDefinition
- 主要用来描述Bean的定义

BeanDefinitionRegistry
- 提供向IOC容器注册BeanDefinition对象的方法
BeanFactory:Spring框架最核心的接口
- 提供IOC的配置机制
- 包含Bean的各种定义,便于实例化Bean
- 建立Bean之间的依赖关系
- Bean生命周期的控制
BeanFactory体系结构

BeanFactory与ApplicationContext的比较
- Beanfactory是Spring框架的基础设施,面向Spring
- ApplicationContexxt面向使用Spring框架的开发者
ApplicationContext的功能(继承多个接口)
- BeanFactory:能够管理、装配Bean
- ResourcePatternResolver:能够加载资源文件
- MessageSource:能够实现国际化等功能
- ApplicationEventPublisher:能够注册监听器,实现监听机制

4.getBean方法
getBean方法的代码逻辑
- 转换BeanName
- 从缓存中加载实例
- 实例化Bean
- 检测parentBeanfactory
- 初始化依赖的Bean
- 创建Bean
Spring Bean的作用域
- singleton:Spring的默认作用域,容器里拥有唯一的Bean实例
- prototype:针对每个getBean请求,容器都会创建一个Bean实例
- request:会为每个http请求创建一个Bean实例
- session:会为每个session创建一个Bean实例
- globalSession:会为每个全局Http Session创建一个Bean实例该作用域仅对Portlet有效
创建过程

销毁过程
- 若实现了DisposableBean接口,则会调用destroy方法
- 若配置了destroy-method属性,则会调用其配置销毁方法
5.SpringAOP方法
关注点分离:不同的问题交给不同的部分去解决
- 面相切面编程的AOP正是此种技术的体现
- 通用化功能代码的实现,对应的就是所谓的切面(Aspect)
- 业务功能代码和切面代码分开后,架构将变得高内聚低耦合
- 确保功能的完整性:切面最终需要被合并到业务中(Weave)
AOP的三种组织方式
- 编译时织入:需要特殊的Java编译器,入AspectJ
- 类加载时织入:需要特殊的Java编译器,入AspectJ和AspectWerkz
- 运行时织入:Spring采用的方式,通过动态代理的方式,实现简单
AOP的主要名词观念
- Aspect:通用功能的代码实现
- Target:被织入Aspect的对象
- Join Point:可以作为切入点的机会,所有方法都可以作为切入点
- Pointcut:Aspect实际被应用在的Join Point,支持正则
- Advice:类里的方法以及这个方法如何织入到目标方法的方式
- Weaving:Aop的实现过程
Advice的种类
- 前置通知(Before)
- 后置通知(AfterReturning)
- 异常通知(AfterThrowing)
- 最终通知(After)
- 环绕通知(Around)
6.Spring AOP的原理
AOP的实现:JdkProcy和Cglib
- 由AopProxyFactory根据AdvisedSupport对象的配置来决定
- 默认的策略如果目标类是接口,则用JDKProxy来实现,否则用后者
- JDKProxy的核心:InvocationHandler接口和Proxy的类
- Cglib:以继承的方式动态生成目标类的代理
- JDKProxy:通过Java的内部反射机制实现
- Cglib:借助ASM实现
- 反射机制在生成类的过程中比较高效
- ASM在生成类之后的执行过程中比较高效
代理模式:接口+真实实现类+代理类
Spring里的代理模式的实现
- 很是实现类的逻辑包含在了getBean方法里
- getBean方法返回的实际上是Proxy的实例
- Proxy实例是Spring采用JDKProxy或CGLIB动态生成的
Spring事务的相关考点
- ACID
- 隔离级别
- 事务传播
