文章目录
Java类加载与反射
类加载机制
Java 虚拟机规范中并没有强制约束一个类在什么时候开始被加载,而是交给虚拟机自己去实现(HotSpot 是按需加载,用到该类的时候就加载)。
不管 Java 程序多么复杂,启动了多少线程,它们都处于该 Java 虚拟机进程里。
出现以下情况,JVM 进程将被终止:
- 程序运行到最后,正常结束。
- 程序运行到使用 System.exit() 或 Runtime.getRuntime().exit() 代码结束程序。
- 程序执行过程中遇到未捕获的异常或错误而结束。
- 程序所在平台强制结束 JVM 进程。
一个类从加载到 JVM 内存,到从 JVM 内存中卸载,整个生命周期会经历 7 个阶段:加载、验证、准备、解析、初始化、使用、卸载。
[外链图片转存中…(img-OVkBO73n-1647331952815)]
类加载的生命周期
1. 加载
指的是将 classpath、jar 包、网络、某个磁盘位置下的类的 class 二进制字节流读进来(将类的 .class 文件读入内存),并且为之创建一个 java.lang.Class 对象,放入元空间,即当程序使用任何类时,系统都会为之建一个 java.lang.Class 对象。这个阶段程序员可以干预,自定义类加载器来实现类的加载。
2. 连接
接着进入连接阶段,负责把类的二进制数据合并到 JRE 中。
连接阶段又可分为三个阶段:
- 验证:检验被加载的类是否有正确的内部结构,并与其他类协调一致。
- 准备:负责为类变量(静态变量)分配内存并设置默认初始值 0 / null / false ;为常量赋值(正式值,等号后的常数值)。
- 解析:JVM 将类的二进制数据中的符号引用翻译成直接引用。
JVM 的类加载机制主要有三种:
- 全盘负责:当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显式地使用另一个类加载器载入。
- 父类委托:先让父类加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载。
- 缓存机制:该机制会保证所有被加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存中搜寻该 Class ,只有当缓存中不存在该 Class 对象时,系统才会读取该类的二进制数据,并将其转换成 Class 对象,再存入 cache。这就是为什么修改 Class 后,程序必须重新启动 JVM 修改才会生效。
3. 初始化
在类的初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码。
在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,才真正初始化类变量和其他资源。
什么时候触发?当new 一个类的对象、访问 / 修改一个类的静态属性、调用一个类的静态方法、用反射 API 对一个类进行调用时。new 对象时,才会执行变量、代码块、构造器。
注意初始化子类时会先初始化父类:父类静态变量 –> 父类静态代码块 –> 子类静态变量 –> 子类静态代码块 –> 父类变量 –> 父类常规代码块 –> 父类构造器 –> 子类变量 –> 子类常规代码块 –> 子类构造器。
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过==加载、连接、初始化==三个步骤来对该类进行初始化,不出意外的话,JVM 会连续完成这三个步骤,所有也把这三个步骤统称为类加载或类初始化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hiDR6zmi-1647331952816)(https://s2.loli.net/2022/02/11/vkm3NESXjPfa8T2.png)]
在初始化阶段,JVM 主要就是对静态属性进行初始化。
在Java类中对静态属性指定初始值的方式:
- 声明静态属性时指定初始值。
- 使用静态代码块为静态属性指定初始值。
JVM 初始化步骤:
- 该类还未被加载和连接,则程序先加载并连接该类。
- 该类的直接父类还未被初始化,则先初始化其直接父类。
- 若类中有初始化语句,则系统依次执行初始化语句。
4. 使用
使用该类。
5. 卸载
卸载的条件很苛刻:
- 给类的所有实例已经被 GC,即 JVM 中不存在该类的任何实例;
- 加载该类的 ClassLoader 已经被 GC;
- 该类的 java.lang.Class 对象没有在任何地方被引用,即没有在任何地方通过反射访问该类
类加载器 ClassLoader
类加载器就是一个程序(一段 C++ / Java 代码 ),负责将 .class 文件(磁盘或网络上)加载到内存中,并为之生成对应的 java.lang.Class 对象,这个动作可以自定义实现。
JVM 启动时,会形成由三个类加载器组成的初始化类加载器层次结构:三者不是继承关系
-
Bootstrap ClassLoader:根类加载器/启动类加载器/父加载器,使用 C++ 语言实现,是虚拟机自身的一部分,加载核心类库(java.lang.* 等)。
其他所有加载器都是 Java 语言实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。
-
Extension ClassLoader:扩展类加载器/母加载器,加载 jre/lib/ext 目录下的扩展 jar 包。
-
System/Application ClassLoader:系统类加载器/应用类加载器,加载应用程序的主函数类。和 Extension ClassLoader 是同级关系,都是 URLClassLoader 的子类,都是 ClassLoader 的间接子类。
-
除了上面的三个,程序员也可以自定义类加载器,指定加载某个路径的类。
通过使用不同的类加载器可以从不同的来源加载类的二进制数据:
- 从本地文件系统加载 class 文件,是绝大部分示例程序的类加载方式。
- 从 JAR 包中加载 class 文件,JDBC 编程时用到的数据库驱动类就是放在 JAR 文件中的,JVM 可以从 JAR 文件中直接加载该 class 文件。
- 通过网络加载 class 文件。
- 把一个 Java 源文件动态编译,并执行加载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0n52vh7-1647331952816)(https://s2.loli.net/2022/02/11/LRQoZCAbGTXS4if.png)]
双亲委派机制的执行原理:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载该类,而是把这个请求委派给上层加载器去完成,每一层都是如此,因此,所有的加载请求最终都会传送到最顶层的 BootStrap ClassLoader 中,只有当上一层类加载器反馈自己无法加载该请求时(其搜索范围中没有该类),下一个类加载器才会尝试自己去加载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msXduIKh-1647331952817)(https://s2.loli.net/2022/02/12/HYuTdl27ZjbgEeW.png)]
作用(好处):
- 避免重复加载,保证类的唯一性;
- 避免核心类库被修改,确保类加载的安全性;
反射机制
反射指间接操作目标对象,通过反射机制操作(读 / 写)字节码 .class 文件。
反射机制:动态获取类的信息,动态调用对象的方法,这些功能来自于 Java 的反射机制。
反射机制主要提供功能:
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的属性和方法;
- 在运行时判断任意一个对象所属的类;
- 在运行时调用任意一个对象的方法;
- 生成动态代理。
反射机制优缺点:
- 优点:增加程序灵活性,在不改变程序的基础上,运行阶段实例化不同类型的对象。
- 缺点:打破了封装,从而可以访问类的私有属性。
反射机制的相关类:
类 | 代表 | 对应 |
---|---|---|
java.lang.Class | 字节码文件 | 整个类 |
java.lang.reflect.Method | 方法字节码 | 类中的方法 |
java.lang.reflect.Constructor | 构造方法字节码 | 类中的构造方法 |
java.lang.reflect.Field | 属性字节码 | 类中的成员变量 |
从Class中获取信息:
类 | 说明 |
---|---|
AccessibleObject类 | 该类是域(field)对象、方法(method)对象、构造函数(constructor)对象的基础类,提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力 |
Array类 | 提供动态地生成和访问JAVA数组的方法 |
Modifier类 | 提供了 static 方法和常量,对类和成员访问修饰符进行解码。 |
Proxy类 | 提供动态地生成代理类和类实例的静态方法 |
使用反射创建对象
Java 中获得 Class 对象通常有三种方式:
- 使用 Class 类的 forName() 静态方法,传入字符串参数,其值是某个类的全限定类名。
- 调用某个类的 class 属性获取该类对应的 Class 对象,如:Person.class 会返回Person 类对应的 Class 对象。
- 调用某个对象的 getClass() 方法,该方法是 java.lang.Object 类中的,所有 Java 对象都可以调用该方法,该方法返回该对象所属类对应的 Class 对象。
[外链图片转存中…(img-d8ocBCnm-1647331952818)]
使用反射访问属性Field、调用方法Method
获取类的属性:
public Field getField(String name) | 返回Field对象,反映此Class对象所表示的类或接口的指定公共(public)成员字段 |
public Field[] getFields() | 返回包含某些Fields对象的数组,反映此Class对象所表示的类或接口的**所有可访问公共(public)**字段 |
public Field getDeclaredField(String name) | 返回Field对象,反映此Class对象所表示的类或接口的**指定已声明(包括公共、私有、保护)**字段 |
public Field[] getDeclaredFields() | 返回Field对象的一个数组,反映此 Class 对象所表示的类或接口的**所声明的所有(包括公共、私有、保护)**字段 |
调用类的方法:
public MethodgetMethod(String name,Class<?>… parameterTypes) | 返回一个 Method 对象,反映此 Class 对象所表示的类或接口的指定公共成员方法 |
public Method[] getMethods() | 返回一个包含某些 Method 对象的数组,反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member方法 |
public MethodgetDeclaredMethod(Stringname,Class<?>… parameterTypes) | 返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法 |
public Method[] getDeclaredMethods() | 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法 |
补充
关于资源绑定器(java.util.ResourceBundle):
- 属性配置文件是文件扩展名为 properties 的文件
- 属性配置文件必须放在类路径(src 文件)下
- 作用是获取属性配置文件中的内容
- 使用资源绑定器读取配置文件时,路径为资源文件编译后的 classes 路径,路径中不能添加文件扩展名 properties
文件的路径问题:
-
获取类路径下文件的绝对路径:
String path=Thread.currentThread().getContextClassLoader().getResourse(String path).getPath();
- Thread.currentThread():属于静态方法,返回当前线程对象。
- Thread.getContextClassLoader():返回当前线程的类加载器对象,避开双亲委派机制进行类加载。
- classLoader().getResourse(String name):只接收文件的相对路径,返回URL对象,String name是资源文件编译后的classes路径(文件的相对路径,相对路径是相对于根目录而言的)。
- url.getPath():返回文件的绝对路径,遇到路径中存在空格或&%等特殊字符会自动编码。
- url.toURl().getPath():不使用自动编码。
异常处理机制
概念
异常是指程序执行过程中可能出现的非正常情况,在 Java 中以类和对象的形式存在。Java 异常机制提高程序健壮性和容错性。
-
异常上抛机制
指发生异常时,创建异常对象并将其上抛给调用者,需要在方法声明位置上使用 throws 关键字。
-
异常捕捉机制
指发生异常时,创建异常对象并将其捕捉,需要在方法体内使用 try {} catch {} 语句块。
-
两者比较
异常上抛不允许后续代码执行,异常捕捉允许。
注意:如果一直上抛异常到 JVM 虚拟机,那么 JVM 会终止程序执行(main 方法不建议上抛)。
异常分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CkBQisz8-1647331952819)(https://s2.loli.net/2022/02/25/QYgochVlybBzrMs.jpg)]
- Error :不可处理的错误,直接退出 JVM 虚拟机
- Exception :可以处理的异常
- 编译时异常:编写程序阶段必须预先处理的异常,否则编译不通过
- 运行时异常:编写程序阶段即使不预先处理也能通过编译的异常
编译时异常 VS 运行时异常:
- 相同点:都发生在运行阶段,异常机制实际上就是 new 异常对象,然后将其抛出,而 new 对象这一操作只能发生在运行阶段
- 不同点:编译时异常发生概率大,需要预处理;运行时异常发生概率小,不需要预处理。
异常对象常用方法
- exception.getMessage():获取异常简单的描述信息
- exception.printStackTrace():打印异常追踪的堆栈信息
相关关键字
- throw:表示在方法体中手动抛出一个异常对象
- throws:在方法声明中,表示此方法在被调用时必须处理异常
- try :不能单独出现,需要和 catch 或 finally 联合使用
- catch :内容可以是确切类型,也可以是其父类,可以写多个(遵循异常类型从小到大的顺序),编写时建议一个异常对应一个 catch 语句块(利于调试)
- finally :finally 语句块中的语句必定执行,通常用于释放或关闭资源
常见异常
NullPointerException:空指针异常
ClassCastException:类型转换异常
ArrayIndexOutOfBoundException:数组下标越界异常
NumberFormatException:数字格式化异常
ConcurrentModificationException:并发修改异常
InstantiationException:实例化异常
InterruptedException:中断异常
NotSerializableException:不可序列化异常
集合
概念
集合是存储任意数量的具有共同属性的对象的容器,集合存储的是 Java 对象的内存地址 / 引用,不能直接存储 Java 对象或基本数据类型,集合类与集合接口都在 java.util 包下。
分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcIOwbmx-1647331952820)(https://s2.loli.net/2022/02/25/dM7kp9LPg2mYf4y.jpg)]
Iterator 接口(迭代器)
Collection 、Map 系列主要用于盛装对象(容器),而 Iterator 则主要用于遍历(迭代访问)Collection 集合中的元素。Iterator 对象称为迭代器。
-
方法
boolean hasNext():如果下一个元素可以迭代,则返回 true
Object next():返回迭代中的下一个元素
void remove():删除迭代器指向的当前对象
-
for…each 循环遍历集合元素
这种方法比 for 循环更加便捷,但是当使用 for…each 循环迭代集合元素时,该集合不能被改变(如删除操作),否则将引发下面的异常(ConcurrentModificationException)。
-
迭代器和 ConcurrentModificationException 并发修改异常
-
该异常的触发条件是当前集合状态和迭代器快照(原集合状态)不同
-
解决方案:
在遍历集合的过程中使用迭代器的 remove() 方法,不是集合对象的 remove() 方法删除元素
获取迭代器后禁止改变集合结构(禁止对集合进行增删改操作)
集合结构发生改变就必须重新获取迭代器
-
-
remove()
删除的是迭代器指向的当前元素,同时删除迭代器快照和集合中的元素,删除元素时会自动更新迭代器与集合
Collection 接口
Collection 接口是 List、Set、Queue 接口的父接口,该接口里定义的方法(如下)可以用于操作 List、Set、Queue 集合。
常用方法
boolean add(Object obj):向集合中添加元素 obj
boolean remove(Object obj):删除集合中的某个元素 obj,底层调用 equals() 方法进行比对
boolean contain(Object obj):判断集合中是否存在元素 obj,底层调用 equals() 方法进行比对
boolean isEmpty():判空
int size():获取集合中元素个数
void clear():清空
Object[] toArray():把集合转换成数组
Iterator<E> iterator():获取集合的迭代器对象
Collection VS Collections:Collection 是接口,Collections 是工具类,提供了大量方法对集合(List、Set、Map)元素进行排序、查询、修改等操作,还提供了将集合对象设置不可变、同步控制等方法。
子接口 List 集合
List 集合存储元素的特点:有序(存储和读取顺序一致)、可重复、有下标
List 集合方法
void add(int index,Object element):指定位置添加元素
Object get(int index):获取指定位置元素
Object remove(int index):删除指定位置元素并返回该元素
Object set(int index,Object element):设置指定位置为某元素,并将原元素返回
int indexOf(Object obj):获取指定对象在集合中第一次出现的索引
int lastIndextOf(Object obj):获取指定对象在集合中最后一次出现的索引
List 集合分类
-
Vector 集合:数组,查询效率高,增删效率低,线程安全(方法都带synchronized)
创建方法:
- new Vector():初始化容量为10,每次扩容为原来的2倍
- new Vector(int initialCapacity):指定初始化容量
- new Vector(int initialCapacity,int capacityIncrement):指定初始化容量和增量
- new Vector(Collection coll):将 Collection 集合转换成 Vector 集合
-
ArrayList 集合:数组,查询效率高,增删效率低,非线程安全
创建方法:
- new ArrayList():初始化容量为10,每次扩容为原来的1.5倍
- new ArrayList(int initialCapacity):指定初始化容量
- new ArrayList(Collection coll):将 Collection 集合转换成 ArrayList 集合
注意:数组扩容涉及到数组拷贝,应该尽可能减少扩容操作,要预估进行合适的初始化容量设置。
源码:底层只有在调用 add();时,才会固定数组长度 10;在调用构造器时(new ArrayList();)底层数组为空{}。
-
LinkedList 集合:双向链表,查询效率低,增删效率高,非线程安全
创建方法:
- new LinkedList():创建一个空的集合(没有初始化容量和扩容机制)
- new LinkedList(Collection coll):将 Collection 集合转换成 LinkedList 集合
**注意:**LinkedList 增删效率未必高于 ArrayList (头部、中间、尾部)。
源码:add();底层将元素封装为一个 Node 对象。
List 集合遍历
-
迭代器遍历
Iterator<String> iterator=list.iterator(); while(iterator.hasNext()) System.out.println(iterator.next());
-
for 循环下标遍历
for(int i=0;i<list.length;i++) System.out.println(list[i]);
-
for-each 循环遍历
for(String str:list) System.out.println(str);
子接口 Set 集合
Set 集合存储元素的特点:无序(存储和读取顺序不一定相同)、不可重复(元素唯一性)、无下标
Set 集合分类
-
HashSet 集合:哈希表(基于 HashMap 实现,HashSet 的值存放在 HashMap的 key 上,HashMap 的 key 是唯一的),非线程安全、效率高。
HashSet 按 Hash 算法存储集合中的元素,具有很好的存取和查找性能。
部分源码:
private static final Object PRESENT=new Object(); private transient HashMap<E,Object> map; public HashSet(){ map=new HashMap<>();//底层new了个HashMap! } public boolean add(E e){ return map.put(e,PRESENT)==null;//调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 }
创建方法:
- new HashSet():初始化容量为16,每次扩容为原来的2倍,扩容因子为0.75
- new HashSet(int initialCapacity):指定初始化容量
- new HashSet(int initialCapacity,float loadFactor):指定初始化容量和扩容因子
- new HashSet(Collection coll):将 Collection 集合转换成 HashSet 集合
向 HashSet 存元素 add():不仅要比较 hashCode,还要结合 equals(),一定要重写这两个方法!(String 和 Integer 底层已经重写这两个方法了)
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象(集合存的都是引用数据类型)的 hashCode() 方法来得到该对象的 hashCode 值(hashCode 是 JDK 根据对象的地址或字符串或数字算出来的 int 型的数值),然后根据这个 hashCode (再结合一定算法:)决定该对象在 HashSet 中的存储位置。查找集合中是否有相同的哈希值,如果没有,则将元素存入集合;如果有相同的哈希值,则后来的对象还要调用 equals(),如果返回 true 则判定元素重复,不存入集合;如果返回 false 则存入集合(前 7 后 8 )。
注意:若两个对象相等,则 hashCode 一定相同,equals() 方法返回 true;但是 hashCode 相同,两个对象不一定相等;两个不同的对象 hashCode 就可能相同。
补充:哈希表
哈希表是一维数组和单链表 / 红黑树的结合体(拉链式)
- 当哈希表中的单向链表上的结点 > 8,单向链表转换成红黑树
- 当哈希表中的红黑树上的结点 < 6,红黑树转换成单向链表
- 结合了数组的高效查询和链表的高效增删(都只发生在某一条链表上)
- 哈希碰撞:两元素的哈希值相等(存储位置相等)。
-
LinkedHashSet 集合:链表 + 哈希表,可以按照输入顺序,输出(插入和遍历顺序一致),元素唯一,继承 HashSet。本质上就是在 HashSet 的基础上增加了一个链表来记录插入顺序。
-
TreeSet 集合:二叉树(中序遍历读取数据),非线程安全、效率高,存储元素自动排序(可排序集合,升序)
创建方法:
- new TreeSet():创建一个空的集合
- new TreeSet(Comparator comp):创建指定比较规则的集合
- new TreeSet(Collection coll):将 Collection 集合转换成 TreeSet 集合
==为什么底层可以自动排序?==由内部比较器 / 外部比较器做到。
- 比较 int 型数据:将比较的数据做差,返回一个 int 型数据,按照 =0、>0、<0 比较
- 比较 String 类型数据:源码,String 类实现了 Comparable 接口,并且重写了接口中的方法 compareTo 方法。
Set 集合遍历
-
迭代器遍历
Iterator<String> iterator=set.iterator(); while(iterator.hasNext()) System.out.println(iterator.next());
-
for-each 遍历
for(String str:set) System.out.println(str);
如何选择创建哪种 Collection 集合?
[外链图片转存中…(img-j6u0wB1o-1647331952820)]
Map 接口
Map 集合以键值对(key-value)的形式存储数据。key 和 value 都是引用数据类型、都是存储对象的内存地址 / 引用,key 起主导作用。
Map 集合存储元素的特点:无序(存储和读取顺序不一定相同)、不可重复(数据元素唯一性)
常用方法
V put(K key,V value):添加键值对
V get(Object key):通过 key 获取 value
V remove(Object key):通过 key 删除键值对
int size():获取键值对数量
void clear():清空
boolean isEmpty():判空
boolean containsKey(Object key):判断是否包含某个 key
boolean containsValue(Object value):判断是否包含某个 value
Set<K> keySet():获取集合中所有的 key,返回一个 Set 集合
Collection<V> values():获取集合中所有的 value,返回一个 Collection 集合
Set<Map.Entry<K,V>> entrySet():将 Map 集合转换成 Set 集合,Set 集合中的元素类型是 Map.Entry<K,V>
Map 集合分类
-
HashMap 集合:哈希表,非线程安全,效率高
添加元素 put(key,value):不可重复,如果 key / value 相同,新的 value 会覆盖掉旧的 value。
创建方法:
- new HashMap():初始容量为16,扩容为原来的2倍,扩容因子为0.75
- new HashMap(int initialCapacity):指定初始容量,必须是2的倍数(保证散列分布均匀、提高存取效率)
- new HashMap(int initialCapacity,float loadFactor):指定初始容量和扩容因子
注意:允许 key 为 null,但是只能存在一个。
-
HashTable 集合:哈希表,线程安全(所有方法带 synchronized),效率低
创建方法:
- new HashTable():初始容量为11,扩容为原来的2倍+1,扩容因子为0.75
- new HashTable(int initialCapacity):指定初始容量
- new HashTable(int initialCapacity,float loadFactor):指定初始容量和扩容因子
**注意:**不允许 key、value 为 null,否则报空指针异常。
-
TreeMap 集合:红黑树
创建方法:
- new TreeMap():创建一个空的集合,无初始化容量和扩容机制
注意:TreeMap 集合的 key 部分按大小自动排序(可排序集合)。
小结
数组 VS 集合
- 数组长度不可变,可以存储基本数据类型和引用数据类型,只能存储同一类型的元素
- 集合长度可变,只能存储引用数据类型,可以存储不同类型的元素
底层数据结构
实现类 | 底层数据结构 | 特点 |
---|---|---|
Vector | 数组 | 有序,可重复,线程安全 |
ArrayList | 数组 | 有序,可重复,非线程安全 |
LinkedList | 双链表 | 有序,可重复,非线程安全 |
HashSet | 哈希表 | 无序,不可重复,非线程安全 |
TreeSet | 二叉树 | 无序,不可重复,非线程安全,自动排序 |
HashMap | 哈希表 | 无序,不可重复,非线程安全 |
HashTable | 哈希表 | 无序,不可重复,线程安全 |
TreeMap | 红黑树 | 无序,不可重复,非线程安全,自动排序 |
补充:ConcurrentHashMap
JDK 1.5 中,有了 concurrent 包,ConcurrentHashMap 是线程安全的,但是跟 HashTable 不同,HashTable 是在所有方法上加了 synchronized 实现线程安全,而 ConcurrentHashMap 则是将整个 Map 分为 N 个 Segment (类似 HashTable),因此可以提供相同的线程安全。
泛型机制
本质是参数化类型,即在类、接口、方法的定义上指定元素的数据类型(只能是引用数据类型),作用是在编译阶段(泛型机制只在程序编译阶段起作用)检查元素类型是否匹配,避免程序在运行阶段出现过多错误。
优缺点:
- 同一集合中的元素类型,不需要进行大量的向下转型
- 元素类型缺乏多样性
自定义泛型:
List<E> list = new ArrayList<>(); Element
List<T> list = new ArrayList<>(); Type
多线程
多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。
概念
-
程序:静态的,是指令的有序集合
-
进程(process):动态,是程序的一次动态执行过程,是资源分配的基本单位,每个进程都有独立的 cpu 时间、code、data(即使是同一程序产生多个进程),导致浪费内存,开销较大(Ctrl+Alt+Del—启动任务管理器查看所有进程)
进程特征:
-
独立性:系统中独立存在的实体,有独立的资源
-
动态性:正在系统中活动的指令集合
-
并发性:单个处理器上可执行多个进程
-
-
线程:进程的一个实体,CPU 调度的基本单位,不能单独执行
-
进程拥有独立的地址空间(内存单元),线程没有(仅有独立的栈空间,但堆空间和方法区共享)
-
一个进程至少有一个线程,同一进程内多个线程共享其资源,提高效率;但是,多个线程共享的系统资源可能就会带来安全隐患。
为什么要用多线程?
- 提高应用程序的响应,对图形化界面更有意义,可增强用户体验;
- 提高计算机系统 CPU 的利用率;
- 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,易于理解和修改。
何时要用?
- 程序需要同时执行两个或多个任务;
- 程序需要实现一些需要等待的任务,如用户输入、文件读写操作、网络操作、搜索等;
- 需要一些后台运行的程序。
线程创建与启动
创建线程时,thread.start() 方法在 JVM 中开辟一个新的栈空间供该分支线程使用。
每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作,run() 方法的主体称为线程体。
注意:启动线程是通过 start() ,不是直接调用 run() 。一个线程对象只能调用一次 start() 方法,如果重复调用,会抛出异常 IllegalThreadStateException。
创建线程有四种方法:
-
继承 Thread 类,重写 run() 方法
启动线程:子类对象.start(); 启动线程,线程(由 JVM)再调用 run() 方法。
缺点:OOP 单继承局限性(无法继承其他类,Java 仅支持接口多实现和类的单继承),成员属性是能用 static ,确保多个线程共用一个对象
问题:多个线程共用一个资源的情况下(购票),线程不安全、数据紊乱
-
实现 Runnable 接口,重写 run() 方法(API )
启动线程:创建目标对象 + new Thread(目标对象).start(); //通过 Thread 类含参构造器创建线程对象,即将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造器中。
优点:可以继承其他类,避免单继承的局限性
问题:线程不安全,多个线程可以共享同一个接口实现类的对象
-
实现 Callable 接口,重写 call() 方法(API 实现, JDK 1.5 之后)
支持泛型的返回值(可以获取线程执行结构),可以抛出异常,需要借助 TutureTask 类,不如获取返回结果。
缺点:效率较低,get() 方法是在当前线程中获取其他线程的执行结果,可能导致当前线程阻塞。
补充:Future 接口,可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。FutureTask 是其唯一实现类,同时又实现了 Runnable 接口,它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
-
使用线程池:使用 JUC( java.util.concurrent )中的线程池创建( JDK 1.5 之后)
对于经常创建和销毁、使用量特别大地资源,性能影响很大。
思路:提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池子。避免频繁地创建销毁、实现重复利用。目的就是为了更好的支持高并发任务,让开发者进行多线程编程时减少竞争条件和死锁的问题。
优点:提高响应速度(减少了创建新线程地时间)、降低资源消耗(重复利用线程池中的资源,不需要每次都创建、销毁)、便于线程管理(线程池可以对线程的创建、停止、数量等因素加以控制,使线程在一种可控的范围内)。
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程生命周期,没有任务时最多保持多久后终止
四种类型的线程池:
-
FixedThreadPool 定长线程池
创建:new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>);
是一种固定大小的线程池;corePoolSize 和 maximumPoolSize 都为用户设定的线程数量 nThreads;keepAliveTime 为 0 意味着一旦有多余的空闲线程,该线程就会被立即停止掉,但是上面写的无效,原因是阻塞队列用了 LinkedBlockingQueue,这是一个无界队列,永远不可能拒绝任务,实际线程将永远维持在 nThreads,所以这时的 maximumPoolSize 和 keepAliveTime 的设置都将无效。
-
CachedThreadPool 可缓存线程池
创建:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECOND,new SynchronousQueue<Runnable>);
是一个可以无限扩大的线程池,适合处理执行时间较小的任务;corePoolSize 为 0,maximumPoolSize 为无限大,意味着线程数量可以无限大;keepAliveTime 为 60s 意味着线程空闲时间超过 60s 就会被杀死;采用 SynchronousQueue 装载等待的任务,这个阻塞队列没有存储空间,意味着只要有请求到来,就必须找到一条工作线程处理,如果当前没有空闲的线程,那么就会创建一条新的线程。
-
SingleThreadExecutor 单一线程池
创建:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>);
只会创建一条工作线程处理任务。
-
ScheduledThreadPool 可调度的线程池
用于处理延时任务或定时任务。
-
补充:线程池相关 API :ExecutorService 和 Executors
-
ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor,ThreadPoolExecutor 提供的构造函数:
- public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue);
- public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory);
- public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler);
- public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
- 形参解释:corePoolSize 线程池中核心线程数最大值;maximumPoolSize 线程池中线程总数最大值(核心线程 + 非核心线程);keepAliveTime 线程池中非核心线程闲置超时时长;workQueue 线程池中的任务队列,维护着等待执行的 Runnable 对象(线程);threadFactory 创建线程的方式,这是一个接口;handler 当提交任务数超过 maximumPoolSize + workQueue 时(饱和了),任务会交给它来处理。
void execute(Runnable command):执行命令,无返回值,一般用来执行 Runnable
<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
void.shutdown():关闭连接池
-
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或定期执行。
一个 Java 应用程序 java.exe,至少有三个线程:main() 主线程、gc() 垃圾回收线程、异常处理线程。
线程常用方法
string getName():获取线程对象名称
void setName(String name):设置线程对象名称
static Thread currentThread():获取当前线程对象
void start():启动线程,并执行 run() 方法
static void sleep(long mills):当前线程进入休眠(阻塞状态)
void interrupt():唤醒正在休眠的线程
void stop():强行终止线程执行(不推荐使用,容易丢失数据)
int getPriority():获取线程优先级(默认5,最低1,最高10)
void setPriority(int newPriority):设置线程优先级
static void yield():礼让,放弃当前线程获取的 CPU 时间片,只是让线程从运行状态进入就绪状态,而不是阻塞状态,其他线程不一定就可以抢占到 CPU 时间片
void join():插队,等待该线程执行结束后再执行其他线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XhSDDQsu-1647331952821)(https://s2.loli.net/2022/02/26/einRYyxd1oXaEfZ.jpg)]
线程调度
抢占时间片:高优先级的线程有更大概率能抢占 CPU 的时间片,同优先级的线程组成队列(先到先服务)。
线程创建时继承父线程的优先级。
用户线程和守护线程
唯一的区别就是判断 JVM 何时离开,若 JVM 中都是守护线程,当前 JVM 就会退出。
① 用户线程:虚拟机必须确保其执行完毕,如 main() —主线程
② 守护线程:虚拟机不用等待其执行完毕,守护线程内部是一个死循环,如 gc() —垃圾回收机制、等待机制、监控机制
③ 正常线程都是用户线程,setDaemon(true) —设置该线程为守护线程
补充:定时器机制
间隔特定时间执行特点程序,可以用到 Thread.sleep() 方法,还有 java.util.Timer 类、Spring Task 框架。
作用:定时器和守护线程联合使用实现数据自动备份。
线程安全
Java 采用的是抢占式调度模型:线程抢占的 CPU 时间片多少取决于优先级,还有一种是均匀式调度模型:平均分配线程抢占的 CPU 时间片。
多线程出现安全问题的原因:当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程就参与进来,导致共享数据的错误。
解决:对于有多条语句操作共享对象的情况,只能让一个线程执行完,在执行过程中其他线程不可以参与执行。
注意:常量(不可修改)和局部变量(存在栈中,而栈内存线程独有,变量不可能被共享)不存在线程安全问题。
同步与异步编程模型:
-
同步:线程之间存在等待关系,线程排队执行
实现:
- 共享对象用 synchronized 修饰:Java 中每个对象都有属于自己的内部锁标记 Monitor
- 实例方法用 synchronized 修饰:该方法表示共享对象一定是 this,且同步的范围是整个方法体,不灵活,效率较低(无故扩大同步范围)
- 静态方法用 synchronized 修饰:表示线程对象要寻找类的锁才能进入就绪状态
-
异步:线程之间无等待关系,多线程并发
解决:
- 使用局部变量 + 静态变量代替成员变量
- 一个线程对应一个对象,对象不共享
- 线程同步机制:synchronized
同步机制
Java 为解决多线程安全问题提出的专业解决方式。
- 同步代码块:synchronized(对象){//需要被同步的代码块}。
- 同步方法:synchronized 声明在方法中。
同步锁机制:当共享资源被一个任务使用时,在它上面加锁,第一个访问这个资源的任务必须锁定该资源,其他任务不可以访问该资源,直到该资源被解锁,其他任务才可以访问并锁定并使用。
synchronized 的锁是什么:任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。
-
同步方法的锁:静态方法(类名.class),非静态方法(this)
-
同步代码块的锁:自己指定,很多时候也是指定为 this 或 类名.class。
注意:必须确保使用同一资源的多个线程共用一把锁。一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用一把锁(this)。
同步的范围:明确哪些代码是多线程运行的,明确多个线程中是否有共享数据,明确多线程运行代码中是否有多条语句操作共享资源。对多条操作共享数据的语句块,我们只能让一个线程都执行完,过程中其他线程不能参与执行。也就是说,所有操作共享数据的语句都要放在同步范围内。不能太大(没发挥多线程功能)也不能太小(没锁住)。
释放锁的操作
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步方法、同步代码块中遇到 break、return 终止了代码块、方法的继续执行
- 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception,导致异常结束
- 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停并释放锁(wait 会释放锁)。
不会释放锁的操作
- 线程在执行同步方法、同步代码块时,程序调用了 Thread.sleep() 、Thread.yield(),暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,不会释放锁(同步监视器),要尽量避免使用。
死锁
指不同的线程因争夺系统资源而产生相互等待的现象,即两个线程都在等待对方释放同步监视器。一旦出现死锁,整个程序不会发生异常,也不会有任何提示。
形成死锁的条件:
- 互斥:某一种资源只允许一个进程访问
- 占有且等待:已经占有资源的进程同时正在等待其他进程释放该资源
- 不可抢占:已经被分配的资源不能被其他进程抢夺
- 循环等待:若干进程组成环路,环路中每个进程都在等待相邻进程释放资源
避免死锁:
- 破坏条件2,所有进程在开始运行前,必须一次性申请需要的全部资源,但是降低了资源利用率
- 破坏条件2,只要有一个资源得不到分配,其他资源也不分配
Lock(锁)
JDK 1.5 开始,Java 提供了更加强大的而线程同步机制——通过显式定义同步锁的对象来实现同步。同步锁使用 Lock 对象充当。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,线程开始访问共享资源前应先获得 Lock 对象。
例:
class A{
private final ReentrantLock lock=new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}finally{
lock.unlock();//如果同步代码块有异常,要将unlock()写入这里
}
}
}
synchronized VS Lock
- Lock 是显式锁,需要手动开启和关闭锁;synchronized 是隐式锁,离开作用域(同步代码块、同步方法)就会自动释放。
- Lock 锁只有代码块锁,synchronized 有代码块锁和方法锁。
- 使用 Lock 锁,JVM 将花费较少时间来调度线程,性能更好,并且具有更好的扩展性(接口可以被多个子类继承)。
- 优先使用顺序:Lock > 同步代码块(已经进入方法体、分配了资源) > 同步方法(在方法体之外)
线程间的同行–经典题目:生产者消费者
以下三个方法只有在 synchronized 方法或代码块中,才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。因为这三个方法必须有锁对象调用(对象.方法),而任意对象都可以作为 synchronized 的同步锁。
wait():令当前线程挂起并放弃 CPU 和同步资源(释放锁)并等待,使别的线程可以访问并修改共享资源,而当前线程排队,等待其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获取对监视器的所有权后才能执行。
notify():唤醒正在排队等待同步资源的最高优先级的线程。
notifyAll():唤醒正在排队等待同步资源的所有线程。
生产者消费者模型
原理:
- 生产者和消费者各自有一个请求队列(内存缓冲区),”仓库“,可以使用 List ;
- 内存缓冲区为空时,消费者线程必须等待
- 内存缓冲区为满时,生产者线程必须等待
- 不空也不满时,两者之间既可以是动态平衡,也可以非动态平衡
相关方法:
- obj.wait():使对象 obj 上的活动线程进入无限等待状态,并且释放占有锁
- obj.notify():唤醒对象 obj 上的任一等待线程
- obj.notifyAll():唤醒对象 obj 上的所有等待线程
单核 CPU VS 多核 CPU:
单核 CPU 是一种假的多线程,因为在一个时间单元内只能执行一个线程的任务。只是因为 CPU 时间单元特别短,因此感觉不出来。多核 CPU (现在服务器都是多核)才能更好的发挥多线程的效率。
并行 VS 并发:
- 并行:多个 CPU 同时执行多个任务,多个人同时做不同的事
- 并发:一个 CPU 同时执行多个任务,多个人同时做同一件事(秒杀)
单核 CPU 不能实现真正的多并发,因为单核 CPU 一个时间点处理一个事件,在多个线程之间频繁切换