JVM
谈谈你对Java的理解
- 平台无关性 Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台,上的机器指令。
- GC
- 语言特性
- 面向对象
- 类库
- 异常处理
ClassLoader
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
类从编译到执行过程
- 编译器将Student.java文件编译为Student.class文件
- ClassLoader将字节码转换为JVM中的Class<Student>对象
- JVM利用Class<Student>对象实例化为Student对象
loadClass方法
ClassLoader种类
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录,
- 自定义ClassLoader:用户自定义ClassLoader
自定义ClassLoader的实现 关键方法:
//自定义ClassLoader
package com.example.classloader;
import java.io.*;
public class MyClassLoader extends ClassLoader{
private String path;
private String classLoaderName;
public MyClassLoader(ClassLoader parent, String path, String classLoaderName) {
super(parent);
this.path = path;
this.classLoaderName = classLoaderName;
}
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
/**
* @author Sunjianwang
* @description 用于寻找类文件
* @date 2022-07-07 20:18
* @param name
* @return java.lang.Class<?>
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = new byte[0];
try {
b = loadClassData(name);
} catch (IOException e) {
throw new RuntimeException(e);
}
return defineClass(name,b,0,b.length);
}
/**
* @author Sunjianwang
* @description 加载类文件
* @date 2022-07-07 20:18
* @param name
* @return byte[]
*/
private byte[] loadClassData(String name) throws IOException {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream bout = null;
try {
in = new FileInputStream(name);
bout = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1){
bout.write(i);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
in.close();
bout.close();
}
return bout.toByteArray();
}
}
类加载器的双亲委派机制
JVM中加载类机制采用的是双亲委派模型,顾名思义,在该模型中,子类加载器收到的加载请求,不会先去处理,而是先把请求委派给父类加载器处理,当父类加载器处理不了时再返回给子类加载器加载 关键代码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
类的加载方式
- 隐式加载:new
- 显示加载:loadClass,forName等
loadClass和forName的区别
类的装载过程
- 加载:通过ClassLoader加载Class文件字节码,生成Class对象
- 链接:
- 校验:检查加载的class的正确性和安全性
- 准备:为类变量分配存储空间并设置类变量初始值
- 解析:JVM将常量池内的符号引用转化为直接引用
- 初始化:执行类变量复制和静态代码块
loadClass不会进行类的初始化 forName会初始化类
什么是反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
//获取Class
Class<?> st = Class.forName("com.example.reflect.entity.Student");
//实例化
Student o = (Student) st.getDeclaredConstructor().newInstance();
System.out.println("ClassName is " + st.getName());
o.printName("反射");
//获取私有方法,不能获取继承的方法
Method getMethod = st.getDeclaredMethod("privateMethod", String.class);
//私有方法设置可访问
getMethod.setAccessible(true);
//执行方法,接收返回数据
Object stuMe = getMethod.invoke(o, "测试");
System.out.println(stuMe);
//获取所有public方法,包括实现的方法
Method printName = st.getMethod("printName", String.class);
System.out.println(printName.invoke(o, "获取方法"));
//获取私有变量
Field name = st.getDeclaredField("name");
name.setAccessible(true);
name.set(o, "获取私有变量");
System.out.println(name.get(o));
Java内存模型
内存模型:
- Class Loader(类加载器):依据特定格式,加载.class文件到内存
- Runtime Data Area(运行时数据区)
- Execution Engine(执行引擎):对命令进行解析
- Native Interface(本地库接口):融合不同开发语言的原生库为Java所用
线程私有:程序计数器、虚拟机栈、本地方法栈 线程共享:MetaSpace(类加载信息),Java堆
程序计数器
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需 要执行的字节码指令
- 和线程是一对一的关系即"线程私有”
- 如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)
- 唯一一个不会发生内存泄露的区域
Java虚拟机栈
- Java方法执行的内存模型
- 包含多个栈帧,栈帧存储了局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存储了8种基本数据类型、对象引用(reference类型) 和 returnAddress类型。
执行add(1,2)
public class ByteCodeSample {
public static int add(int a, int b){
int c = 0;
c = a + b;
return c;
}
}
JVM指令
- iconst_0:将int值0压入操作数栈中
- istore_2:将操作数栈中的值pop出,存入局部变量表中第2个位置
- iload_0:将第0个变量压入操作数栈顶
- iload_1:将第1个变量压入操作数栈顶
- iadd:进行运算,将运算结果压入栈顶
- istore_2:将操作数栈中的值pop出,存入局部变量表中第2个位置
- iload_2:将第2个变量压入操作数栈顶
- ireturn:将栈顶数据返回
递归为什么会出现StackOverflowError异常 每调用一次方法,就会在虚拟机栈中创建一个栈帧,当方法调用次数过多,即所创建栈帧超出了虚拟机栈的最大深度,出现StackOverflowError异常
本地方法栈
与虚拟机栈相似,主要作用于标注了native的方法
元空间与永久代的区别
Java堆(Heap)
Java7
Java8 如图所示,Java7堆中的永久代(PermGen Space)在Java8堆中被元空间(Metaspace)取代,永久代和元空间的最大区别就是永久代使用的是JVM中的堆内存,而元空间直接使用物理内存。 整体来看,Java8堆中分为了三大区域:新生区(Young Generation)、养老区(Old Generation)、元空间(Metaspace),其中新生区又分为伊甸园区(Eden Space)、幸存者0区(From区)、幸存者1区(To区),From区和To区并不是固定的,在复制后进行互换,哪块区域为空哪块区域是To区。 堆要分区的唯一理由就是优化GC性能。
内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
堆和栈的区别
- 引用对象、数组时,栈里定义变量保存堆中目标的首地址
- 管理方式:栈自动释放空间,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
元空间、堆、线程独占部分间的联系
public class HelloWorld {
private String name;
public void sayHello(){
System.out.println("Hello:" + name);
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
int a = 1;
HelloWorld hw = new HelloWorld();
hw.setName("Test");
hw.sayHello();
}
}
垃圾回收机制
垃圾标记算法
对象被判定为垃圾的标准 没有被其他对象引用
引用计数算法
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当做垃圾进行收集
优点
- 执行效率高,程序执行受影响较小
缺点
- 无法检测出循环引用的情况,导致内存泄漏
可达性算法
可以作为GC Root的对象
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native)的引用对象
- 活跃线程的引用对象
垃圾收集算法
标记-清除算法(Mark and Sweep)
特点:碎片化
复制算法(Copying)
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制到空闲面
- 将对象面所有对象内存清除
特点:
- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景
标记-整理算法(Compacting)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 整理:移动所有存活的对象,且按照内存地址次序一次排列,然后将末端内存地址以后的内存全部回收
特点:
- 避免内存的不连续行
- 不用设置两块内存互换
- 适用于存活率高的场景,如老年代
分代收集算法(Generational Collector)
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
- 目的:提高JVM垃圾回收效率
GC分类
- Minor GC:年轻代
- Major GC:老年代
- Full GC: 清理整个堆空间,包括年轻代和老年代空间,Full GC比Minor GC慢,但执行频率低
年轻代:尽可能快速地收集生命周期短的对象
Minor Gc
- 几乎任何对象都是在伊甸园区进行创建,此时伊甸园区和幸存区都是空的
- 随着对象的不断创建,伊甸园区的空间逐渐填满
- 这时候触发第一次Minor GC(Young GC),删除未引用的对象,并将存活的对象转移到幸存者0区,然后清空伊甸园区
- 随着对象的创建,伊甸园空间又满了,这时候触发第二次Minor GC(Young GC),删除未引用的对象。将伊甸园区中的对象移到幸存者1区,将幸存者0区中的对象年龄递增后也移到幸存者1区,然后清空伊甸园区和幸存者0区
- 随着对象的创建,伊甸园空间再次满了,这时候触发第三次Minor GC(Young GC),这一次幸存者空间将发生互换。幸存者将被移到幸存者0区,幸存者1区中的对象年龄递增后也移到幸存者0区,然后清空伊甸园区和幸存者1区。
- 随着不断的Minor GC,幸存对象在两个幸存区不断的进行交换存储,年龄也不断递增,直到达到了指定的阈值(这个例子中是8,由JVM参数MaxTenuringThreshold决定),他们将被移动到养老区。除了达到阈值会被移动到养老区,还有以下情况: 1. 对象在触发Minor GC后还是无法放入伊甸园区或者To区,那么将直接放到养老区,若养老区也无法放下,则触发OOM 2. 幸存者区相对年龄的所有对象大小超过空间的一半,那么年龄大于这些对象的对象将被移到养老区,无需等到年龄阈值。
- 随着上述过程的不断出现,当养老区快满时,将触发Major GC(Full GC)进行养老区的内存清理。若养老区执行了GC之后发现依然无法进行对象的保存,就会产生OOM异常。
性能调优参数-Xss 规定每个线程虚拟机栈(堆栈)的大小 -Xms 堆的初始大小 -Xmx 堆能达到的最大值(一般将其和Xms设置为一样大,避免当堆的大小不满足程序要求自动扩容时造成内存抖动,影响程序运行稳定性.) -SurvivorRatio Eden和Survivor的比值,默认8:1 -NewRatio 老年代和年轻代内存大小的比例 -MaxTenuringThreshold 对象从年轻代晋升到老年代经过GC次数的最大阈值
老年代:存放生命周期较长的对象
- 标记-清理算法
- 标记-整理算法
触发Full GC的条件
- 老年代空间不足
- 永久代空间不足(针对JDK7及以前的版本)
- CMS GC时出现promotion failed,concurrent mode failure
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- System.gc()
- 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC
Stop-the-World
- JVM由于要执行GC而停止了应用程序的执行
- 任何一种GC算法中都会发生
- 多数GC优化通过减少Stop-the-World发生的时间来提高程序性能
Safepoint
- 分析过程中对象引用关系不会发生的点
- 产生Safepoint的地方:方法调用;循环跳转;异常跳转等
- 安全点数量要适中
常见的垃圾收集器
- Server:启动较慢,启动后比Client模式更快
- Client:启动较快 使用Java -version查看虚拟机运行在哪种模式
年轻代
Serial收集器(-XX:+UserSerialGC,复制算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
ParNew收集器(-XX:+UserParNewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率步入Serial,在多核下执行才有优势
Parallel Scavenge收集器
- 比起关注用户线程停顿时间,更关注系统的吞吐量(代码的总运行时间/(代码的总运行时间 + 垃圾回收时间))
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
- 在启动策略中加入-XX:+UserAdaptiveSizePolicy讲内存管理调优任务交由虚拟机完成
老年代
Serial Old收集器(-XX:+UserSerialOldGC,标记-整理算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX: +UseParallelOldGC,标记-整理算法)
- 多线程,吞吐量优先
CMS收集器(-XX:+UserConcMarkSweepGC,标记-清除算法)
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预处理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
Garbage First(-XX:+UserG1GC,复制+标记-整理算法)
- 年轻代和老年代收集器
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
面试问题
Object的finalize()方法的作用是否和C++的析构函数作用相同
- 不相同,析构函数的调用是确定的,finalize方法调用是不确定的
- 宣告一个对象死亡要经过两次标记,第一次标记:在可达性分析后,发现没有与GCRoots相连接的引用链,进行第一次标记,同时判断是否执行finalize方法(是否重写了finalize方法或是否被调用过),如果重写了finalize方法且没有被调用过,则会将其添加到F-Queue队列中,然后虚拟机会创建一个finalize线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象的finalize()执行缓慢,极端情况下死循环,那么就会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
- 为对象创造一次逃脱死亡的机会
/**
* finalize()实践
* @author: Sunjianwang
* @version: 1.0
*/
public class Finalization {
public static Finalization finalization;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalize");
finalization = this;
}
public static void main(String[] args) {
finalization = new Finalization();
System.out.println("First Print " + finalization);
//第一次成功拯救自己
finalization = null;
System.gc();
//finalize()执行优先级低,睡一下
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Second Print " + finalization);
System.out.println(finalization == null ? "dead" : "alive");
//第二次拯救失败
finalization = null;
System.gc();
//finalize()执行优先级低,睡一下
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("End Print " + finalization);
System.out.println(finalization == null ? "dead" : "alive");
}
}
//打印结果
First Print entity.Finalization@3b07d329
Finalize
Second Printentity.Finalization@3b07d329
alive
End Printnull
dead
Java中的强引用,软引用,弱引用,虚引用
引用级别:强引用>软引用>弱引用>虚引用
引用类型 | 被回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 垃圾回收时 | 对象缓存 | GC运行后终止 |
虚引用 | Unknown | 标记、哨兵 | Unknown |
类结构图
强引用(Strong Refence)
- 最普遍的引用:
Object obj = new Object()
- 抛出OOM终止程序也不会回收具有强引用的对象
- 通过将对象设置为null来弱化引用,使其被回收
软引用(Soft Reference)
- 对象处在有用但非必须的状态
- 只有当内存空间不足时,GC会回收该引用的对象的内存
- 可以用来实现告诉缓存
String s = new String("强引用");
SoftReference<String> softReference = new SoftReference<String>(s);//软引用
弱引用(Weak Reference)
- 非必须的对象,比软引用更弱一些
- GC时会被回收
- 被回收的概率也不大,因为GC线程优先级比较低
- 适用于引用偶尔被使用且不影响垃圾收集的对象
WeakReference<String> weakReference = new WeakReference<String>(s);//弱引用
虚引用(PhantomReference)
- 不会决定对象的生命周期
- 任何时候都可能呗垃圾回收器回收
- 跟踪对象被垃圾收集器回收的活动,起哨兵作用
- 必须和引用队列
ReferenceQueue
联合使用
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>(s,referenceQueue);//虚引用
引用队列(ReferenceQueue)
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
- 存储关联的且被GC的软引用,弱引用及虚引用
NormalObject.java
/**
* TODO
*
* @author: Sunjianwang
* @version: 1.0
*/
public class NormalObject {
public String name;
public NormalObject(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("试图回收" + name);
}
}
NormalObjectWeakReference.java
/**
* TODO
*
* @author: Sunjianwang
* @version: 1.0
*/
public class NormalObjectWeakReference extends WeakReference<NormalObject> {
public String name;
public NormalObjectWeakReference(NormalObject referent, ReferenceQueue<NormalObject> objectReferenceQueue) {
super(referent,objectReferenceQueue);
this.name = referent.name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("NormalObjectWeakReference 回收" + name);
}
}
ReferenceQueueTest.java
/**
* TODO
*
* @author: Sunjianwang
* @version: 1.0
*/
public class ReferenceQueueTest {
private static ReferenceQueue<NormalObject> referenceQueue = new ReferenceQueue<NormalObject>();
private static void checkQueue(){
Reference<NormalObject> ref = null;
while ((ref = (Reference<NormalObject>) referenceQueue.poll()) != null){
if (ref != null){
System.out.println("在队列中:" + ((NormalObjectWeakReference)ref).name);
System.out.println("引用对象:" + ref.get());
}
}
}
public static void main(String[] args) {
ArrayList<WeakReference<NormalObject>> weakList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
weakList.add(new NormalObjectWeakReference(new NormalObject("Weak" + i),referenceQueue));
System.out.println("创建对象实例:" + weakList.get(i));
}
System.out.println("第一次:");
checkQueue();
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("第二次:");
checkQueue();
}
}
//打印结果
创建对象实例:reference.NormalObjectWeakReference@74a14482
创建对象实例:reference.NormalObjectWeakReference@1540e19d
创建对象实例:reference.NormalObjectWeakReference@677327b6
第一次:
试图回收Weak2
试图回收Weak1
试图回收Weak0
第二次:
在队列中:Weak0
引用对象:null
在队列中:Weak2
引用对象:null
在队列中:Weak1
引用对象:null
多线程及并发
进程和线程
进程和线程的由来
- 串行:初期的计算机智能串行执行任务,并且需要长时间等待用户输入
- 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍然无法并发执行
- 进程:进程独占内存空间,保存各自运行状态,相互间不干扰且可以互相切换,为并发处理任务提供了可能
- 共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行
进程和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位
- 所有与进程相关的资源,都被记录在PCB中
- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
- 线程只由堆栈寄存器、程序计数器和TCB组成
总结:
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程程序健壮(一个进程崩溃,在保护模式下不会影响其他进程,但是一个线程崩溃那整个进程都会挂掉)
- 进程的切换比线程的切换开销大
Java中进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个程序会产生一个进程,进程包含至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
Java程序天生就是多线程程序
/**
* 展示Java多线程例子
*
* @author: Sunjianwang
* @version: 1.0
*/
public class MultiThread {
public static void main(String[] args) {
// 获取Java线程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
for (ThreadInfo threadInfo:
threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
System.out.println("Test");
}
}
//输出结果
[6] Monitor Ctrl-Break //监听线程转储或“线程堆栈跟踪”的线程
[5] Attach Listener //负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //在垃圾收集前,调用对象 finalize 方法的线程
[2] Reference Handler //用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收的线程
[1] main //main 线程,程序入口
Thread中的start和run方法的区别
- 调用start方法会创建一个新的子线程并启动
- 调用run方法只是Thread方法的一个普通调用,依旧是在当前线程下执行
Thread和Runnable的关系
- Thread是实现了Runnable接口的类,使得run支持多线程
- 因类的单一继承原则,推荐多使用Runnable接口
如何实现处理线程的返回值
主线程等待法
/**
* 处理线程返回值之阻塞主线程以获取线程返回值
*
* @author: Sunjianwang
* @version: 1.0
*/
public class CycleWait implements Runnable{
private String value;
public void run(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
value = "我有值啦!!!";
}
public static void main(String[] args) throws InterruptedException {
CycleWait cycleWait = new CycleWait();
Thread t = new Thread(cycleWait);
t.start();
//循环法阻塞主线程等待子线程执行结束
while (cycleWait.value == null){
Thread.sleep(100);
}
System.out.println("value = " + cycleWait.value);
}
}
使用Thread类的join()阻塞当前线程以等待子线程处理完毕
/**
* 处理线程返回值之阻塞主线程以获取线程返回值
*
* @author: Sunjianwang
* @version: 1.0
*/
public class CycleWait implements Runnable{
private String value;
public void run(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
value = "我有值啦!!!";
}
public static void main(String[] args) throws InterruptedException {
CycleWait cycleWait = new CycleWait();
Thread t = new Thread(cycleWait);
t.start();
//Join()方法,阻塞当前线程,等待当前线程执行结束
t.join();
System.out.println("value = " + cycleWait.value);
}
}
通过Callable接口实现:通过FutureTask或者线程池获取
MyCallable.java
/**
* Callable:可以获取线程返回值
*
* @author: Sunjianwang
* @version: 1.0
*/
public class MyCallable implements Callable<String> {
private String name;
public MyCallable(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
System.out.println(name + "准备好执行...");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
System.out.println(name + "执行完成...");
return name + "执行完成,这是返回值";
}
}
FutureTaskDemo.java
/**
* 使用FutureTask获取线程返回值
*
* @author: Sunjianwang
* @version: 1.0
*/
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyCallable("线程0"));
new Thread(futureTask).start();
if (!futureTask.isDone()){
System.out.println("任务还没有执行完成!");
}
System.out.println("返回值:" + futureTask.get());
System.out.println("程序继续执行...");
}
}
ThreadPoolDemo.java
/**
* 使用线程池获取线程返回值
*
* @author: Sunjianwang
* @version: 1.0
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> submit = executorService.submit(new MyCallable("线程1"));
Future<String> submit1 = executorService.submit(new MyCallable("线程2"));
if (!submit.isDone()){
System.out.println("线程1还没有执行完成");
}
if (!submit1.isDone()){
System.out.println("线程2还没有执行完成");
}
try {
System.out.println(submit.get());
System.out.println(submit1.get());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
} finally {
executorService.shutdown();
}
System.out.println("主线程继续执行...");
}
}
线程的状态
- 新建(New):创建后尚未启动的线程的状态
- 运行(Runnable):包含Running和Ready
- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式的被唤醒.使线程进入Waiting状态的方法:
- 没有设置Timeout参数的Object.wait()方法
- 没有设置TimeOut参数的Thread.join()方法
- LockSupport.park()方法
- 限期等待(Timed Waiting):在一定时间后系统会自动唤醒.使线程进入Timed Waiting的方法:
- Thread.sleep()方法
- 设置了Timeout参数的Object.wait()方法
- 设置了TimeOut参数的Thread.join()方法
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
- 阻塞(Blocked):等待获取排它锁
- 结束(Terminated):已终止线程的状态,线程已经结束执行
Sleep和Wait的区别
基本的区别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能在synchronized方法或synchronized块中使用
本质的区别
- Thread.sleep()只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
/**
* sleep和wait方法区别
*
* @author: Sunjianwang
* @version: 1.0
*/
public class SleepAndWaitDemo {
public static void main(String[] args) {
final Object o = new Object();
//线程1
new Thread(() -> {
System.out.println("线程1开始执行,准备获取锁...");
synchronized (o){
System.out.println("线程1获取到锁,开始执行任务...");
try {
Thread.sleep(20);
System.out.println("线程1执行wait方法,释放CPU和锁资源");
o.wait(2000);
System.out.println("线程1执行完毕,释放锁");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
//阻塞一下,让线程1获取到资源
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
System.out.println("线程2开始执行,准备获取锁...");
synchronized (o){
System.out.println("线程2获取到锁,开始执行任务...");
try {
System.out.println("线程2执行sleep方法,不会释放锁");
Thread.sleep(1000);
System.out.println("线程2执行完毕,释放锁");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
//打印结果
线程1开始执行,准备获取锁...
线程1获取到锁,开始执行任务...
线程2开始执行,准备获取锁...
线程1执行wait方法,释放CPU和锁资源
线程2获取到锁,开始执行任务...
线程2执行sleep方法,不会释放锁
线程2执行完毕,释放锁
线程1执行完毕,释放锁
notify和notifyAll的区别
锁池 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中. 等待池 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池. notify和notifyAll的区别
- notifyAll会让所有处于等待池的线程全部进入到锁池去竞争获取锁的机会
- notify只会随机选取一个处于等待池的线程进入到锁池去竞争获取锁的机会
synchronized
互斥锁的特性
- 互斥性:即在同一时间只允许一个线程持有某个对象锁 ,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值) ,否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
对象锁和类锁的总结
- 有线程访问对象的同步代码块时 ,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
- 同一个类的不同对象的对象锁互不干扰;
- 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
- 类锁和对象锁互不干扰。
示例 SyncThread.java
/**
* 多线程实例
*
* @author Sunjianwang
* @version 1.0
*/
public class SyncThread implements Runnable{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")){
sync();
} else if (threadName.startsWith("B")) {
syncObjectBlock();
}else if (threadName.startsWith("C")){
syncObjectMethod();
}else if (threadName.startsWith("D")){
syncClassBlock();
}else if (threadName.startsWith("E")){
syncClassMethod();
}
}
/**
* 异步方法
* @author 孙建旺
*/
private void sync(){
try {
System.out.println(Thread.currentThread().getName() + "异步线程在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "异步线程在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "开始");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* 同步对象锁-同步代码块
* @author 孙建旺
*/
private void syncObjectBlock(){
System.out.println(Thread.currentThread().getName() + "_当前执行方法syncObjectBlock,时间:" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this){
try {
System.out.println(Thread.currentThread().getName() + "同步代码块在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "同步代码块在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 同步对象锁-同步方法
* @author 孙建旺
*/
private synchronized void syncObjectMethod(){
System.out.println(Thread.currentThread().getName() + "_当前执行方法syncObjectMethod,时间:" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this){
try {
System.out.println(Thread.currentThread().getName() + "同步方法在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "同步方法在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 同步类锁-同步代码块
* @author 孙建旺
*/
private void syncClassBlock(){
System.out.println(Thread.currentThread().getName() + "_当前执行方法syncClassBlock,时间:" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class){
try {
System.out.println(Thread.currentThread().getName() + "同步类在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "同步类在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 同步类锁-同步方法
* @author 孙建旺
*/
private synchronized static void syncClassMethod(){
System.out.println(Thread.currentThread().getName() + "_当前执行方法syncClassMethod,时间:" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "同步方法在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "开始");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "同步方法在" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
SyncDemo.java
/**
* Demo
*
* @author Sunjianwang
* @version 1.0
*/
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();
}
}
//打印结果
E_thread1_当前执行方法syncClassMethod,时间:10:36:29
B_thread2_当前执行方法syncObjectBlock,时间:10:36:29
D_thread2_当前执行方法syncClassBlock,时间:10:36:29
C_thread1_当前执行方法syncObjectMethod,时间:10:36:29
B_thread1_当前执行方法syncObjectBlock,时间:10:36:29
A_thread1异步线程在10:36:29开始
C_thread1同步方法在10:36:29开始
A_thread2异步线程在10:36:29开始
D_thread1_当前执行方法syncClassBlock,时间:10:36:29
E_thread1同步方法在10:36:29开始
C_thread1同步方法在10:36:30结束
E_thread1同步方法在10:36:30结束
A_thread1异步线程在10:36:30开始
A_thread2异步线程在10:36:30开始
B_thread1同步代码块在10:36:30开始
D_thread1同步类在10:36:30开始
D_thread1同步类在10:36:31结束
B_thread1同步代码块在10:36:31结束
D_thread2同步类在10:36:31开始
B_thread2同步代码块在10:36:31开始
D_thread2同步类在10:36:32结束
B_thread2同步代码块在10:36:32结束
E_thread2_当前执行方法syncClassMethod,时间:10:36:32
C_thread2_当前执行方法syncObjectMethod,时间:10:36:32
E_thread2同步方法在10:36:32开始
C_thread2同步方法在10:36:32开始
E_thread2同步方法在10:36:33结束
C_thread2同步方法在10:36:33结束
Spring
Spring IOC
IOC(Inversion of Control):让程序员不在关注怎么去创建对象,而是关注与对象创建之后的操作,把对象的创建、初始化、销毁等工作交给spring容器来做
依赖注入方式:
- Setter
- Interface
- Constructor
- Annotation
Spring事务传播机制
- REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
- SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
- MANDATORY: 当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER:不使用事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)