接口
继承树
Collection接口
Map接口
Collection 接口
- Collection接口:单列集合,用来存储一个一个的对象
- List接口:extends Collection,存储有序的、可重复的数据。 –>“动态”数组
- ArrayList、LinkedList、Vector
- Set接口:extends Collection,存储无序的、不可重复的数据 –>高中讲的“集合”
- HashSet、LinkedHashSet、TreeSet
- List接口:extends Collection,存储有序的、可重复的数据。 –>“动态”数组
Collection 接口方法
遍历Collection的两种方式:
① 使用迭代器==Iterator==
② foreach循环(或增强for循环):==内部仍然调用的是迭代器==
迭代器Iterator接口:
==Iterable==:接口,规定实现类或子接口必须要有提供迭代器的能力
==Iterator==:迭代器的类型,迭代器使用Iterable接口中iterator方法返回的
java.utils包下定义的迭代器接口:Iterator
- Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
- GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
==支持删除行为==:删除当前遍历到的元素(迭代器对象.remove()),迭代器在迭代的时候不支持集合本身的修改行为(add|remove),否则,会引发java.util.ConcurrentModificationException
并发修改异常
遍历代码实现:
Iterator iterator = coll.iterator();
//hasNext():判断是否还下一个元素
while(iterator.hasNext()){
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
符合迭代器的设计模式
- 抽象集合接口(Iterable(抽象接口))
- 集合的具体实现类实现Iterable(必须拥有给外界提供迭代器(Iterator)的能力)
- 抽象迭代器接口(Iterator)
- 具体的迭代器实现类(实现Iterator,体现为不同集合中的内部类)
List接口
==存储的数据特点:存储有序的、可重复的数据。==
List接口常用方法:
- 增:
add(Object obj)
- 删:
remove(int index) / remove(Object obj)
- 改:
set(int index, Object ele)
- 查:
get(int index)
- 插:
add(int index, Object ele)
- 长度:
size()
- 遍历:
- ① Iterator迭代器方式
- ② 增强for循环
- ③ 普通的循环
实现类1:ArrayList
底层:底层是一个Object类型的数组,初始的默认长度0,在第一次add时,容量变为10,也可以指定长度,初始长度如果满了,底层进行自动扩容,扩容为原来的1.5倍oldCapacity + (oldCapacity >> 1)
。10—->15—->22,如果对集合中的元素个数可以预估,那么建议预先指定一个合适的初始容量new ArrayList(20);
优点:查找效率高,向末尾添加元素也可以
缺点:增加 、删除牵扯到数组的扩容和移动,效率低
实现类2:LinkedList
底层:是一个链表(双向)结构,不是线性存储。
优点:增加、删除效率高
缺点:查找效率低
实现类3:Vector
底层:是一个Object类型的数组,初始的默认长度为10,扩容的时候扩容为原来的2倍 ,如果自己指定扩容的长度,那么就是就容量加指定的,如果没有指定,就是旧容量的2倍。
优点:线程安全,通过synchronized同步锁实现
缺点:效率低
存储的元素的要求:
==添加的对象,所在的类要重写equals()方法==
三个实现类的异同
Set接口
==存储的数据特点:无序的、不可重复的元素(如果hashCode返回值一样和equals为true,则是重复元素)==
具体说明:
以HashSet为例说明:
- 无序性:不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。
- 不可重复性:保证添加的元素照equals()判断时,不能返回true,hashCode不可以一样.即:相同的元素只能添加一个。
==Set接口中没额外定义新的方法,使用的都是Collection中声明过的方法。==
实现类1:HashSet
**底层:**HashSet的底层是一个HashMap,只是将HashMap中的值设置为一个常量,用所有的键组成了一个HashSet
优点:==可以存储null值==
缺点:线程不安全
实现类2:LinkedHashSet
LinkedHashSet 是 HashSet 的子类
底层:是一个LinkedHashMap,底层维护了一个数组 + 双向链表
- 遍历其内部数据时,可以按照添加的顺序遍历
- 在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。(双向链表)
- 对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
存储对象所在类的要求:
向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
实现类3:TreeSet
TreeSet 是 SortedSet(继承于Set)接口的实现类,TreeSet 可以确保集合元素处于排序状态。
==不可以放null对象==
存储对象所在类的要求:
- 向TreeSet中添加的数据,要求是相同类的对象而且对象必须是可比较的。
- 两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator,创建TreeSet的时候可以作为构造方法参数传入!)
Map接口
- —-Map:双列数据,存储key-value对的数据 —类似于高中的函数:y = f(x)
- —-HashMap
- —-LinkedHashMap
- —-TreeMap
- —-Hashtable
- —-Properties:常用来处理配置文件。key和value都是String类型
- —-HashMap
Map接口常用方法:
存储结构的理解:
- ==Map中的key== : 无序的、不可重复的,使用Set存储所有的key —> key所在的类要重写equals()和hashCode() (以HashMap为例)
- ==Map中的value== : 无序的、可重复的,使用Collection存储所的value —>value所在的类要重写equals() > 一个键值对:key-value构成了一个Entry对象。
- ==Map中的entry== : 无序的、不可重复的,使用Set存储所的entry
实现类1:HashMap
底层:所有的键构成了一个HashSet,整体是数组+链表/红黑树
优点:效率高,可以存储null值、null键
缺点:线程不安全
- 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
- 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap的put( )过程
- 根据键的hash码(调用键的hashcode方法)进行哈希运算,得到一个整数哈希值
- 判断哈希表是否为空或者长度是否为0,如果是,要对数组进行初始化,如果否,进入3
- 根据1得到的哈希值计算数组索引(与运算),得到一个和数组存储位置匹配的索引i
- 判断i号位置是否为null,如果null,就将键和值封装为一个Entry类型的对象进行插入,如果不为null,进入5
- 判断key是否存在(使用equals进行判断),如果存在,覆盖原有的值,如果不存在,进入6
- 判断i号位置是否为一个树结构,如果是一个树结构,在树中进行插入,如果不是树结构,进入7
- 为链表结构,对链表进行遍历,判断key是否存在,存在就覆盖,不存在就在链表中插入新的节点
- 链表中插入新节点后,如果i号位置的元素个数大于等于8,i号位置的所有元素转换为树结构,如果不大于等于8,新节点正常插入结束
- size++
- 判断是否要进行扩容,如果需要扩容,就执行Resize()进行扩容
- 结束
实现类2:LinkedHashMap
底层:==HashMap的子类==;在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
优点:保证在遍历map元素时,可以照添加的顺序实现遍历。对于频繁的遍历操作,此类执行效率高HashMap。
缺点:线程不安全
实现类3:TreeMap
底层:==数组+链表/红黑树==
优点:保证照添加的key-value对进行排序,实现按照键排序遍历(自然排序或定制排序)
缺点:
==键不可以为null,值可以为null==
实现类4:Hashtable
底层:数组+链表/红黑树
优点:线程安全
缺点:效率低;不能存储null的key和value
不可以放入null值、null键
实现类5:Properties
底层:Hashtable的子类,数组+链表/红黑树
优点:一般用来存储配置文件
缺点:==key和value都是String类型==
面试题
- 为什么HashMap的长度为什么要设计成2的n次方?
- 为了方便将去余运算转换为位运算
- 为什么设计扩容因子
- 为了减少一个桶里元素碰撞的概率,本质就是不要让一个桶中的元素个数太多
- 根据key怎么找到值的(get(key))?
- 根据key的哈希码先找到桶的位置,然后再在一个桶中用equals方法进行比对,找到对应的元素,获取其值。
- 为什么使用hash码相关的集合的时候,重写equals方法的时候建议也重写hashCode方法
- 如果equals返回true.但是哈希码不一样,有可能会放到不同的桶中,不同的桶中就存在了键重复的元素了,有漏洞,最终目的是为了让equals返回true的两个对象能放到一个桶中,保证键不重复
- HashMap和Hashtable的区别:
- 继承的类不一样,HashMap继承自AbstractMap,Hashtable继承自Dictionary类
- 根据hash码计算存储位置的过程和算法是不同的(hashMap最后进行位运算,hashtable最后进行取余的运算)。
- Hashtable不能放入null键、null值,但是hastMap可以放入null键、null值
- Hashtable初始的默认长度是11,HashMap是16.
- Hashtable线程安全,效率低,HashMap线程不安全,效率高
- 扩容方式不同,HashMap扩容为原来的2倍,Hashtable扩容为原来的二倍加1
int newCapacity = (oldCapacity << 1) + 1
- Hashtable支持Enumeration遍历 ,HashMap不支持
- HashMap在jdk8中相较于jdk7在底层实现方面的不同:
- jdk7:底层初始创建一个长度为16的数组,jdk8:初始化没有指定HashMap数组大小,而是在添加第一个元素时,进行扩容操作
- jdk8底层的数组是:Node[],而非Entry[](jdk7)
- 首次调用put()方法时,底层创建长度为16的数组
- jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
- 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所有数据改为使用红黑树存储。
Collections工具类
作用:操作Collection和Map的工具类
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
Collections常用方法
集合的线程安全
List的线程安全
如果使用ArrayList多个线程同时添加和读取,可能会出现并发修改异常ConcurrentModificationException
Vector
使用synchronized关键字,对修改、添加、删除等方法进行修饰,实际开发中,我们很少使用,强同步性能低
Collections
可以使用Collections中的静态方法synchronizedXXX将XXX类型的集合转为线程安全版本
CopyOnWriteArrayList(推荐)
List list = new CopyOnWriteArrayList();
使用写时复制技术,也就是读的时候可以并发进行读,但是写,只允许一个线程写,复制一份和当前集合相同的集合,然后在写完以后,进行合并
使用Lock可重入锁进行实现
Set的线程安全
Collections
和List的使用方法一样
CopyOnWriteArraySet
和List的使用方法一样
Map的线程安全
HashTable
使用synchronizd强同步,效率低,不推荐
Collections
和List的使用方法一样
ConcurrentHashMap
Map map = new ConcurrentHa
File类
- File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)
- File类声明在java.io包下
- File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法**,并未涉及到写入或读取文件内容的操作。**如果需要读取或写入文件内容,必须使用IO流来完成。
- 后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的”终点”.
File的实例化
构造器:
路径分隔符:
File类的常用方法
IO流(stream)
I:input ; O : output
流的分类
- 操作数据单位:字节流(8bit)、字符流 (16bit)
- 数据的流向:输入流、输出流
- 流的角色:节点流、处理流
IO 流体系
节点流
如:InputStream、Reader、OutputStream、Writer
节点流与具体节点相连接,直接读写节点数据
- 字节流操作字节,比如:
.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
- 字符流操作字符,只能操作普通文本文件。最常见的文本文件:
.txt,.java,.c,.cpp
等语言的源代码。尤其注意.doc,excel,ppt
这些不是文本文件。
输入流InputStream & Reader
InputStream(字节流) 和 Reader **(字符流)**是所有输入流的基类。
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该==显式关闭文件 IO 资源(使用close方法)。==
FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
输出流OutputStream & Writer
**OutputStream(字节流)和Writer(字符流)**是所有输出流的基类。
- 在写入一个文件时,如果使用构造器
FileOutputStream(file)
,则目录下有同名文件将被覆盖。 - 如果使用构造器
FileOutputStream(file,true)
,则目录下的同名文件不会被覆盖, 在文件内容末尾追加内容。
因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组, 即以 String 对象作为参数
==显示关闭IO资源,需要使用flush和close方法==
FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter
文件的复制
public static void copy(String copyBy,String copyTo) throws IOException {
//输入流
InputStream inputStream = new FileInputStream(new File(copyBy));
//输出流
OutputStream outputStream = new FileOutputStream(new File(copyTo));
//创建缓冲数组
byte[] bytes = new byte[1024];
int i;
//循环写入缓冲,然后从缓冲数组中输出到目标文件
while ((i = inputStream.read(bytes)) != -1){
outputStream.write(bytes,0,i);
}
//释放资源
outputStream.flush();
inputStream.close();
outputStream.flush();
}
处理流:
缓冲流:
BufferedInputStream 、BufferedOutputStream、BufferedReader、BufferedWriter
优点:提供流的读取、写入的速度
提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb
转换流:
- InputStreamReader:将一个字节的输入流转换为字符的输入流
- 解码:字节、字节数组 —>字符数组、字符串
- OutputStreamWriter:将一个字符的输出流转换为字节的输出流
- 编码:字符数组、字符串 —> 字节、字节数组
- 说明:编码决定了解码的方式
常见的编码表
- ASCII:美国标准信息交换码。
- 用一个字节的7位可以表示。
- ISO8859-1:拉丁码表。欧洲码表
- 用一个字节的8位表示。
- GB2312:中国的中文编码表。最多两个字节编码所有字符
- GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
- Unicode:国际标准码,融合了目前人类使用的所字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
- UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
客户端/浏览器端 <—-> 后台(java,GO,Python,Node.js,php) <—-> 数据库
要求前前后后使用的字符集都要统一:UTF-8.
对象流:
ObjectInputStream、OjbectOutputSteam
用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
实现序列化的对象所属的类需要满足:
- 需要实现接口:Serializable
- 当前类提供一个全局常量:serialVersionUID (读入和写出要保持一致)
- 除了当前Person类需要实现Serializable接口之外,还必须保证其内部所属性也必须是可序列化的。(默认情况下,基本数据类型可序列化)
补充:
ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量
多线程
程序、进程、线程的区别
- 程序(program)
- 概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
- 进程(process)
- 概念:程序的一次执行过程,或是正在运行的一个程序。
- 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
- 线程(thread)
- 概念:进程可进一步细化为线程,是一个程序内部的一条单一的执行路径(顺序控制流),可以共享所属进程的数据。
- 说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
多线程程序的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
并发和并行
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
创建多线程
一个Java应用程序java.exe,其实至少三个线程:
- 主线程(
main( )
) - 垃圾回收线程(
gc( )
) - 异常处理线程。当然如果发生异常,会影响主线程。
方式一:继承Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类的run() –> 将此线程执行的操作声明在run()中 (线程要完成的任务)
- 创建Thread类的子类的对象
- 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
注意点:
问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
方式二:实现Runnable接口
- 创建一个实现了Runnable接口的类
- **实现类去实现Runnable中的抽象方法:**run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
两种方式的对比
开发中:==优先选择:实现Runnable接口的方式==
- 原因:
- 实现的方式没类的单继承性的局限性
- 实现的方式更适合来处理多个线程共享数据的情况。
- 联系:
public class Thread implements Runnable
- 相同点:
- 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
- 目前两种方式,要想启动线程,都是调用的Thread类中的start()。
方式三:实现Callable接口
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值,get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
Thread类
java.lang.Thread
构造器
常用方法
线程的优先级:
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 –>默认优先级
获取和设置优先级:
getPriority()
:获取线程的优先级
setPriority(int p)
:设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
守护线程和用户线程
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
线程同步
通过同步机制,来解决线程的安全问题。
方式一:同步代码块synchronized
synchronized(同步监视器){
//需要被同步的代码
}
- 操作共享数据的代码,即为需要被同步的代码。–>不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器(Object类型),俗称:锁。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
- 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this ;静态的同步方法,同步监视器是:当前类本身
方式三:Lock
可重入锁
可以重复使用的锁,锁的创建只有一次,可以重复调用lock和unlock
Lock可重入锁代表实现类为:ReentrantLock
公平锁和非公平锁
公平锁的效率比非公平锁低
公平锁总是可以保证让所有线程中等待时间最长的线程先执行
在new ReentrantLock(true)
时,参数为true,创建的就是公平锁,不传参,默认是非公平锁
读写锁
ReadWriteLock读写锁代表实现类为:ReentrantReadWriteLock
两个线程都是写锁:互斥,同步执行
两个线程一写一读:互斥,同步执行
两个线程都是读锁:共享,异步执行
synchronized 与 Lock的异同:
- 相同:
- 二者都可以解决线程安全问题
- 不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
- Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
说明:
- 出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续
- 我们使用同步时,要避免出现死锁。
解决死锁
1、调整锁的顺序,避免可能出现的死锁
2、调整锁的范围,避免在一个同步代码块中使用另一个同步代码块
3、使用可重入锁ReentrantLock
线程通信
使用synchronized强同步
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
注意:wait方法等待的线程,在哪等待,被唤醒后,就从哪里开始执行,可能会出现虚假唤醒的问题,所以建议wait循环使用在while循环中
使用Lock接口下的可重入锁
- 获取Condition对象
Condition condition = lock.newCondition();
- 使用condition对象的方法
- await():当前线程等待,对应wait()
- signal():唤醒一个线程,对应notify()
- signalAll():唤醒所有线程,对应notifyAll()
sleep() 和 wait()的异同
- 相同点:
- 一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
- 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:
- 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
同步锁释放的操作
不会释放锁的操作
ThreadLocal类
可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据(可以存放线程范围内的局部变量)
实际使用场景
1、在javaweb项目里面,我们做MyBatis的工具类的时候,为了可以满足多个线程中,各个线程关闭自己的SqlSession对象的需求,而不会产生线程安全的问题,所以,我们需要将SqlSession对象存入到ThreadLocal对象中
常用方法
set()
用于向ThreadLocal对象中存值
get()
用于向ThreadLocal对象中取值
remove()
用于从ThreadLocal对象中删除值
CAS自旋锁
解决的问题
例如,一个int类型的变量,在高并发的情况下进行加减,通常会导致线程不安全,数值不准确的问题,例如
public class Test1 {
static int age = 0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 2000; i1++) {
age++;
}
}
}).start();
}
//判断除了java默认的主线程和垃圾回收线程都执行完了
while(Thread.activeCount()>2){
Thread.yield();
}
//代码到了此处,线程数量只剩2个,上面的循环代码已经执行完成了
System.out.println(age);
}
}
//此时的age小于40000,因为出现了线程不安全的现象
解决方式
1、使用synchronized同步锁的方式
缺点是,该方式是同步的,必须一个线程执行完,才会执行另一个线程,效率低下
public class Test1 {
static int age = 0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (Test1.class){
for (int i1 = 0; i1 < 2000; i1++) {
age++;
}
}
}
}).start();
}
//等代码代码运行完毕之后,age是多少
//判断线程数量还剩下几个
while(Thread.activeCount()>2){
Thread.yield();
}
//代码到了此处,线程数量只剩2个,上面的循环代码已经执行完成了
System.out.println(age);
}
}
2、通过CAS自旋锁的方式进行
通过AtomicInteger,可以在线程提交的时候,用自己工作空间中的数值,与现在主存中的数值进行比对,如果一致的话,就压入主存,如果数值已经发生改变的话,那么会进行自旋,也就是重新进行运算,再进行比较
public class Test1 {
static AtomicInteger age = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 2000; i1++) {
age.getAndIncrement();
}
}
}).start();
}
//等代码代码运行完毕之后,age是多少
//判断线程数量还剩下几个
while(Thread.activeCount()>2){
Thread.yield();
}
//代码到了此处,线程数量只剩2个,上面的循环代码已经执行完成了
System.out.println(age);
}
}
//此时的结果一定是40000
但是,这样的方法,可能会产生ABA的问题,
比如当前主存的值是100.
并发:
线程A: 从主存中拿到100,放入自己的工作空间中。
线程B: 从主存中拿到100,放入自己的工作空间中。
线程A: 100+1,并且刷新了主存,主存中的值101.
线程C: 从主存中拿到101,减1,并且把100放入主存。
线程B: 执行+1动作,比对CAS,竟然相同,由于线程C对数据的操作,认为没有人改过,直接提交成功了。
解决ABA问题
在CAS的思想上,利用版本戳的思想,从原始数据开始,给每次的数据都加上一个版本,每次对数据发生修改,版本也会进行迭代
具体的实现类,AtomicStampedReference,这个类在实例化时,第二个参数为版本号
public class Test3 {
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,0);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(
100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(
101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
int version = atomicStampedReference.getStamp();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(
100, 101, version, version + 1));
}
});
thread1.start();
thread2.start();
Thread.sleep(2000);
System.out.println(atomicStampedReference.getReference()+" "+atomicStampedReference.getStamp());
}
}
线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
- 好处:
-
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 提高响应速度(减少了创建新线程的时间)
-
- 便于线程管理
- 相关API
-
- JDK 5.0起提供了线程池相关API:ExecutorService和Executors
- 线程池所在的包:java.util.concurrent
- JDK 5.0起提供了线程池相关API:ExecutorService和Executors
阻塞队列
JDK7提供了7个阻塞队列。分别是:
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列,如果没有定义上限,上限就是Integer.MAX_VALUE。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,对元素进行持有直到一个特定的延迟到期,注入的元素必须实现Delayed接口。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
线程池使用的也是阻塞队列
阻塞队列和非阻塞队列的区别
- 入队
- 阻塞队列:当数据满的时候,放入数据,数据丢失
- 非阻塞队列:当数据满的时候,进行阻塞等待,只有什么时候队列中有出队的,才会将数据入队
- 出队
- 阻塞队列:当队列无元素,阻塞等待,什么时候有数据放进来,什么时候出队
- 非阻塞队列:当队列无元素,取出的是null
线程池的执行顺序
1、用户提交任务,判断核心线程是否正在处理,如果核心线程有空闲,直接使用核心线程执行
2、如果核心线程没有空闲,判断队列是否满了,如果没有满,就把认为放进队列中
3、如果队列已经满了,那么判断线程池中线程+新任务线程是否大于最大线程数,如果大于,抛出异常,拒绝执行
4、如果小于等于最大线程数,创建新的线程,执行任务
线程池的创建
1、ThreadPoolExecutor
/* 参数说明
* 参数1:线程池创建后,核心线程数
* 参数2:最大线程数
* 参数3:核心线程外,新创建的线程空闲时,最大存活时间
* 参数4:最大存活时间的单位
* 参数5:用于存放任务的阻塞队列
* 参数6:用于创建线程的线程工厂
* 参数7:任务被拒绝后的策略
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,2,3, TimeUnit.SECONDS,new LinkedBlockingDeque<>(3),Executors.defaultThreadFactory(), defaultHandler);
线程池中,提交任务submit()与execute()之间的区别
- execute()是专门用来提交Runable接口任务的;submit()既可以提交Runable任务也可以提交Callable任务
- execute()没有返回结果;submit()可以使用Future接收线程的返回结果
- execute()方法中不能处理任何异常;submit()支持处理任务中的异常,使用
Funture.get()
2、Executors
可以使用Executors的静态方法,来创建线程池
创建方法 | 线程池 | 阻塞队列 | 使用场景 |
---|---|---|---|
newSingleThreadExecutor() | 单线程的线程池 | LinkedBlockingQueue | 适用于串行执行任务的场景,⼀个任务⼀个任务地执行 |
newFixedThreadPool(n) | 固定数目线程的线程池 | LinkedBlockingQueue | 适用于处理CPU密集型的任务,适用执行长期的任务 |
newCachedThreadPool() | 可缓存线程的线程池 | SynchronousQueue | 适用于并发执行大量短期的小任务 |
newScheduledThreadPool(n) | 定时、周期执行的线程池 | DelayedWorkQueue | 周期性执行任务的场景,需要限制线程数量的场景 |
例如
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
Future<Integer> future = es.submit(new Task("a"));
System.out.println(future);
// 关闭线程池:
es.shutdown();
}
阿里巴巴Java开发手册规定线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
实现网络通信需要解决的两个问题
- 如何准确地定位网络上一台或多台主机;定位主机上的特定的应用
- 找到主机后如何可靠高效地进行数据传输
网络通信的两个要素:
- 对应问题一:IP和端口号
- 对应问题二:提供网络通信协议:TCP/IP参考模型(应用层、传输层、网络层、物理+数据链路层)
通信要素一:IP和端口号
IP的理解
- IP:唯一的标识 Internet 上的计算机(通信实体)
- 在Java中使用InetAddress类代表IP
- IP分类:IPv4 和 IPv6 ; 万维网 和 局域网
- 域名: www.baidu.com www.mi.com www.sina.com www.jd.com
- 域名解析:域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。
- 本地回路地址:127.0.0.1 对应着:localhost
InetAddress类:
此类的一个对象就代表着一个具体的IP地址
端口号
正在计算机上运行的进程。
要求:不同的进程不同的端口号
范围:被规定为一个 16 位的整数 0~65535。
端口号与IP地址的组合得出一个网络套接字:Socket
通信要素二:网络通信协议
分型模型
TCP和UDP的区别
TCP三次握手和四次挥手
服务端格式:
/**
* 服务器端
*/
public class ServerSocketTest {
public static void main(String[] args) throws IOException {
//1、创建服务器端的ServerSocket,指明自己的端口号
ServerSocket serverSocket = new ServerSocket(9999);
//2、调用accept()表示接收来自于客户端的socket
Socket socket = serverSocket.accept();
//3、获取输入流
InputStream inputStream = socket.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
//4、获取输出流
OutputStream outputStream = socket.getOutputStream();
ObjectOutputStream outputStream1 = new ObjectOutputStream(outputStream);
//5、循环收发
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("客户端说:" + objectInputStream.readUTF());
System.out.println("请发送你要和客户端说的话:");
String s = scanner.next();
if (s.equals("exit")){
break;
}
outputStream1.writeUTF(s);
outputStream.flush();
outputStream1.flush();
}
//5、关闭资源
inputStream.close();
objectInputStream.close();
socket.close();
serverSocket.close();
}
}
客户端格式:
/**
* 客户端
*/
public class SocketTest {
public static void main(String[] args) throws IOException {
//1、创建Socket对象,指明服务器端的ip和端口号
Socket socket = new Socket("localhost",9999);
//2、获取一个输出流,用于输出数据
OutputStream outputStream = socket.getOutputStream();
ObjectOutputStream outputStream1 = new ObjectOutputStream(outputStream);
//3、获取输入流,用于接收服务器端数据
InputStream inputStream = socket.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
//4、循环收发
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请发送你要和服务器端说的话:");
String s = scanner.next();
if (s.equals("exit")){
break;
}
outputStream1.writeUTF(s);
outputStream.flush();
outputStream1.flush();
System.out.println("服务器端说:" + objectInputStream.readUTF());
}
//5、关闭资源
outputStream.close();
outputStream1.close();
socket.close();
}
}
泛型的概念
所谓泛型,就是==允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型==。这个类型参数将在使用(例如,继承或实现这个接口,用这个类型声明变量、创建对象时确定(即传入实际的类型参数,也称为类型实参)。
==JDK1.5引入==
泛型的定义中,不可以使用基本数据类型,可以使用对应的包装类进行替换
可避免的问题
1、类型不安全,导入数据存入混乱,添加泛型,在编译时就会进行类型检查,保证了数据安全
2、避免了强制类型转换时,出现异常ClassCastException
泛型的使用
List<String> li = new ArrayList<String>();
1、在实例化集合类时,可以指明具体的泛型类型
2、指明完泛型以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。
3、如果实例化时,没指明泛型的类型。默认类型为java.lang.Object类型。
4、泛型可以嵌套使用
5、JDK7新特性,类型推断,可以写成:
List<String> li = new ArrayList<>();
泛型类
修饰符 class 类名<泛型形参1,泛型形参2,......> {
在类中使用泛型形参对类型进行占位;
}
泛型形参名称,一般由单个大写字母组成,常用的:E、T、V、N、K
如果泛型类在实例化的时候没有指明泛型的类型,那么默认的类型就是Object类型,如果实例化的类是带有泛型的,那么建议在实例化的时候指明类的泛型
- ==泛型不同的两个引用不可以互相赋值==
- 在类、接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型,但==在静态方法中不能使用类的泛型==。
- 异常类不能是泛型的
- 如果子类在继承带泛型的父类时,指明了泛型类型。则实例化子类对象时,不再需要指明泛型。
- 子类除了指定或保留父类的泛型,还可以增加自己的泛型
泛型接口
修饰符 interface 类名<泛型形参> {
在接口中使用泛型形参对类型进行占位;
}
泛型方法
泛型方法是指在方法中出现了泛型的结构,泛型参数与类的泛型参数没任何关系和类是不是泛型类也没有关系。
可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。
public <E> List<E> 方法名(E[] arr){
方法体;
}
泛型在继承方面的体现
类A是类B的父类,**G<A>
和G<B>
**不具备子父类关系,属于并列关系
通配符
==通配符:?==
类A是类B的父类,G<A>
和G<B>
是没关系的,二者共同的父类是:G<?>
以List<?>数据的读写为例:
l 读取List的对象list中的元素时,永远是数据安全的,因为不管list的元素真实类型是什么,它都是Object类型。
l 对于写入list中的元素时,由于因为我们不知道c的元素类型,我们不能向其中添加对象。
l 在写入元素时,唯一的例外是null,它是所有类型的成员
限制的通配符
//? extends A:相当于小于等于,可以放A和A的子类,所以在接收的时候,可以使用A或A的父类进行接收
G<? extends A> 可以作为G<A>和G<B>的父类,其中B需要是A的子类
//? super A:相当于大于等于,可以放A和A的父类,所以,在接收的时候,也只可以使用Object类型接收
G<? super A> 可以作为G<A>和G<B>的父类,其中B需要是A的父类
? extends A:不可以进行写入,因为写入的类型无法确定,继承关系的限制。
? super A:可以写入A以及A的子类,因为其代表A或A的父类,所以父类肯定可以指向子类
反射的理解
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
==框架 = 反射 + 注解 + 设计模式。==
反射机制能提供的功能
Class类的理解
1.类的加载过程:
程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾)。
接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为Class的一个实例。
2.换句话说,Class的实例只对应着加载到内存中的一个运行时类。
3.加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。
4.一个加载的类在 JVM 中只会有一个Class实例
获取Class实例的几种方式:
方式一:调用运行时类的属性:.class
Class class1 = 类名.class;
方式二:通过运行时类的对象,调用getClass()
Person p1 = new Person();
Class class1 = p1.getClass();
方式三:调用Class的静态方法:forName(String classPath),可能抛出ClassNotFoundException异常
Class class1 = Class.forName("路径");
方式四:使用类的加载器
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("类的全类名");
创建类的对象的方式
方式一:new + 构造器
方式二:要创建Xxx类的对象,可以考虑:Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在。可以调用其静态方法,创建Xxx对象。
方式三:通过反射
包相关信息
//可以通过Class对象的getPackage()方法进行获取
Class c = 类名.class;
Package p = c.getPackage();
Class实例可以是哪些结构的说明
数组的Class实例中,只要类型和维度一样,那么两个Class实例相等
int[] a = new int[10];
int[] b = new int[100];
a.class == b.class
//结果为true
Class类的常用方法
创建运行时类对象的方法
Class<Person> clazz = Person.class;
Person obj = clazz.newInstance();
newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。
使用此方法,要求:
- 运行时类必须提供空参的构造器
- 空参的构造器的访问权限足够。通常,设置为public。
没有无参的构造器就不能创建对象了吗?
==不是!==只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。
1)通过Class类的getDeclaredConstructor(Class … parameterTypes)
取得本类的指定形参类型的构造器
2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
3)通过Constructor实例化对象。
获取类的完整结构
接口、父类
构造器
方法
Field属性结构
Annotation注解
泛型
所在包Package
调用类的指定结构
调用方法
//一、获取类的Class实例、并获得运行时类的实例
Class clazz = Person.class;
Person p = (Person) clazz.newInstance();
//二、获取需要调用的方法,getDeclaredMethod():
//参数1 :指明获取的方法的名称 参数2:指明获取的方法的形参列表
Method show = clazz.getDeclaredMethod("show", String.class);
//三、保证当前方法可以访问
show.setAccessible(true);
//四、调用方法的invoke()
//参数1:方法的调用者 参数2:给方法形参赋值的实参
//invoke()的返回值即为对应类中调用的方法的返回值。
Object returnValue = show.invoke(p,"CHN");
调用属性
Class clazz = Person.class;
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");
//2.保证当前属性是可访问的
name.setAccessible(true);
//3.设置指定对象的此属性值
name.set(p,"Tom");
//4、获取指定对象的此属性值
System.out.println(name.get(p));
调用指定的构造器
Class clazz = Person.class;
//1.获取指定的构造器
//getDeclaredConstructor():参数:指明构造器的参数列表
Constructor constructor = clazz.getDeclaredConstructor(String.class);
//2.保证此构造器是可访问的
constructor.setAccessible(true);
//3.调用此构造器创建运行时类的对象,此时才会进行类加载
Person per = (Person) constructor.newInstance("Tom");
关于setAccessible方法的使用
什么是注解
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
与注释的区别
==注释==:用来解释说明,是给我们程序员看的
==注解==:用来解释说明,是给程序看的
功能
编写文档:通过代码里标识的注解生成文档,如:生成文档doc文档
代码分析:通过代码里标识的注解对代码进行分析,如:使用反射
编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查,如:@Override
使用注解
- 将注解写在类,方法,成员变量的上面即可
- 如果注解有成员(无默认值),那么在使用的时候必须在注解名后指明成员的值
- 如果一个注解中有多个属性的时候,我们在使用注解的时候,要给所有的属性附上值,之间用逗号分隔。
- 在注解中,有一个非常特别的属性名,叫做value,如果一个注解中,只有一个属性,而且这个属性的名字叫做value的话,那我们在使用注解的时候,就可以不写该属性的名字
@注解名
public void show() {}
@注解名
int i;
@注解名
class user {
public void show() {}
}
注解的定义
预定义注解
定义好的注解,常用的有:
@Override:检测是否是方法的重写
@Deprecated:检测方法是否已过时
@SuppressWarnings:压制警告
注意:一般情况下,我们会在@SuppressWarnings中的参数里传递”all”,代表压制所有情况的警告
自定义注解
注解只有成员变量,没有方法体
public @interface 注解名{
public String 属性名();
public String 属性名() default 默认值;
String 属性名() default 默认值;
//注解的权限修饰符可以省略,默认也是public
}
注解的实质
public interface MyAnnotation1 extends java.lang.annotation.Annotation {}
通过反编译可以了解到,注解的本质是默认继承java.lang.annotation.Annotation接口的接口
注解的属性
由于注解本质就是接口,所以在接口可以定义抽象方法,在接口中叫抽象方法,在注解中就叫做属性
在注解中,属性的返回值类型可以写以下几种数据类型:’
- 基本数据类型
- String
- 枚举
- 注解
- 还有以上几种数据类型的数组类型
元注解
用来标记注解的注解
@Retention
用于指定注解可以保留多长时间
value属性值
RetentionPolicy.SOURCE:如果该注解的值为这个,表示被该元注解所标注的注解生存时长只会保留在.java的时候
RetentionPolicy.CLASS:如果该注解的值为这个,表示被该元注解所标注的注解生存时长只保留在.java的时候和字节码文件的时候,但不会进内存中
RetentionPolicy.RUNTIME:如果该注解的值为这个,表示被该元注解所标注的注解生存时长保留在.java的时候和字节码文件的时候,也会进内存JVM中
@Target
指定注解用于修饰哪些程序元素
value属性值
ElementType.TYPE:如果target注解的值为这个,就说明被target注解所标注的注解只能放在类、接口的上面
ElementType.FIELD:如果target注解的值为这个,就说明被target注解所标注的注解只能放在属性的上面
ElementType.METHOD:如果target注解的值为这个,就说明被target注解所标注的注解只能放在方法的上面
@Documented
在使用javadoc命令生成文档后,被该元注解修饰了将会消失
@Inherited
指定注解具有继承性
注解的解析
就是指使用反射技术,获取注解的属性值。
注意:如果想要使用反射来解析注解,前提条件,该注解一定要有元注解@Retention,而且其值一定要为RetentionPolicy.RUNTIME,否则属性不进内存,会有空指针异常
常用API
获取到类名上的注解的属性值
//获取运行时类的Class对象
Class cls = 带有注解的类的类名.getClass();
//判断是否有注解
if(cls.isAnnotationPresent(注解.class)){
//获取注解对象
注解 a = (注解)cls.getAnnotation(注解.class);
//获取属性值
a.属性名();
}
获取到方法上的注解的属性值
//获取运行时类的Class对象
Class cls = 带有注解的类的类名.getClass();
Method m = cls.getMethod("方法名");
//判断是否有注解
if(m.isAnnotationPresent(注解.class)){
//获取注解对象
注解 a = (注解)m.getAnnotation(注解.class);
//获取属性值
a.属性名();
}
类加载器
用来加载.class文件
可以将本地磁盘上的字节码文件加载进JVM的方法区,生成字节码文件对象
类加载的三种方式
//1、由new关键字创建一个类的实例,在由运行时刻用 new 方法载入
Person person = new Person();
//2、使用Class.forName(),通过反射加载类型,并创建对象实例
Class clazz = Class.forName("Person");
Object person =clazz.newInstance();
//3、使用某个ClassLoader实例的loadClass()方法,通过该 ClassLoader 实例的 loadClass() 方法载入。应用程序可以通过继承 ClassLoader 实现自己的类装载器。
Class clazz = ClassLoader.getSystemClassLoader().loadClass("Person");
Object person =clazz.newInstance();
分类
==引导类加载器(根类加载器、启动类加载器,BootstrapClassLoader)==
- 负责加载的区域是jdk中的jre中lib中rt.jar(核心类库),无法调用
==扩展类加载器(ExtClassLoader)==
- 负责加载的区域是jdk中的jre中lib中ext中的资源文件
==系统类加载器(AppClassLoader)==
- 负责加载的区域是classpath路径下的资源文件,默认情况下是项目的bin目录下
- 注意:classpath路径不是固定的,是可以自己设置的
简单理解,就是引导类加载器加载Java的类,扩展类加载器加载导入的外部类,系统类加载器加载自定义类
分层
上层:引导类加载器
中层:扩展类加载器
下层:系统类加载器
加载顺序(双亲委派)
- 类加载器加载存在委托机制,==此机制的目的是防止篡改源码==
- 系统类加载器:任何类一开始都是由系统类加载器来加载,也就说最下层来加载的。但是由于类加载器具备委托机制。所以,它不会马上去自己加载,而是委托给上一层类加载器来加载,即扩展类加载器来加载。如果扩展类加载器加载到了就加载进内存,如果扩展类加载器没有加载到,就自己再去加载,如果它自己也没有加载到,就会报异常ClassNotFoundException
- 扩展类加载器:如果轮到扩展类加载器来加载的话,由于类加载器具备委托机制,它会让上一层类加载器来加载,即引导类加载器来加载,如果引导类加载器加载到了,就进内存,如果引导类加载器加载不到,再由自己来加载,如果自己也没有加载到,就返回到系统类加载器来加载
- 引导类加载器:如果轮到引导类加载器来加载的话,由于它没有上一层,所以自己来加载,如果加载到,就进内存,如果它自己没有加载到,就会返回到扩展类加载器来加载
- 扩展类加载器和系统类加载器由于也是类,也是JDK中提供的类,所以是由引导类加载器来加载器
- 引导类加载器没有谁来加载,不是一个类,是JVM的一部分,是由C语言编写的
相关API
获取类加载器对象
1.先获取类的字节码文件对象
Class clazz = Person.class;
2.通过字节码文件对象获取类加载器对象
ClassLoader classLoader = clazz.getClassLoader();
3.获取类加载器的上一层类加载器
ClassLoader classLoader1 = classLoader.getParent();
使用类加载器来读取配置文件
原始方法,通过Properties类API进行读取
// 在src的路径下,有一个jdbc.properties的配置文件
Properties p = new Properties();
p.load(new FileInputStream("src/jdbc.properties"));
String driver = p.getProperty("driver");
System.out.println(driver);
类加载器读取
方式一:
// 获取类的字节码文件对象
Class clazz = Demo2.class;
// 获取类加载器的对象
ClassLoader classLoader = clazz.getClassLoader();
// 使用类加载器对象读取配置文件
InputStream is = classLoader.getResourceAsStream("jdbc.properties");
Properties p = new Properties();
p.load(is);
String driver = p.getProperty("driver");
System.out.println(driver);
//注意:使用类加载器的方式读取配置文件,默认的根目录是相对于classpath目录
方式二:
// 获取类的字节码文件对象
Class clazz = Demo1.class;
// 获取类加载器对象
ClassLoader classLoader = clazz.getClassLoader();
// 使用类加载器对象读取配置文件
URL url = classLoader.getResource("jdbc.properties");
String path = url.getPath();
FileInputStream fis = new FileInputStream(path);
Properties p = new Properties();
p.load(fis);
String driver = p.getProperty("driver");
System.out.println(driver);
/注意:使用类加载器的方式读取配置文件,默认的根目录是相对于classpath目录
原创文章,作者:,如若转载,请注明出处:https://blog.ytso.com/274548.html