卞建彬:
day03 List Set
数据结构
数据结构: 就是数据的存储方式
不同的数据结构代表了不同的存储方式
不同的数据结构中, 会有不同的存入, 取出, 查找的方式和效率, 数据的存放方式也不同
栈
栈的特点:
先进后出 (FILO, First In Last Out)
入口和出口在同一侧
入栈(压栈): 将元素存入栈
出栈(弹栈): 从栈中取出元素
栈的适用场景:
栈内存 (main方法先进栈调用, main方法中的其他方法都调用完毕后, main才能出栈)
反转内容
队列
队列的特点:
先进先出 (FIFO, First In First Out)
入口和出口在两端
队列的适用场景:
秒杀, 抢购
在线售票
处理高并发场景
数组
数组的特点:
查询快: 通过 第一个元素地址值 + 索引 可以快速计算出该索引元素的地址值
增删慢: 增加一个元素, 要创建长度+1的新数组, 然后将原数组元素复制到新数组, 然后存入新元素; 删除类似
数组的适用场景:
查询多, 增删少的数据存储场景 链表
链表: 链表由多个 节点(Node / Entry) 组成
单向链表: 每个节点存储 数据 和 下一个节点的地址值
双向链表: 每个节点存储 数据, 上一个节点地址值 和 下一个节点地址值
链表的特点:
查询慢: 要找到其中某个节点, 只能从第一个节点一个一个向后寻找
增删快: 只需要修改保存的下一个节点的地址值, 就可以快速完成增删
链表的适用场景:
查询少, 增删多的场景
链表可以实现栈和队列的结构, 因为栈和队列增删频繁
代码中链表节点的实现:
// 单向链表的节点
class Node<T> {
T data; // 存储的数据
Node next; // 下一个节点的地址值
}
// 双向链表的节点
class Node<T> {
T data; // 存储的数据
Node before; // 上一个节点的地址值
Node after; // 下一个节点的地址值
}
红黑树
红黑树: 是一种 平衡 二叉 查找 树
平衡: 左子节点和右子节点数量相等
二叉: 每个节点最多有2个子节点
查找: 节点存储的元素是按照大小顺序存储的
特点:
元素存储过程中就完成了大小排序
查询比链表快, 增删比数组快 (数组和链表的折中)
红黑树的适用场景:
查询和增删都有, 需要元素自动排序的场景
代码中树节点的实现
// 树的节点
class Entry<T> {
T data; // 存储的数据
Entry left; // 左子节点的地址值
Entry right; // 右子节点的地址值
Entry parent; // 父节点的地址值
}
List集合
List介绍和常用方法 List集合体系的特点:
1. 元素存取有序 (存入和取出元素的顺序一致) 2. 元素可以重复 3. 有索引
List子体系中的实现类都具有上述特点
java.util.List接口:
// 常用特有成员方法 (都是按照索引来操作的)
void add(int index, E element): 将指定的元素, 添加到该集合中的指定位置上
E get(int index): 返回集合中指定位置的元素
E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素
E set(int index, E element): 用指定元素替换集合中指定位置的元素, 返回值的更新前的元素
ArrayList集合的特点 ArrayList底层的数据结构:
数组
ArrayList的特点:
查询快
增删慢
线程不安全, 效率高
ArrayList适用场景:
存储的数据"查询多, 增删少"的场景.
LinkedList集合的特点和特有方法
LinkedList底层的数据结构:
链表
LinkedList的特点:
查询慢
增删快
线程不安全, 效率高
LinkedList适用场景:
存储的数据"查询少, 增删多"的场景.
java.util.LinkedList<E>类: 链表结构, 查询慢, 增删快
// 特有成员方法(主要操作开头和末尾元素)
void addFirst(E e): 将指定元素插入此列表的开头
void addLast(E e): 将指定元素添加到此列表的结尾
E getFirst(): 返回此列表的第一个元素
E getLast(): 返回此列表的最后一个元素
E removeFirst(): 移除并返回此列表的第一个元素
E removeLast(): 移除并返回此列表的最后一个元素
E pop(): (其实就是removeFirst())从此列表所表示的栈中弹出一个元素
void push(E e): (其实就是addFirst())将元素添加到此列表所表示的栈中
Vector集合
JDK 1.0 版本中只有一个 Vector集合
JDK 1.2 开始增加了Collection集合体系
Vector底层的数据结构:
数组
Vector的特点:
查询慢
增删快
(同步)线程安全, 效率低
Vector目前几乎没人使用
Set集合体系
HashSet集合 Set集合体系特点:
1. 元素不可重复
2. 没有索引
HashSet特点:
1. 元素不可重复
2. 没有索引
3. 元素存取无序 (存入和取出顺序有可能不一致)
4. 底层采用 哈希表 结构. (查询快)
哈希表 = 数组 + 链表或红黑树
java.util.HashSet类:
// 常用方法
boolean add(E e): 添加元素, 根据元素的 hashCode() 和 equals() 方法判断是否重复. 重复则不添加并返回false, 不重复则添加并返回true
HashSet原理: 哈希值 哈希值:
一个十进制数值, 一般是通过将该对象的内部地址转换成一个整数来实现的
public native int hashCode();
可以调用系统本地代码(C/C++)计算出一个对象地址的哈希值
hashCode()方法的作用
方法内部的算法用于将对象计算为一个哈希值, 便于根据哈希值比较对象是否"相等"
哈希值主要是为了提高对象存储在 哈希表 中的效率
注意:
1. 如果我们不满意Object中的哈希值计算方法, 可以重写hashCode()方法.
但在Java代码中不能直接重写带有 native 的方法, 重写时应该将 native 去掉
@Override
public int hashCode() {}
2. hashCode() 方法有可能将"不同的对象"计算出"相同的哈希值", 这称为"哈希冲突", 在出现冲突后, 一般再通过 equals() 方法来继续判断对象是否"相等"
"重地" "通话"
HashSet原理: 哈希表结构
哈希表:
JDK 8以前 : 哈希表 = 数组 + 链表
JDK 8及之后 : 哈希表 = 数组 + 链表或红黑树
数组中存储的每个元素, 是哈希值相同的一组节点的链表或红黑树
HashSet原理: 存储元素不重复的原理
HashSet集合保证添加元素不重复的原理:
调用 add(E e) 添加元素时, 先调用 hashCode() 获取哈希值, 和当前HashSet集合中的元素比较
如果哈希值不同, 则认为元素不重复, 添加, 并返回true
如果哈希值相同, 则有可能是哈希冲突, 所以继续调用元素的 equals() 方法和所有哈希值相同的元素比较
如果 equals() 比较所有元素都没有相同的, 则认为元素不重复, 添加, 并返回true
如果 equals() 比较出有相同的元素, 则认为元素重复, 不添加, 并返回false
HashSet存储自定义元素的去重
自定义JavaBean对象实现在HashSet中去重:
JavaBean默认继承Object类中的 hashCode() 和 equals() 方法, 都是根据对象地址值判断是否重复的
要根据属性值判断是否重复, 应该在JavaBean类中重写 hashCode() 和 equals() 方法, 使其按照属性值比较
LinkedHashSet集合
LinkedHashSet特点:
1. 元素存取有序 (存入和取出顺序一致)
2. 元素不可重复
3. 没有索引
LinkedHashSet底层数据结构:
哈希表 + 链表 (也就是: 数组 + 链表或红黑树 + 链表)
其中, 哈希表用于存储数据, 额外的链表用于记录元素添加时的先后顺序, 以便在获取元素时保持顺序一致
总结: 什么时候用List, 什么时候用Set?
要存储的元素可以重复的, 用List集合:
增删少, 用ArrayList
增删多, 用LinkedList
要存储的数据要求不重复, 或者相对一个集合去重, 用Set集合:
不要求存取顺序一致, 用HashSet
要求存取顺序一致, 用LinkedHashSet
可变参数
可变参数:
JDK 5 出现. 指同一个类型参数个数可变的参数
可变参数的本质就是一个"数组"
格式: 用在方法的参数中
修饰符 返回值类型 方法名(int... 变量名) {
// 可以直接将 变量名 当作 数组名 使用
}
方法名();
注意事项:
1. 可变参数可以传递的参数个数, 可以是 0个, 1个, 多个
2. 一个方法的参数列表中, 只能有一个可变参数
3. 如果方法的参数有多个, 可变参数必须写在参数列表的最后
Collections集合工具类
Collections集合工具类: addAll(), shuffle()
java.util.Collections类: 操作集合的工具类
// 静态方法
static <T> boolean addAll(Collection<? super T> c, T... elements):往集合中添加一些元素
static void shuffle(List<?> list): 打乱集合顺序
static <T> void sort(List<T> list): 将集合中元素按照默认规则排序
static <T> void sort(List<T> list,Comparator<? super T> c):将集合中元素按照指定规则排序
Collections集合工具类: sort(List<T> list)
sort(List<T> list): 默认按照"升序"将元素排序
数字, 字母, 都可以按照升序排序
自定义JavaBean对象默认不能排序, 因为不知道如何比较哪个对象大, 哪个对象小
自定义JavaBean对象要想排序, 需要实现 Comparable<E> 接口, 重写 int compareTo(E e) 方法
规则:
this-参数: 升序(从小到大)
参数-this: 降序(从大到小)
Collections集合工具类: sort(List<T> list,Comparator<? super T> )
Comparable接口和Comparator接口区别
Comparable: 让JavaBean自身具有可比较性 (自己和其他人比)
Comparator: 定义一个比较器类, 用比较器对象比 (让第三个人来帮两个人比较)
Comparator使用方式:
定义类作为比较器, 实现Comparator<E>接口, 重写 int compare(E o1, E o2) 方法, 泛型为要比较的元素的类型
在Collections.sort(List<T> list,Comparator<? super T> c)方法中传入自定义比较器对象
规则:
o1-o2: 升序(从小到大)
o2-o1: 降序(从大到小)
day04 Map集合 Map体系
Map集合概述
java.util.Map<K, V>接口
Map集合特点:
1. 是双列集合, 一个元素包含两个值 (键key, 值value) 键值对
2. key和value的类型可以相同, 也可以不同
3. key不允许重复, value可以重复
4. key和value是一一对应的, 一个键只能对应一个值
Map集合适合存储"一对一关系"的数据
public class Node<K, V> {
K key;
V value;
}
Map常用子类
java.util.Map<K, V>接口: 双列集合的根接口, 规定了共性的方法
|
|_ HashMap<K, V>类: 底层哈希表. key存取无序不可重复
|_ LinkedHashMap<K, V>类: 底层哈希表+链表. key存取有序不可重复
映射: 键和值的对应关系 mapping
HashSet底层使用的就是HashMap
LinkedHashSet底层使用的就是LinkedHashMap
Map中常用方法
java.util.Map接口: 双列集合的顶层
// 成员方法
V put(K key, V value): 添加/修改键值对. 如果键存在, 则用新值替换已有值
V remove(Object key): 根据键删除键值对, 返回被删除元素的值
V get(Object key): 根据键获取值. 如果键不存在, 则返回null
boolean containsKey(Object key): 判断是否包含指定的键
Set<K> keySet(): 获取Map集合中所有的键, 存储到Set集合中
Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的Entry对象的集合(Set集合)
public class Entry<K,V> { // 键值对 对象
K key;
V value;
}
Map遍历方式一: keySet()方法实现通过键找值 Set<K> keySet(): 获取Map集合中所有的键, 存储到Set集合中
keySet()遍历步骤:
1. Map对象调用 keySet() 方法, 获取包含所有key的Set集合
2. 遍历Set集合, 获取每个key
3. 通过Map对象调用 get(Object key) 方法根据key获取到value
Entry键值对对象介绍 java.util.Map.Entry接口:
// 常用成员方法
K getKey(): 获取Entry对象中的键
V getValue(): 获取Entry对象中的值
Entry对象就是一个节点, 节点中存储了key和value
拿到一个Entry对象就可以从中获取key和value
java.util.Map接口
Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)
Map遍历方式2: 通过entrySet()获取Entry对象形式遍历 java.util.Map接口
Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)
entrySet()方法遍历Map步骤:
1. Map对象调用 entrySet() 获取包含所有Entry对象的Set集合
2. 遍历Set集合, 获取每个Entry对象
3. 调用Entry对象的 getKey() 和 getValue() 方法获取键和值
HashMap存储自定义数据类型作为键
HashMap存储自定义JavaBean对象作为key保证key唯一不重复, 需要让JavaBean重写 hashCode() 和 equals() 方法
LinkedHashMap类
LinkedHashMap底层: 哈希表 + 链表
key不允许重复, 但key存取有序
LinkedHashSet的底层就是使用LinkedHashMap实现的
HashSet HashMap
Hashtable类
Hashtable和HashMap:
相同点:
1. 底层都是哈希表
不同点:
1. Hashtable不允许存储 null 值和 null 键; HashMap允许存储 null 值和 null 键
2. Hashtable线程安全效率低; HashMap线程不安全效率高
Hashtable目前很少使用.
但其子类 Properties 集合, 可以与IO流结合使用, 应用较多
JDK9对集合添加的优化
使用集合添加大量元素时, 反复add(...)比较麻烦. JDK 9 为集合提供了一些静态方法, 可以方便的对集合进行初始化
java.util.List
// 静态方法
static List<E> of(E... e): 返回包含指定元素的 不可变List 集合
java.util.Set
// 静态方法
static Set<E> of(E... e): 返回包含指定元素的 不可变Set 集合
java.util.Map
// 静态方法
static Map<K, V> of(K k1, V v1, ...): 返回包含指定键值对的 不可变Map 集合
注意事项:
1. of() 方法只适用于List接口, Set接口, Map接口, 不适用于接口的实现类
2. of() 方法的返回值是一个不可变的集合, 集合不能再使用 add(), put() 方法添加元素, 会抛出异常
3. Set接口和Map接口在调用 of() 方法的时候, 不能有重复的元素, 否则会抛出异常
将 不可变集合 的元素转移到常见集合实现类中:
//List
ArrayList<Integer> list = new ArrayList<>(List.of(1,2,3));
//Set
HashSet<Integer> set = new HashSet<>(Set.of(1,2,3));
// Map
HashMap<Integer, String> map = new HashMap<>(Map.of(1,"a",2,"b"));
Debug调试模式介绍
断点:
breakpoint, 在debug模式下, 程序运行到断点会暂停住, 便于我们在程序运行过程中查看
Debug调试程序:
可以让代码逐行执行,查看代码执行的过程,调试程序中出现的bug
使用方式:
在行号的右边,鼠标左键单击,添加断点(每个方法的第一行,哪里有bug添加到哪里)
右键,选择Debug执行程序
程序就会停留在添加的第一个断点处
执行程序:
f8:逐行执行程序
f7:进入到方法中
shift+f8:跳出方法
f9:跳到下一个断点,如果没有下一个断点,那么就结束程序
ctrl+f2:退出debug模式,停止程序
Console:切换到控制台
day05 异常 多线程 异常
异常的概念和体系
异常: 指的是程序在执行过程中, 出现的非正常的情况, 最终会导致JVM的非正常停止
注意:
在Java中, 每种异常都有对应的类来描述, 发生了一个异常, 就是出现了这个异常类的"对象"
异常不是语法错误
// 异常的体系结构
java.lang.Throwable // 体系最顶层
|_ Error // 不应该试图捕获的严重问题, 不能处理的错误
|_ Exception // 可以处理的异常
// 异常信息详解:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
at java.util.HashMap$KeyIterator.next(Unknown Source)
at com.itheima.practice_08.MapTest.main(MapTest.java:31)
解释:
Exception in thread "main": 在名为"main"的线程发生异常
java.util.ConcurrentModificationException: 异常的具体类型, 这里是并发修改异常
at xxx.xxx.xxx: 异常发生的位置, 因为层层方法调用, 所以会出现多个类的方法, 这些方法组成了异常链
只要不是我们的包名, 一般不用看. 是我们的包名, 才看
at com.itheima.practice_08.MapTest.main(MapTest.java:31):
说明这个异常, 发生在com.itheima.practice_08下的MapTest类的main方法中, 具体是MapTest.java文件中的第31行
一般 (MapTest.java:31) 这一部分是可以鼠标点击的, 点击后就直接跳转到报错的这一行
异常的分类
// 异常的体系结构
java.lang.Throwable // 体系最顶层
|_ Error // 错误
|_ Exception // 编译时异常
|_ RuntimeException // 运行时异常
错误(Error): 不能捕获处理的严重问题.
必须将程序停下来, 修改代码才能解决
错误的类名都是 "XxxError" 方式
异常(Exception): 可以捕获处理的问题.
程序执行起来后, 如果有合适的处理方式, 即使发生异常, 程序也能处理该异常并继续运行
异常的类名都是 "XxxException" 方式
1. 编译时异常:
编译时期就会发生的异常, 必须在编译时期处理
2. 运行时异常:
编译时正常, 运行时才会发生的异常
异常的产生过程解析
异常是如何产生的
异常产生的过程:
1. 当执行的代码发生异常操作时, JVM会创建一个对应的异常类对象, 包含异常的内容, 原因, 位置
2. 如果执行代码的方法没有对异常进行 try...catch 处理, 则会向该方法调用处的方法抛(向上层抛). 如果所有方法(包括main()方法)都没有 try...catch 处理异常, 则该异常会被JVM按照默认的处理方式处理
3. JVM对于异常的默认处理方式是:
1. 将异常信息(内容, 原因, 位置)打印到控制台
2. 终止当前的程序
异常关键字: throw制造异常
throw 关键字有什么作用
throw 制造一个异常的语法格式是怎样的
throw关键字作用:
在方法中抛出指定的异常对象
格式:
throw new 异常类名("异常原因字符串");
注意:
1. throw 必须写在方法的内部
2. throw 后面new的异常对象, 必须是 "Exception" 或 "Excetion的子类" 的对象
3. 一个方法内部 throw 了一个异常对象, 则该方法可以分为2种情况来处理该异常:
如果 throw 的是"运行时异常"(RuntimeException及其子类)对象, 那么可以不处理
该异常最终会交给JVM处理, 结果就是: 打印异常信息到控制台, 并立刻结束程序
如果 throw 的是"编译时异常"(Exception及其子类), 则必须处理:
处理方式1: throws 抛出
处理方式2: try...catch 捕获
Objects工具类方法: requireNonNull()非空判断
java.util.Objects: 操作各种对象相关的工具类
// 静态方法
static <T> T requireNonNull(T obj): 检查传入参数是不是null. 是null则抛异常; 非null则返回该对象
static <T> T requireNonNull(T obj, String message): 检查传入参数是不是null. 是null则抛异常, 同时带有原因; 非null则返回该对象
可以用来简化代码的编写:
// 普通方式
if (obj == null) {
throw new NullPointerException();
}
// Objects.requireNonNull(T obj)方式
Objects.requireNonNull(obj);
异常的处理方式1: throws声明抛出异常
提前了解IO用到的一些异常类名:
FileNotFoundException: 文件找不到时抛出的异常
IOException: 只要是IO操作的异常都属于该异常, 是FileNotFoundException的父类
异常处理的第一种方式:
throws, 声明抛出 (方法自己不处理, 交给方法调用者处理, "甩锅给别人")
作用: 告诉方法的调用者, 调用此方法有可能发生异常, 需要在编写代码时处理
格式:
修饰符 返回值类型 方法名() throws 异常类名1, 异常类名2, ... {
}
注意:
1. throws 必须写在方法声明上
2. throws 后面的异常类名, 一般是 Exception 或 Exception的子类
(RuntimeException及其子类也行, 但是没有什么意义)
3. 方法内部如果抛出了多个异常对象, throws 后也必须声明多个异常
如果抛出的异常对象有子父类关系, 那么直接声明父类异常即可
4. 调用了一个带有 throws 声明抛出异常的方法, 就必须处理该异常:
要么继续声明 throws 抛出
要么 try...catch 捕获处理异常
异常处理方式2: 捕获异常
try...catch:
捕获并处理异常 (方法内部自己处理异常, 不交给别人, "自己背锅")
格式:
try {
// 可能产生异常的代码
} catch (异常类名 变量名) {
// 处理异常的代码
// 一般会将异常信息存储到日志中
}
...
} catch (异常类名 变量名) {
// 处理异常的代码
// 一般会将异常信息存储到日志中
}
注意:
1. try 中可能会抛出多种异常, 就可以写多个 catch 分别处理每种异常
2. 如果 try 中产生了异常, 就会从产生异常的那一行代码直接跳转到对应的 catch 中执行处理代码, 然后继续执行 try...catch 之后的其他代码; 如果 try 中没有产生异常, 那就不会执行 catch , 执行完 try 中的代码后, 继续执行 try...catch 之后的其他代码
// JDK 7增加
try {
// 可能发生异常的代码
} catch (异常类型1 | 异常类型2 | 异常类型3 | ... 异常对象名) {
// 处理任意一个异常的代码
}
try {
// 可能发生异常的代码
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
// 处理异常的代码
}
Throwable中的3个异常处理方法
java.lang.Throwable
// 成员方法
String getMessage(): 异常的信息. 没有原因返回null
String toString(): 异常的类型和原因信息
void printStackTrace(): 使用标准错误输出流打印异常信息
finally代码块 格式:
try {
// 可能发生异常的代码
} catch(异常类型 异常变量名) {
// 处理异常
}
...
catch(异常类型 异常变量名) {
// 处理异常
} finally {
// 无论是否发生异常, 是否捕获, 最后都会执行的代码.
// 通常在这里执行释放资源的操作
}
注意:
1. finally 必须和 try...catch 一起使用
2. finally 一般用于释放资源 (IO流时用到)
异常注意事项1: 捕获多个异常的3种方式
捕获多个异常:
1. 多个异常分别 try...catch 处理
2. 一个 try 多个 catch
如果异常存在继承关系, 子类异常在上, 父类异常在下
3. 多个异常, 一次捕获一次处理
用Exception多态捕获
//1. 多个异常分别try...catch处理
try {
// 可能发生异常的代码
} catch (异常类型 变量名) {
// 处理异常
}
try {
// 可能发生异常的代码
} catch (异常类型 变量名) {
// 处理异常
}
//2. 一个try多个catch
try {
// 可能发生异常的代码
// 可能发生异常的代码
// 可能发生异常的代码
}catch (异常类型1 变量名){ // 子类异常在上
// 处理异常1
}catch (异常类型2 变量名){ // 父类异常在下
// 处理异常2
}
//3.
try {
// 可能发生异常的代码
// 可能发生异常的代码
// 可能发生异常的代码
// 可能发生异常的代码
// 可能发生异常的代码
}catch (Exception e){ // 父类异常多态接收
// 处理异常
}
运行时异常, 可以不抛出 throws 也不捕获 try catch, 交给JVM处理
异常注意事项2: finally中有return语句
如果 finally 代码块中有 return 语句, 则永远返回 finally 中的 return 语句的值
应该避免在 finally 中写 return 语句
异常注意事项3: 子父类继承重写方法时的异常要求
子父类继承关系下, 子类重写父类带有throws的方法:
1. 如果父类抛出多个异常, 子类重写父类方法时可以有3种方式:
a: 抛出和父类相同的异常
b: 抛出父类异常的子类
c: 不抛出异常
2. 父类方法没有抛出异常, 子类重写父类该方法时也不可抛出异常
此时子类产生该异常, 只能捕获处理, 不能声明抛出
一般情况下:
父类方法声明的异常是什么样的, 子类重写的方法声明异常就什么样, 保持一致即可
自定义异常类
如果Java提供的异常类不足以满足我们的需求, 我们也可以自己定义异常类
定义编译时异常: 继承 Exception
定义运行时异常: 继承 RuntimeException
为了在抛出异常时, 显示一些提示信息, 我们可以定义2个构造, 一个无参, 一个有String参数
多线程
计算机基本概念: 并发与并行
并发: (交替执行) 指两个或多个事件在"同一时间段内"发生
并行: (同时执行) 指两个或多个事件在"同一时刻"发生 (同时发生)
我们研究的是"并发"
并发的实现, 是依靠电脑CPU快速地在多个任务之间切换执行实现的
计算机基本概念: 进程
进程: 一个应用程序在内存中的一次执行过程
每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程
进程也是程序的一次执行过程,是系统运行程序的基本单位
系统运行一个程序即是一个进程从创建、运行到消亡的过程
计算机基本概念: 线程
线程: 是进程内的一个独立执行单元 (一条代码执行路径)
一个程序运行后至少有一个进程, 一个进程中可以包含多个线程
公司 > 部门 > 员工
程序 > 进程 > 线程
多线程的好处:
效率高
多个线程之间互不影响
线程的调度 多个线程要并发执行, 哪个线程被执行, 哪个线程不被执行, 就是调度
线程的调度方式:
1. 分时调度: 所有线程轮流使用CPU, 平分占用CPU的时间
2. 抢占式调度: 优先让优先级高的线程使用CPU; 如果优先级相同, 则随机选择一个线程执行
Java使用的是"抢占式"调度
主线程
Exception in thread "main"
主线程:
我们以前编写的代码, 也在一条线程中执行, 该线程叫作"main线程", 也称为"主线程"
如果我们没有额外创建线程, 那么我们的程序就只有一个线程, 即主线程, 此时程序是"单线程"的
单线程的执行特点:
同一个线程内的代码, 从上往下依次执行
创建多线程程序的第一种方式: 继承Thread类
实现多线程的第一种方式:
1. 定义类, 继承 Thread 类
2. 重写 run() 方法, run方法内部是线程要执行的任务
3. 创建Thread子类的对象, 调用 start() 方法启动线程
java.lang.Thread类: 表示线程. 实现了Runnable接口
void start(): 启动线程, 即让线程开始执行run()方法中的代码
注意:
必须调用 start() 方法来开启线程, 不能直接调用 run() 方法, 调用 run() 会变成单线程
同一个线程对象, 不能多次调用 start() 方法
Java是抢占式调度, 不同线程的代码, 执行顺序是随机的
day06 线程 同步 线程间通信
线程
多线程原理1: 线程执行的随机性
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权
CPU高速随机切换 (本质)
多线程原理2: 多线程的内存
多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈
栈是每个线程各自的, 堆是所有线程共用的
Thread常用方法:
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
Thread(): 创建Thead对象
Thread(String threadName): 创建Thead对象并指定线程名
Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
// 成员方法
void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
void start(): 启动线程, 即让线程开始执行run()方法中的代码
String getName(): 获取线程的名称
void setName(String name): 设置线程名称
// 静态方法
static Thread currentThread(): 返回对当前正在执行的线程对象的引用
static void sleep(long millis): 让所在线程睡眠指定的毫秒
创建多线程程序的方式2: 实现Runnable接口
创建线程的第2种方式:
1. 定义类, 实现Runnable接口
2. 重写 run() 方法, 要执行的代码(任务)
3. 创建Runnable实现类对象 (任务对象)
4. 创建Thread类对象, 在构造方法中传入Runnable实现类对象 (将任务和线程绑定)
5. 通过Thread对象调用start()方法启动线程
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
Thread和Runnable的区别
实现Runnable的好处:
1. 避免单继承的局限性
2. 增强了程序的扩展性, 降低了程序的耦合性(解耦)
线程是Thread, 任务是Runnable实现类对象. 相当于将线程和任务分离
耦合性: 相互之间的关系的紧密程度
耦合性高: 相互之间的关系非常紧密
耦合性低: 相互之间的关系不太紧密
我们追求 "低耦合"
匿名内部类方式创建线程
线程安全问题
模拟电影院卖票: 线程安全问题概述
模拟电影院卖票: 代码实现
多个卖票窗口 : 多个Thread对象
卖票任务 : Runnable实现类, 重写run()方法
共享的票 : 因为多个Thread对象使用同一个Runnable实现类对象, 所以将票数定义为Runnable实现类的成员变量
线程安全问题的原因
问题发生场景:
多个线程操作共享资源
问题发生原因:
JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题
解决线程安全问题方式1: 同步代码块
解决多线程操作共享数据的安全问题的3种方式:
1. 同步代码块
2. 同步方法
3. Lock锁机制
同步代码块: 使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
synchronized (锁对象) {
// 操作共享数据的代码
}
注意:
锁对象可以是"任意类型的一个对象"
锁对象必须是"被多个线程共享的唯一的"对象
锁对象的作用: 只让一个线程在同步代码块中执行
同步技术解决线程安全问题的原理
锁对象, 也称为"同步锁", "对象监视器"
同步的原理:
线程进入同步代码块前, 会争夺锁对象, 只有一个线程会抢到锁对象
进入同步代码块的线程, 会持有锁对象, 并执行同步代码块中的代码
此时同步代码块外的线程, 处于阻塞状态, 只能等待
当同步代码块内的线程执行完代码块, 会离开同步代码块, 并归还锁对象给同步代码块
等在同步代码块外的其他线程就可以继续争夺锁对象
解决线程安全问题方式2: 同步方法
同步方法: 使用 synchronized 关键字修饰的方法, 具有默认的锁对象
非静态同步方法的锁对象: this
// 非静态同步方法
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
静态同步方法
静态同步方法:
public static synchronized void method(){
// 可能会产生线程安全问题的代码
}
静态同步方法的锁对象: 当前类的字节码对象 Class对象
获取一个类的字节码对象的3种方式:
1. 对象名.getClass()
2. 类名.class
3. Class.forName("类的全名");
字节码对象的特点:
同一个类, 他的字节码对象只有"唯一的一个"
锁对象必须是多个线程共享的同一个对象
解决线程安全问题方式3: Lock锁
java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
// 成员方法
void lock(): 获取锁
void unlock(): 释放锁
java.util.concurrent.locks.ReentrantLock类: Lock的实现类
使用方式:
public class RunnableImpl implements Runnable {
// 成员变量创建锁对象, 该锁对象也要所有线程共享唯一一个
Lock lock = new ReentrantLock();
@Override
public void run() {
// 加锁
lock.lock();
try {
// 操作共享变量的代码...
} finally {
// 在finally中保证释放锁
lock.unlock();
}
}
}
线程安全和效率的特点:
线程安全, 效率低
线程不安全, 效率高
线程间的通信
线程状态概述
Object类中关于线程的方法:
java.lang.Object类:
// 成员方法 (<<注意>>: 只能通过"锁对象"调用)
void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(): 让当前线程处于"无限等待"状态
void wait(long timeout): 让当前线程处于"计时等待"状态, 时间到或被唤醒后结束此状态
void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
"注意!! 以上方法只能通过锁对象调用"
线程的生命周期中, 可以出现有6种状态:
1. "NEW 新建"
线程被创建, 但没有调用 start() 启动
2. "RUNNABLE 可运行"
调用 start()方法后已启动, 但可能正在执行 run() 方法的代码, 也可能正在等待CPU的调度
3. "BLOCKED (锁)阻塞"
线程试图获取锁, 但此时锁被其他线程持有
4. "WAITING 无限等待"
通过锁对象调用无参的 wait() 进入此状态.
等待其他线程通过锁对象执行 notify() 或 notifyAll() 才能结束这个状态
5. "TIMED_WAITING 计时等待"
如通过锁对象调用有参的 wait(long millis) 或 sleep(long millis), 则进入此状态.
直到时间结束之前被其他线程通过锁对象执行 notify()或 notifyAll()唤醒, 或时间结束自动唤醒
6. "TERMINATED 终止"
run()方法结束(执行结束, 或内部出现异常), 则进入此状态
等待唤醒案例: 生产者消费者问题需求分析
老板(生产者)是一个线程, 该线程的任务是: 生产包子, 然后通知顾客来吃
顾客(消费者)是另外一个线程, 该线程的任务是: 消费包子, 然后让老板做
两个线程之间, 必须顾客要包子, 然后老板做包子, 然后顾客吃包子, 这是相互协作的场景, 所以线程之间要相互进行通信, 告知彼此应该谁来做事
wait() 和 sleep() 的区别:
1. wait会释放锁, 恢复时需要重新获取锁; sleep不会释放锁
2. wait可以被notify/notifyAll唤醒; sleep不会
3. wait要用锁对象调用; sleep要用Thread类名调用
________________________________________
|