# 1. 对象和封装
# 构造方法
1. 构建创造对象的方法
Student stu = new Student() // 创建对象 |
2. 特点
1.无返回值
2.方法名与类名相同
3. 主要作用
完成数据初始化
//类中有一个默认的无参构造方法
//加了构造方法后,默认的构造方法就不存在
方法重载
1.同一个类
2.方法名相同
3.参数列表不同 【个数、类型、顺序】
4.与返回类型无关,访问修饰符无关
this 关键字
1.当前对象
2.区分成员变量与局部变量
3.调用已定义好的构造方法 【必须在构造方法的第一行】
# 访问修饰符
public:公共的,允所有类访问;
private:私有的,仅允许本类访问
protected:受保护的,允许同一包或子包下所有类访问;
default:默认修饰符,允许同一包下所有类访问。
#
# 封装
1. 作用
- 保护数据的合理性
2. 步骤
- 属性私有化
- 生成 setter/getter 方法
- 添加逻辑判断
# static 关键字
- 可以修饰属性与方法,称为类方法类变量
- 多个对象共用一个属性
# 继承 [extends]
- 解决代码冗余问题
- 提高代码重用性
# 父子类概念
单根继承 一个类只有一个直接父类 [is a]
当没有明确继承指定的父类时,默认继承了超级父类 Object
继承是具有传递性的
不能继承父类的哪些内容?
私有的 不同包默认访问修饰符的 构造方法
继承后 创建子类对象执行顺序
父属性 --> 父代码块 --> 父类构造方法 --> 子属性 --> 子代码块 --> 子类构造方法
# super 指向父类对象
- super () 指定调用父类的构造方法
- super. 属性 super. 方法 【区分子类有同名属性和方法】
- 当父类没有无参构造方法时,子类够着方法中必须
# 重写的特征:
- 相同的方法名
- 参数列表相同
- 返回值类型相同或是其子类
- 不能缩小访问修饰符权限
# 重写后 子类对象调用问题
- 先找自己本类,没有找父类
重写就是为了实现自己所特有的逻辑操作
# Object java 的根类
- equals
- toString
访问控制修饰符 | 同一类 | 同一个包 | 子类 | 不同包 |
---|---|---|---|---|
public (公共的) | ✓ | ✓ | ✓ | ✓ |
protected (受保护的) | ✓ | ✓ | ✓ | ✕ |
default (默认修饰符) | ✓ | ✓ | ✕ | ✕ |
private (私有的) | ✓ | ✕ | ✕ | ✕ |
# 多态
# 1. 概念和特点
多态顾名思义就是多种形态,是指对象能够有多种形态。在面向对象中最常用的多态性发生在当父类引用指向子类对象时。在面向对象编程中,所谓多态意指相同的消息给予不同的对象会引发不同的动作。换句话说:多态意味着允许不同类的对象对同一消息做出不同的响应。
例如,火车类和飞机类都继承自交通工具类,这些类下都有各自的 run () 方法,交通工具的 run () 方法输出交通工具可以运输,而火车的 run () 方法输出火车会跑,飞机的 run () 方法则输出飞机会飞,火车和飞机都继承父类的 run () 方法,但是对于不同的对象,拥有不同的操作。
任何可以通过多个 IS-A 测试的 Java 对象都被视为多态的。在 Java 中,所有 Java 对象都是多态的,因为任何对象都能够通过 IS-A 测试以获取其自身类型和 Object 类。
# 2. 实现多态
#
2.1 实现条件
在 Java 中实现多态有 3 个必要条件:
满足继承关系
要有重写
父类引用指向子类对象
2.1 实例
例如,有三个类 Pet、Dog、Cat:
父类 Pet:
class Pet { | |
// 定义方法 eat | |
public void eat() { | |
System.out.println("宠物吃东西"); | |
} | |
} |
子类 Dog 继承 Pet
class Dog extends Pet { // 继承父类 | |
// 重写父类方法 eat | |
public void eat() { | |
System.out.println("狗狗吃狗粮"); | |
} | |
} | |
子类Cat继承Pet | |
class Cat extends Pet { // 继承父类 | |
// 重写父类方法 eat | |
public void eat() { | |
System.out.println("猫猫吃猫粮"); | |
} | |
} |
在代码中,我们看到 Dog 和 Cat 类继承自 Pet 类,并且都重写了其 eat 方法。
现在已经满足了实现多态的前两个条件,那么如何让父类引用指向子类对象呢?我们在 main 方法中编写代码:
public void main(String[] args) { | |
// 分别实例化三个对象,并且保持其类型为父类 Pet | |
Pet pet = new Pet(); | |
Pet dog = new Dog(); | |
Pet cat = new Cat(); | |
// 调用对象下方法 | |
pet.eat(); | |
dog.eat(); | |
cat.eat(); | |
} |
运行结果:
宠物吃东西 | |
狗狗吃狗粮 | |
猫猫吃猫粮 |
在代码中,Pet dog = new Dog ();、Pet cat = new Cat (); 这两个语句,把 Dog 和 Cat 对象转换为 Pet 对象,这种把一个子类对象转型为父类对象的做法称为向上转型。父类引用指向了子类的实例。也就实现了多态。
# 2.3 向上转型
向上转型又称为自动转型、隐式转型。向上转型就是父类引用指向子类实例,也就是子类的对象可以赋值给父类对象。例如:
Pet dog = new Dog(); |
这个是因为 Dog 类继承自 Pet 类,它拥有父类 Pet 的全部功能,所以如果 Pet 类型的变量指向了其子类 Dog 的实例,是不会出现问题的。
向上转型实际上是把一个子类型安全地变成了更加抽象的父类型,由于所有类的根类都是 Object,我们也把子类类型转换为 Object 类型:
Cat cat = new Cat(); | |
Object o = cat; |
# 2.4 向下转型
向上转型是父类引用指向子类实例,那么如何让子类引用指向父类实例呢?使用向下转型就可以实现。向下转型也被称为强制类型转换。例如:
// 为 Cat 类增加 run 方法 | |
class Cat extends Pet { // 继承父类 | |
// 重写父类方法 eat | |
public void eat() { | |
System.out.println("猫猫吃猫粮"); | |
} | |
public void run() { | |
System.out.println("猫猫跑步"); | |
} | |
public static void main(String[] args) { | |
// 实例化子类 | |
Pet cat = new Cat(); | |
// 强制类型转换,只有转换为 Cat 对象后,才能调用其下面的 run 方法 | |
Cat catObj = (Cat)cat; | |
catObj.run(); | |
} | |
} |
运行结果:
猫猫跑步 |
我们为 Cat 类新增了一个 run 方法,此时我们无法通过 Pet 类型的 cat 实例调用到其下面特有的 run 方法,需要向下转型,通过 (Cat) cat 将 Pet 类型的对象强制转换为 Cat 类型,这个时候就可以调用 run 方法了。
使用向下转型的时候,要注意:不能将父类对象转换为子类类型,也不能将兄弟类对象相互转换。以下两种都是错误的做法:
// 实例化父类 | |
Pet pet = new Pet(); | |
// 将父类转换为子类 | |
Cat cat = (Cat) pet; | |
// 实例化 Dog 类 | |
Dog dog = new Dog(); | |
// 兄弟类转换 | |
Cat catObj = (Cat) dog; |
不能将父类转换为子类,因为子类功能比父类多,多的功能无法凭空变出来。兄弟类之间不能转换,这就更容易理解了,兄弟类之间同样功能不尽相同,不同的功能也无法凭空变出来。
# 3.instanceof 运算符
instanceof 运算符用来检查对象引用是否是类型的实例,或者这个类型的子类,并返回布尔值。如果是返回 true,如果不是返回 false。通常可以在运行时使用 instanceof 运算符指出某个对象是否满足一个特定类型的实例特征。其使用语法为:
<对象引用> instanceof 特定类型 |
例如,在向下转型之前,可以使用 instanceof 运算符判断,这样可以提高向下转型的安全性:
Pet pet = new Cat(); | |
if (pet instanceof Cat) { | |
// 将父类转换为子类 | |
Cat cat = (Cat) pet; | |
} |
# 抽象类与接口
# 抽象类
- 抽象类的关键字 abstract
- 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类
- 抽象方法
- abstract 修饰
- 没有方法体
- 子类必须重写父类的抽象方法【除非子类也是抽象类】
- 抽象类不能直接实例化・,不能 new 但有构造方法
- 当父类实例化没有实际意义,或之类必须重写父类的某些方法,使用抽象类抽象方法
# 接口
会定义普通接口
interface
抽象方法 静态方法 default 修饰方法 static 修饰方法
接口可以继承多个接口
接口被子类实现时需要重写所有的抽象方法 除非子类也是抽象类
子类可以同时实现多个接口
接口是一种能力 体现在方法上
接口是一种约束 体现在注释上
抽象类与接口有何区别
- 不能实例化【抽象类有构造方法,接口没有构造方法】
- 抽象类可以没有抽象方法,但接口 jdk1.7 以前只有抽象方法 1.8 以后可以出现 default 及 static 修饰的方法
- 抽象类可以写做任意变量,接口只有静态变量
- 抽象类只能被单继承,接口可以多实现
- 类只能继承一个类,接口可以继承多个接口
# 接口作用
扩展性及维护性 降低代码耦合性
# 异常
# 什么是异常
- 实际工作中,遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求、你的程序要打开某个文件,这个文件可能不存在或者文件格式不对,你要读取数据库的数据,数据可能是空的等。我们的程序再跑着,内存或硬盘可能满了。等等。
- 软件程序在运行过程中,非常可能遇到刚刚提到的这些异常问题,我们叫异常,英文是:Exception,意思是例外。这些,例外情况,或者叫异常,怎么让我们写的程序做出合理的处理。而不至于程序崩溃。
- 异常指程序运行中出现的不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。
- 异常发生在程序运行期间,它影响了正常的程序执行流程。
# 简单分类
- 要理解 Java 异常处理是如何工作的,你需要掌握以下三种类型的异常:
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常:运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误:错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
# 异常体系结构
- Java 把异常当做对象来处理,并定义一个基类 java.lang.Throwable 作为所有异常的超类。
- 在 Java API 中已经定义了许多异常类,这些异常类分为两大类。错误 Error 和 Exception。
# 集合框架
Collection 单列集合 不唯一 无序
List 不唯一 有序
ArrayList
LinkdList
Set 唯一 无序
TreeSet
HashSet
Map 双列集合 【key/value】
HashMap
TreeMap
泛型
- 约束录入数据类型
- 避免获取数据时转换异常
封装类型 封装 拆箱
- 集合存储数据时,只允许 Object 类型
- 基本类型与对应封装类型可以互转
- 确定基本类型与封装类的对应关系
- int Integer
- char Character
- byte Byte
- short Short
- long Long
- float Float
- double Double
- boolean Boolean
- 封装类为 null 时,请不要拆箱
int i = 10; | |
Integer myi = new Integer(); | |
myi = i; // 装箱 | |
i = myi; // 拆箱 | |
System.out.println("引用:" + my); | |
System.out.println("基本:" + i); |
# ArrayList
ArrayList 与 LinkedList 的区别
ArrayList 是数组结构 查询 遍历速度快
# HashMap
HashMap 存储空间不连续,只能通过 Key 值访问数据
Java 中,HashMap 类是基于哈希表的 Map 接口的实现。它提供所有可选的映射操作,并允许使用 null 值和 null 键。但此类不保证映射的顺序,特别是它不保证该顺序恒久不变。Hashtable 类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。
在 Java 集合框架中,有些类是线程同步安全的类,它们是 Vector、Hashtable、Stack、enumeration。除了这些之外,其他的都是非线程安全的类和接口。
# LinkedList
- 本题考查 LinkedList 集合类的常用方法。Java 集合框架中,LinkedList 类实现所有可选的列表操作,并且允许所有元素(包括 null)。
- offer() 是将指定元素添加到此列表的未尾(最后一个元素)。
- **pop ()** 方法从此列表所表示的堆栈处弹出一个元素,即移除并返回此列表的第一个元素。
- offerFirst() 方法是在此列表的开头插入指定的元素。
- **get ()** 方法是返回此列表中指定位置处的元素。
- **push ()** 方法是将元素推入此列表所表示的堆栈,即将该元素插入此列表的开头。
- 注意:与 offerFirst () 方法对应的 offerList () 方法则是在此列表未尾插入指定的元素。
# 泛型集合
- 泛型一样遵循多态,父引用指向子对象。
- 泛型是 Java SE1.5 的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
- 这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
- Java 语言引入泛型的好处是安全简单。
- 在 Java SE1.5 之前,没有泛型的情况的下,通过对类型 Object 的引用来实现参数的 “任意化”,“任意化” 带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。
- 对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
- 泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
# 实用类
# 枚举
限定内容的选值 实现对输入的值进行约束
枚举可以有构造方法、普通方法、属性
# Timer 定时器
//timer.schedule(TimerTask task, long delay, long period) | |
//delay – 任务执行前的延迟毫秒数。period – 连续任务执行之间的时间(以毫秒为单位) | |
Timer timer = new Timer(); | |
timer.schedule(new TimerTask() { | |
@Override | |
public void run() { | |
// 方法体 | |
} | |
},0,1000) |
# Java I/O
# 流的分类
- 按流向划分:输入流和输出流
- 按处理单元划分:字节流和字符流
- 按流的角色划分:节点流和处理流
# 多线程
# 核心概念:
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc 线程;
- main () 称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如 cpu 调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
# 继承 Thread 类(重点)
- 子类继承 Thread 类具备多线程能力
- 启动线程:子类对象.start ()
- 不建议使用:避免 OOP 单继承的局限性
# 实现 Runnable 接口(重点)
- 实现接口 Runnable 具有多线程能力
- 启动线程:传入目标对象 + Thread 对象.start ()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
# 实现 Runnable 接口
# 实现步骤:
- 实现 callable 接口,需要返回值类型
- 重写 call 方法,需要抛出异常
- 创建目标对象
- 创建执行任务:ExecutorService ser = Executors.newFixedThreadPool (1);
- 提交执行:Future<Boolean> result1 = ser.submit (t1);
- 获取结果:boolean r1 = result1.get ();
- 关闭服务:ser.shutdownNow ();
# 好处:
- 可以定义返回值
- 可以抛出异常
# 静态代理
# 静态代理模式总结:
- 真实对象和代理对象都要实现同一个接口
- 代理对象要代理真实对象
# 好处:
- 代理对象可以做很多真实对象做不了的事情
- 真实对象专注做自己的事情
# Lambda 表达式
理解 Functional Interface(函数式接口)是学习 Java8 Lambda 表达式的关键所在。
# 函数式接口的定义:
任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
public interface Runnable { | |
public abstract void run(); | |
} |
对于函数式接口,我们可以通过 Lambda 表达式来创建该接口的对象。
# 为什么要使用 Lambda 表达式:
- 避免匿名内部类定义过多
- 可以让你的代码看起来很简洁
- 去掉了一堆没有意义的代码,只留下核心的逻辑。
# Lambda 表达式的简化:
- lambda 表达式只能有一行代码的情况下才能简化成为一行,如果有多行那么就用代码块包裹。
- 前提是接口为函数式接口
- 多个参数也可以去掉参数类型,要去掉就都去掉
# 线程停止
- 建议线程正常停止 -> 利用次数,不建议是循环。
- 建议使用标志位 -> 设置一个标志位
- 不要使用 stop 或者 destroy 等过时或者 JDK 不建议使用的方法
# 示例:
public class TestStop implements Runnable { | |
/** | |
* 1. 设置一个标志位 | |
*/ | |
private boolean flag = true; | |
@Override | |
public void run() { | |
int i = 0; | |
while (flag) { | |
System.out.println("run....Thread" + i++); | |
} | |
} | |
/** | |
* 2. 设置一个公开的方法停止线程,转换标志位 | |
*/ | |
public void stop() { | |
this.flag = false; | |
} | |
public static void main(String[] args) { | |
TestStop testStop = new TestStop(); | |
new Thread(testStop).start(); | |
for (int i = 0; i < 1000; i++) { | |
System.out.println("main" + i); | |
if (i == 900) { | |
// 调用 stop 方法切换标志位,让线程停止 | |
testStop.stop(); | |
System.out.println("线程该停止了"); | |
} | |
} | |
} | |
} |
# 转态
# 线程休眠
- **sleep (时间)** 指定当前线程阻塞时间的毫秒数;
- sleep 存在异常 InterruptedException;
- sleep 时间达到后线程进入就绪状态;
- sleep 可以模拟网络延时,倒计时等。
- 每一个对象都有一个锁,sleep 不会释放锁;
# 线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让 CPU 重新调度,礼让不一定成功!看 CPU 心情
public class TestYield { | |
public static void main(String[] args) { | |
MyYield myYield = new MyYield(); | |
new Thread(myYield,"a").start(); | |
new Thread(myYield,"b").start(); | |
} | |
} | |
class MyYield implements Runnable { | |
@Override | |
public void run() { | |
System.out.println(Thread.currentThread().getName() + "线程开始执行"); | |
Thread.yield(); // 礼让 | |
System.out.println(Thread.currentThread().getName() + "线程停止执行"); | |
} | |
} |
# 线程状态观测
- Thread.State
线程状态。线程可以处于以下状态之一:
- NEW
尚未启动的线程处于此状态。
- RUNNABLE
在 Java 虚拟机中执行的线程处于此状态。
- BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
- WAITING
正在等待另一个线程执行特定动作的线程处于此状态。
- TIMED WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
- TERMINATED
已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
- Thread t = new Thread () 线程对象一旦创建就进入到了新生状态。
- 当调用 start () 方法,线程立即进入就绪状态,但不意味着立即调度执行。
- 当调用 sleep,wait 或同步锁定时线程进入阻塞状态,就是代码不往下执行,阻塞阻塞事件解除后,重新进入就绪状态,等待 cpu 调度执行。
- 进入运行状态,线程才真正执行线程体的代码块。
- 线程中断或者结束,一旦进入死亡状态,就不能再次启。
# 线程优先级
- Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
- 线程的优先级用数字表示,范围从 1-10.
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
- 使用以下方式改变或获取优先级
- getPriority().setPriority(int xxx)
注意:先设置优先级再启动!
# 守护 (daemon) 线程
线程分为用户线程和守护线程。
虚拟机必须确保用户线程执行完毕。
虚拟机不用等待守护线程执行完毕。
如,后台记录操作日志,监控内存,垃圾回收等待..
/** | |
* 测试守护线程 | |
* 上帝守护你 | |
* @author Pretend | |
*/ | |
public class TestDaemon { | |
public static void main(String[] args) { | |
God god = new God(); | |
You you = new You(); | |
Thread thread = new Thread(god); | |
// 默认是 false 表示用户线程,正常的线程都是用户线程... | |
thread.setDaemon(true); | |
// 上帝守护线程启动 | |
thread.start(); | |
// 你 用户线程启动... | |
new Thread(new You()).start(); | |
} | |
} | |
/** | |
* 上帝 | |
*/ | |
class God implements Runnable { | |
@Override | |
public void run() { | |
while (true) { | |
System.out.println("上帝保佑着你"); | |
} | |
} | |
} | |
/** | |
* 你 | |
*/ | |
class You implements Runnable { | |
@Override | |
public void run() { | |
for (int i = 0; i < 36500; i++) { | |
System.out.println("你一生都开心地活着"); | |
} | |
//Hello,world | |
System.out.println("=====Goodbye!world!====="); | |
} | |
} |
# 线程同步(重点、难点)
# 多个线程操作同一个资源
并发:同一个对象被多个线程同时操作
- 现实生活中,我们会遇到 “同一个资源,多个人都想使用” 的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队。一个个来.
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线,程再使用。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.
# 同步方法
- 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:
synchronized 方法和 synchronized 块.
同步方法:public synchronized void method (int args){}
- synchronized 方法控制对 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为 synchronized 将会影响效率
# 同步块
- 同步块:synchronized**(Obj){}**
- Obj 称之为同步监视器
- Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this,就是这个对象本身,或者是 class [反射中讲解】
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码.
- 第二个线程访问,发现同步监视器被锁定,无法访问.
- 第一个线程访问完毕,解锁同步监视器.
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问.
# 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有 “两个以上对象的锁” 时,就可能会发生 “死锁” 的问题
错误代码:
package com.pretend.thread.syn; | |
/** | |
* 死锁:多个线程互相抱着对方需要的资源,然后形成僵持. | |
* @author Pretend | |
*/ | |
public class DeadLock { | |
} | |
/** | |
* 口红 | |
*/ | |
class Lipstick { | |
} | |
/** | |
* 镜子 | |
*/ | |
class Mirror { | |
public static void main(String[] args) { | |
Makeup g1 = new Makeup(0,"灰姑凉"); | |
Makeup g2 = new Makeup(1,"白雪公主"); | |
g1.start(); | |
g2.start(); | |
} | |
} | |
class Makeup extends Thread { | |
// 需要的资源只有一份,用 static 来保证只有一份 | |
static Lipstick lipstick = new Lipstick(); | |
static Mirror mirror = new Mirror(); | |
// 选择 | |
int choice; | |
// 使用化妆品的人 | |
String girlName; | |
public Makeup(int choice, String girlName) { | |
this.choice = choice; | |
this.girlName = girlName; | |
} | |
@Override | |
public void run() { | |
// 化妆 | |
try { | |
makeup(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* 化妆,互相持有对方的锁,就是需要拿到对方的资源 | |
*/ | |
private void makeup() throws InterruptedException { | |
if (choice == 0) { | |
// 获得口红的锁 | |
synchronized (lipstick) { | |
System.out.println(this.girlName + "获得口红的锁"); | |
Thread.sleep(1000); | |
// 一秒钟后想获得镜子 | |
synchronized (mirror) { | |
System.out.println(this.girlName + "获得镜子的锁"); | |
} | |
} | |
} else { | |
// 获得镜子的锁 | |
synchronized (mirror) { | |
System.out.println(this.girlName + "获得镜子的锁"); | |
Thread.sleep(2000); | |
// 两秒钟后想获得口红 | |
synchronized (lipstick) { | |
System.out.println(this.girlName + "获得口红的锁"); | |
} | |
} | |
} | |
} | |
} |
正确代码:
package com.pretend.thread.syn; | |
/** | |
* 死锁:多个线程互相抱着对方需要的资源,然后形成僵持. | |
* @author Pretend | |
*/ | |
public class DeadLock { | |
} | |
/** | |
* 口红 | |
*/ | |
class Lipstick { | |
} | |
/** | |
* 镜子 | |
*/ | |
class Mirror { | |
public static void main(String[] args) { | |
Makeup g1 = new Makeup(0,"灰姑凉"); | |
Makeup g2 = new Makeup(1,"白雪公主"); | |
g1.start(); | |
g2.start(); | |
} | |
} | |
class Makeup extends Thread { | |
// 需要的资源只有一份,用 static 来保证只有一份 | |
static Lipstick lipstick = new Lipstick(); | |
static Mirror mirror = new Mirror(); | |
// 选择 | |
int choice; | |
// 使用化妆品的人 | |
String girlName; | |
public Makeup(int choice, String girlName) { | |
this.choice = choice; | |
this.girlName = girlName; | |
} | |
@Override | |
public void run() { | |
// 化妆 | |
try { | |
makeup(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* 化妆,互相持有对方的锁,就是需要拿到对方的资源 | |
*/ | |
private void makeup() throws InterruptedException { | |
if (choice == 0) { | |
// 获得口红的锁 | |
synchronized (lipstick) { | |
System.out.println(this.girlName + "获得口红的锁"); | |
Thread.sleep(1000); | |
} | |
// 一秒钟后想获得镜子 | |
synchronized (mirror) { | |
System.out.println(this.girlName + "获得镜子的锁"); | |
} | |
} else { | |
// 获得镜子的锁 | |
synchronized (mirror) { | |
System.out.println(this.girlName + "获得镜子的锁"); | |
Thread.sleep(2000); | |
} | |
// 一秒钟后想获得口红 | |
synchronized (lipstick) { | |
System.out.println(this.girlName + "获得口红的锁"); | |
} | |
} | |
} | |
} |
# 死锁避免方法
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
# Lock(锁)
- 从 JDK 5.0 开始,Java 提供了更强大的线程同步机制通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当
- java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。
- 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
- ReentrantLock(可重入锁)类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。
示例:
import java.util.concurrent.locks.ReentrantLock; | |
/** | |
* 测试 Lock 锁 | |
* @author Pretend | |
*/ | |
public class TestLock { | |
public static void main(String[] args) { | |
TestLock2 testLock2 = new TestLock2(); | |
new Thread(testLock2).start(); | |
new Thread(testLock2).start(); | |
new Thread(testLock2).start(); | |
} | |
} | |
class TestLock2 implements Runnable { | |
int ticketNums = 10; | |
// 定义 lock 锁 | |
private final ReentrantLock lock = new ReentrantLock(); | |
@Override | |
public void run() { | |
while (true) { | |
try { | |
// 加锁 | |
lock.lock(); | |
if (ticketNums > 0) { | |
try { | |
// 模拟延时,放大问题发生性 | |
Thread.sleep(1000); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
System.out.println(ticketNums--); | |
} else { | |
break; | |
} | |
} finally { | |
// 解锁 | |
lock.unlock(); | |
} | |
} | |
} | |
} |
# synchronized 与 Lock 的对比
- Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized 是隐式锁,出了作用域自动释放
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
- Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
# 线程协作(难点、重点)
生产消费者模式
# 线程通信
- 应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费.
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止.
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止.
Java 提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与 sleep 不同,会释放锁. |
wait(long timeout) | 指定等待的毫秒数. |
notify() | 唤醒一个处于等待状态的线程. |
notifyAll() | 唤醒同一个对象上所有调用 wait()方法的线程,优先级别高的线程优先调度. |
注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 llegalMonitorStateException
# 解决方式 1
并发协作模型 “生产者 / 消费者模式”--> 管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个 “缓冲区生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
示例
package com.pretend.thread.advanced; | |
/** | |
* 测试:生产者消费者模型 --> 利用缓冲区解决:管程法 | |
* 消费者,消费者,产品,缓冲区 | |
* @author Pretend | |
*/ | |
public class TestPc { | |
public static void main(String[] args) { | |
SynContainer container = new SynContainer(); | |
new Productor(container).start(); | |
new Consumer(container).start(); | |
} | |
} | |
/** | |
* 生产者 | |
*/ | |
class Productor extends Thread { | |
SynContainer container; | |
public Productor(SynContainer container) { | |
this.container = container; | |
} | |
/** | |
* 生产 | |
*/ | |
@Override | |
public void run() { | |
for (int i = 0; i < 100; i++) { | |
container.push(new Chicken(i)); | |
System.out.println("生产了" + i + "鸡"); | |
} | |
} | |
} | |
/** | |
* 消费者 | |
*/ | |
class Consumer extends Thread { | |
SynContainer container; | |
public Consumer(SynContainer container) { | |
this.container = container; | |
} | |
/** | |
* 消费 | |
*/ | |
@Override | |
public void run() { | |
for (int i = 0; i < 100; i++) { | |
System.out.println("消费了-->" + container.pop().id + "只鸡"); | |
} | |
} | |
} | |
/** | |
* 产品 | |
*/ | |
class Chicken { | |
// 产品编号 | |
int id; | |
public Chicken(int id) { | |
this.id = id; | |
} | |
} | |
/** | |
* 缓冲区 | |
*/ | |
class SynContainer { | |
// 需要一个容器大小 | |
Chicken[] chickens = new Chicken[10]; | |
// 容器计数器 | |
int count = 0; | |
/** | |
* 生产者放入产品 | |
* @param chicken 产品 | |
*/ | |
public synchronized void push(Chicken chicken) { | |
// 如果容器满了,就要等待消费者消费 | |
if (count == chickens.length) { | |
// 通知消费者消费。生产等待 | |
try { | |
this.wait(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
// 如果没有满,我们就需要丢入产品 | |
chickens[count] = chicken; | |
count ++; | |
// 可以通知消费者消费了 | |
this.notifyAll(); | |
} | |
public synchronized Chicken pop() { | |
// 判断能否消费 | |
if (count == 0) { | |
// 等待生产者生产,消费者等待 | |
try { | |
this.wait(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
// 如果可以消费 | |
count--; | |
Chicken chicken = chickens[count]; | |
// 吃完了,通知生产者 | |
this.notifyAll(); | |
return chicken; | |
} | |
} |
# 解决方式 2
并发协作模型 “生产者 / 消费者模式”--> 信号灯法
示例
package com.pretend.thread.advanced; | |
/** | |
* 测试生产者消费者问题 2:信号灯法,标志位解决 | |
* @author Pretend | |
*/ | |
public class TestPc2 { | |
public static void main(String[] args) { | |
Tv tv = new Tv(); | |
new Player(tv).start(); | |
new Watcher(tv).start(); | |
} | |
} | |
/** | |
* 生产者 --> 演员 | |
*/ | |
class Player extends Thread { | |
Tv tv; | |
public Player(Tv tv) { | |
this.tv = tv; | |
} | |
@Override | |
public void run() { | |
for (int i = 0; i < 20; i++) { | |
if (i % 2 == 0) { | |
this.tv.play("快乐大本营播放中"); | |
} else { | |
this.tv.play("抖音:记录美好生活"); | |
} | |
} | |
} | |
} | |
/** | |
* 消费者 --> 观众 | |
*/ | |
class Watcher extends Thread { | |
Tv tv; | |
public Watcher(Tv tv) { | |
this.tv = tv; | |
} | |
@Override | |
public void run() { | |
for (int i = 0; i < 20; i++) { | |
tv.watch(); | |
} | |
} | |
} | |
/** | |
* 产品 --> 节目 | |
*/ | |
class Tv { | |
/** | |
* 演员表演,观众等待 T | |
* 观众观看,演员等待 F | |
* 表演的节目 | |
*/ | |
String voice; | |
boolean flag = true; | |
/** | |
* 表演 | |
* @param voice 声音 | |
*/ | |
public synchronized void play(String voice) { | |
if (!flag) { | |
try { | |
this.wait(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
System.out.println("演员表演了:" + voice); | |
// 通知观众观看 | |
this.notifyAll(); // 通知唤醒 | |
this.voice = voice; | |
this.flag = !this.flag; | |
} | |
public synchronized void watch() { | |
if (flag) { | |
try { | |
this.wait(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
System.out.println("观看了:" + voice); | |
// 通知演员表演 | |
this.notifyAll(); | |
this.flag = !this.flag; | |
} | |
} |
# 线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
- 可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(….)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
# 使用线程池
JDK 5.0 起提供了线程池相关 API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
- void execute(Runnable command):执行任务 / 命令,没有返回值,一般用来执行 Runnable
- <T>Future<T> submit(Callable-T>task):执行任务,有返回值,一般又来执行 Callable
- void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
示例
package com.pretend.thread.advanced; | |
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
/** | |
* 测试线程池 | |
* @author Pretend | |
*/ | |
public class TestPool { | |
public static void main(String[] args) { | |
//1. 创建服务,创建线程池 | |
ExecutorService service = Executors.newFixedThreadPool(10); | |
// 执行 | |
service.execute(new MyThread()); | |
service.execute(new MyThread()); | |
service.execute(new MyThread()); | |
service.execute(new MyThread()); | |
//2. 关闭连接 | |
service.shutdown(); | |
} | |
} | |
class MyThread extends Thread { | |
@Override | |
public void run() { | |
System.out.println(Thread.currentThread().getName()); | |
} | |
} |
# 创建线程的三种方式
package com.pretend.thread.advanced; | |
import java.util.concurrent.Callable; | |
import java.util.concurrent.ExecutionException; | |
import java.util.concurrent.FutureTask; | |
/** | |
* 回顾总结线程的创建 | |
* @author Pretend | |
*/ | |
public class ThreadNew { | |
public static void main(String[] args) { | |
new MyThread1().start(); | |
new Thread(new MyThread2()).start(); | |
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3()); | |
new Thread(futureTask).start(); | |
try { | |
Integer integer = futureTask.get(); | |
System.out.println(integer); | |
} catch (InterruptedException | ExecutionException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
/** | |
* 1. 继承 Thread | |
*/ | |
class MyThread1 extends Thread { | |
@Override | |
public void run() { | |
System.out.println("MyThread1"); | |
} | |
} | |
/** | |
* 2. 实现 Runnable 接口 | |
*/ | |
class MyThread2 implements Runnable { | |
@Override | |
public void run() { | |
System.out.println("MyThread2"); | |
} | |
} | |
/** | |
* 3. 实现 Callable 接口 | |
*/ | |
class MyThread3 implements Callable<Integer> { | |
@Override | |
public Integer call() { | |
System.out.println("MyThread3"); | |
return 100; | |
} | |
} |