异常
学习异常处理的目的:
不能让程序因为一个小小的问题而导致整个程序崩溃。
什么是异常:
在程序编译或运行过程中出现的问题则称为异常。
异常的继承体系:
Throwable 类是 Java 语言中所有错误或异常的超类。
- Error:错误
- Exception:异常
- 编译时异常
- 运行时异常
图解:
Throwable常用方法:
String getMessage()
- 获得异常信息字符串
String toString()
- 获得异常详细信息字符串: 类全名:异常原因
- 与直接sout(对象)输出的一样。
void printStackTrace()
- 追踪异常的栈信息(追踪异常的根源)
- 推荐使用
示例代码:
public class ExceptionDemo02 {
public static void main(String[] args){
test02();
}
private static void test02() {
test01();
}
private static void test01() {
// 创建Throwable对象
Throwable t = new Throwable("空指针异常");
// 获得异常信息字符串
System.out.println(t.getMessage());
// 获得异常详细信息字符串:类全名:异常原因
System.out.println(t);
System.out.println(t.toString());
// 追踪异常的栈信息(追踪异常的根源)
t.printStackTrace();
}
}
错误和异常的区别:
- 错误一般是由操作系统反馈给JVM的,无法针对错误进行处理,只能修改错误行的代码。
- 异常一般是JVM反馈给Java程序,可以针对异常进行处理,如果不处理,则结果和错误一样。
- 其实有的错误也是可以处理的,但是系统不建议。一般遇到错误,就修改代码,不要就想着去处理!!!
异常和错误的判断:
- 根据异常的类名进行判断,如果是以Exception结尾,则是异常,否则就是错误。
- 错误一般都是Error结尾,但是有一个例外:ThreadDeath
异常处理
异常处理的方式:
- JVM处理方式
- 手动处理方式
JVM处理方式:
- 将异常信息打印在控制台上。
- 退出JVM,终止程序运行。
- 异常信息: 异常的类名:异常的原因:异常的位置
手动处理方式:
- 抛出处理
- 捕获处理
- 单捕获处理
- 多捕获处理
捕获处理的格式:
try{
编写可能会出现异常的代码
}catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}finally{不管是否出现异常都要执行的代码。
}
- 每个catch里的异常参数变量名可以相同。
格式说明:
- try代码块:编写可能出现异常的代码,只要try里面有一行代码出现异常,该行代码后面的代码就不会执行。
- catch代码块:编写出现异常之后要执行的代码,如果try代码块中的代码没有出现异常,则不会执行该代码块的代码。
- finally代码块:不管是否出现异常,都会执行该代码块。只要代码执行流程进入了try代码块,就算try里面的代码块有return语句,退出方法,finally的代码也会进行。若想不运行,可以在try代码块里加入 System.exit
finally代码块的作用:
用来释放资源,比如关闭流,关闭数据库资源。
多catch捕获处理注意事项:
- 如果多个catch的异常类型之间有继承关系,则父类类型要放在子类类型的后面。
- 如果多个catch之间的异常类型之间没有继承关系,则没有先后顺序要求。
是否捕获异常只需要捕获Exception异常即可? :
- 不是,因为实际开发,有时需要针对不同的异常有不同的处理方式,如果针对所有异常的处理方式都是一样的则可以直接捕获Exception即可。
异常处理之抛出处理
throw关键字作用:
将异常对象抛给方法调用者并结束当前方法的运行。
throw关键字的使用格式:
throw new 异常类名("异常信息字符串");
throw关键字的使用位置:
使用在方法体中
throws关键字作用:
将方法体中可能会出现的异常标识出来报告给方法调用者,让方法调用者注意处理异常。
throws关键字的使用格式:
修饰符 返回值类型 方法名(参数列表)throws 异常类名1,异常类名2...{}
throws关键字的使用位置:
使用在方法声明上。
方法重写时异常处理注意事项 (针对编译时异常而言):
- 父类方法中没有声明异常时,子类重写方法时也不能声明异常。
- 父类方法中有声明异常时,子类重写方法时可以声明小于等于父类声明的异常。
- 重写方法不能抛出与父类平级的其他编译时异常。
- 子类重写方法时不能声明大于父类声明的异常或与父类声明的异常无关的异常。
编译时异常和运行时异常
什么是运行时异常:
只要是RuntimeException或其子类异常都属于运行时异常。
什么是编译时异常:
除了运行时异常以外的所有异常都是编译时异常。
运行时异常的特点:
- 方法体中抛出的异常是运行时异常,则可以处理,也可以不处理。
- 方法声明中声明的异常是运行时异常,则方法调用者可以处理,也可以不处理。
编译时异常的特点:
- 方法体中抛出的异常是编译时异常,则要求必须要处理。
- 方法声明中声明的异常是编译时异常,则要求方法调用者一定要处理。
为什么Java编译器对运行时异常处理如此松散?:
因为运行时异常一般是可以通过程序猿良好的编程习惯避免的。
对 “方法体中抛出的异常是运行时异常,则可以处理,也可以不处理。” 这句话的理解:
虽然不处理都会崩,但是:
- 如果抛出的是运行时异常,则编译可以通过,但是运行不行通过。
- 如果抛出的是编译时异常,则编译都通过不了。就算同if语句来避免它,不走该语句,也编译不通过。
自定义异常
自定义异常的步骤:
1)创建一个类,类名:XxxException。 (不是这样的类名也不会报错,只是为了规范)
2)继承官方的异常类
- RuntimeException:运行时异常
- Exception:编译时异常
3)提供构造方法
- 无参构造
- 有参构造
- public XxxException(String message) {super(message);}
线程
写代码时要保证单线程能正确运动,再开启多线程。
学习多线程的目的:
为了让多个任务能够并行执行,提高程序的执行效率。
并行和并发的概念:
- 两个或多个任务在同一时间执行,比如任务A和任务B在18:23:30执行了。
- 两个或多个任务在同一时间段执行,比如任务A和任务B在18:23:30到18:25:30执行了。
进程的概念:
一个正在运行中的程序就是一个进程。
进程的作用:
用来封装线程,为线程执行任务提供资源(内存资源)。
线程的概念:
进程中的一个独立的执行路径(执行单元)。
线程的作用:
用来执行代码的。
什么是主线程:
- 程序启动过程中自动创建并执行main方法的线程则称为主线程。
- Java程序启动过程中默认会创建两个线程:一个是主线程,一个是垃圾回收器的线程(后台线程)。
主线程的执行路径:
从main方法开始直到main方法结束。
什么是子线程:
除了主线程以外的所有线程都是子线程。
子线程的执行路径:
从run方法开始直到run方法结束。
注意事项:
- CPU负责调度线程,一个CPU只能同时执行一个线程。
- 每个线程都会有自己独立的栈空间。
- 在同一个线程中,代码是从上往下按顺序执行的。
- 同一个进程中的线程共享进程的堆空间。
线程的运行模式:
- 分时式模式:每个线程平均分配CPU使用权,每个线程使用CPU的时间是相同。
- 抢占时模式:优先级高的线程抢到CPU的概率会高,如果优先级相同,则所有线程一起去抢夺CPU ,哪个线程抢到CPU就执行哪个线程的任务。Java程序的线程运行模式就属于该种模式。(Java只有这种)
Thread类常用方法:
String getName()
- 获得线程名称,默认名称:Thread‐序号
void setName(String name)
- 设置线程名称。
static Thread currentThread()
- 获得执行当前方法的线程对象。
static void sleep(long time)
- 让当前线程休眠指定的毫秒值
创建线程的方式1:继承Thread类
- 创建一个类继承Thread类。
- 重写run方法:将线程任务相关的代码写在该方法中。
- 创建Thread子类对象,调用start方法开启线程。
注意:
- 不要手动调用run方法:不会开启新的执行路径,而是在当前线程执行run方法的代码。
示例代码:
/**
自定义线程
*/
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("run...." + i);
}
}
}
public class ThreadDemo01 {
public static void main(String[] args){
// 创建线程对象:相当于招聘一个工人
MyThread t = new MyThread();
// 开启线程:开辟新的执行路径执行run方法的代码
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("main...." + i);
}
}
}
创建线程方式2:实现Runnable接口
- 创建类实现Runnable接口,重写run方法:将线程任务相关的代码写在该方法中。
- 创建实现类对象,根据实现类对象创建Thread类对象。
- 调用Thread类的start方法开启线程。
注意:
- Runnable接口中的run方法,参数列表为空,不带参数。
- 如果写public void(int num),则此方法不是Runnable接口的run方法。
实现Runnable的好处:
- 解决了Java类单继承的局限性。
- 可以更好的在多个线程之间共享数据。
- 将线程和任务进行了分离,降低了程序的耦合性。(高内聚,低耦合)
- 为线程池提供前提条件。
示例代码:
public class MyRunnable implements Runnable {
/*
线程要执行的任务代码
*/
@Override
public void run() {
System.out.println("子线程..." + Thread.currentThread().getName());
}
}
public class ThreadDemo01 {
public static void main(String[] args){
// 创建Runnable接口的实现类对象
MyRunnable target = new MyRunnable();
// 创建Thread类对象
Thread t = new Thread(target);
// 调用start方法开启线程:会在新的路径中执行run方法
t.start();
}
}
创建线程方式3:使用匿名内部类
什么时候使用匿名内部类创建线程:
如果任务只需要执行一次时,可以考虑使用匿名内部类,否则不推荐使用。
线程安全
什么是线程安全:
两个或两个以上的线程在执行任务过程中操作共享资源仍然能得到正确的结果则称为线程安全。
同步和异步的概念:
- 同步:按顺序执行。
- 异步:多线程的代名词。
线程同步:
多个线程之间要按顺序执行(不能同时执行)。
实现线程同步(线程安全)的方式:
- 同步代码块
- 同步方法
- Lock锁
同步代码块
同步代码块的格式:
synchronized(锁对象){
// 操作共享资源的代码
}
同步代码块的原理:
- 能够保证同一时间只能有一个线程执行代码块中的代码。
同步代码块注意事项:
- 锁对象可以是任意类型的对象。
- 锁对象必须被所有线程共享。
- 所有线程必须共用一个锁对象。
自己的小总结:
当用同步代码块时:
- 当用继承Thread类时,synchronize()里的对象不能用this,只能用 类名.class
- 当用实现Runnable接口时,synchronize()里的对象既可以用this,也能用 类名.class
示例代码:
public class ThreadDemo01 {
public static void main(String[] args) {
TicketThread t = new TicketThread();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.setName("A");
t2.setName("B");
t1.start();
t2.start();
}
}
public class TicketThread implements Runnable {
int tickets = 100;
@Override
public void run() {
while(true){
synchronized (this){
if (tickets > 0){
System.out.println(Thread.currentThread().getName() + " 卖了一张票,还剩 " + (--tickets) + "张票");
continue;
}
}
System.out.println("票没了");
break;
}
}
}
同步方法
同步方法的格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
}
同步方法的原理:
能够保证同一时间只有一个线程执行方法体的代码。
同步方法的锁对象:
- 非静态同步方法:锁对象是:this
- 静态同步方法:锁对象是:类名.class
类名.class获得的是Class对象,字节码对象
- 每一个类都会有一个Class对象。
- 每一个类的Class对象都是唯一的。
静态同步方法和非静态同步方法的选择:
- 如果方法体中需要访问非静态成员,则定义为非静态同步方法。
- 如果方法体中不需要访问任何非静态成员,则可以将该方法定义静态同步方法。
示例代码:
public class ThreadDemo01 {
public static void main(String[] args) {
TicketThread target = new TicketThread();
// 创建线程卖票:模拟窗口
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
// 设置线程名称
t1.setName("美女A");
t2.setName("美女B");
// 开启线程卖票
t1.start();
t2.start();
}
}
public class TicketThread implements Runnable {
static int ticket = 100;
@Override
public void run() {
while(ticket > 0){
sale();
}
System.out.println("票没了");
}
public static synchronized void sale(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + " 卖了一张票,还剩"+(--ticket)+ "张票");
}
}
}
Lock锁
Lock接口概述:
- JDK1.5新特性。
- 专门用来实现线程安全的技术。
Lock接口常用实现类:
ReentrantLock:互斥锁
Lock接口常用方法:
void lock()
- 获取锁。
void unlock()
- 释放锁。
Lock接口注意事项:
获取锁和释放锁的代码必须成对出现。
synchronized和Lock接口的选择:
- 如果资源竞争不激烈,则选择谁都差不多。
- 如果资源竞争很激烈,则选择Lock接口的效率会高于synchronized关键字。
示例代码:
public class Demo1 {
public static void main(String[] args) {
TicketThread target = new TicketThread();
// 创建线程卖票:模拟窗口
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
// 设置线程名称
t1.setName("美女A");
t2.setName("美女B");
// 开启线程卖票
t1.start();
t2.start();
}
}
public class TicketThread implements Runnable {
int ticket = 100;
@Override
public void run() {
Lock l = new ReentrantLock();
while(true){
l.lock();
try {
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + " 卖了一张票,还剩" + (--ticket) + "张票");
continue;
}
}finally {
l.unlock();
}
System.out.println("没票了");
break;
}
}
}
线程状态
线程的六种状态(生命周期): (面试题)
New | 新建状态 | 刚刚创建出来,还没有调用start方法之前的状态。 |
Runnable | 可运行状态 | 可能正在执行,也可能不是正在执行,只有在该状态下的线程才有资格抢CPU。 |
Blocked | 锁阻塞 | 线程要等待另一个线程释放锁对象。 |
Waiting | 无线等待状态 | 线程调用了wait方法进入的状态,需要其他线程调用notify方法唤醒。 |
Timed Waiting | 计时等待状态 | 线程调用了sleep方法或wait(long time) 方法进入的状态。 |
Teminated | 死亡状态 | 线程任务执行完毕或调用了stop方法。 |
线程池
什么是线程池:
一个用来创建和管理线程对象的容器。
线程池的核心思想:
线程复用。
线程池的好处:
- 减少资源的消耗,避免频繁的创建和销毁线程。
- 提高程序的响应速度。
- 提高线程的可管理性。
如何获得线程池对象:
- 在JDK1.5之后,官方专门提供了一个线程池工具类用来创建线程池对象。
- 该类叫:Executors,通过该类的静态方法来创建线程池对象,静态方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
- 根据指定的线程数量创建线程池对象。
ExecutorService接口概述:
只要是实现了该接口的对象就是一个线程池对象。
ExecutorService接口常用方法:
Future<?> submit(Runnable task)
- 提交Runnable任务。
Future<T> submit(Callable<T> task)
- 提交Callable任务
void shutdown()
- 销毁线程池,会等待线程池中已经提交的所有任务执行完毕之后再销毁。
void shutdownNow()
- 立即销毁线程池:如果线程池中还没有开始执行的任务就不会执行了。
Future接口概述:
用来封装Callable任务执行完毕的结果。
Future接口常用方法:
V get()
- 获得call方法执行完毕的返回值。
- get方法是阻塞方法,会阻塞当前线程的进行。
线程池提交Runnalbe任务步骤:
1)创建线程池对象并指定线程数量。
- ExecutorService tp = Executors.newFixedThreadPool(3);
2)创建类实现Runnable接口,重写run方法:将线程任务相关的代码写在该方法中。
3)创建实现类对象,调用线程池对象的submit方法传递实现类对象。
- tp.submit(new RunnableTask());
4)调用线程池对象的shutdown方法销毁线程池。
- tp.shutdown();
示例代码:
public class RunnableTask implements Runnable {
@Override
public void run() {
System.out.println("线程任务..." + Thread.currentThread().getName());
}
}
public class ThreadPoolDemo01 {
public static void main(String[] args){
// 创建线程池对象并指定线程数量
ExecutorService tp = Executors.newFixedThreadPool(3);
// 提交Runnable任务
tp.submit(new RunnableTask());
// 销毁线程池:(在实际开发中一般是不销毁的)
tp.shutdown();
}
}
线程池提交Callable任务:
与提交Runnable基本一样,不同的是实现:
- 实现Callable接口。
- 重写call方法(有返回值)。
Callable和Runnable接口的选择:
- 如果任务执行完毕之后需要返回一个结果给调用者,则选择Callable。
- 如果任务执行完毕之后不需要返回结果给调用者,则随便选择。
多线程等待唤醒机制
线程等待与唤醒概述:
- 线程等待与唤醒又称为线程间通信。
- 等待唤醒机制是实现两个或多个线程在执行任务过程相互配合相互协作的一种技术。
线程等待与唤醒相关的方法:
void wait()
- 等待,让线程释放CPU的使用权进入无限等待状态。
- wait方法会自动释放锁,而sleep方法不会释放锁。
void notify()
- 唤醒,随机唤醒一个正在等待状态的线程,让其进入可运行状态。
void notifyAll()
- 唤醒所有,唤醒所有正在等待状态的线程,让其进入可运行状态。
以上三个方法必须由锁对象调用且必须在同步代码块或同步方法中调用。
否则会有:
- java.lang.IllegalMonitorStateException:非法监视器状态异常
sleep和wait方法的区别:
- sleep方法调用需要指定时间,wait方法调用可以指定时间,也可以不指定时间。
- sleep方法不需要被唤醒且不能中途被唤醒,而wait方法如果没有指定时间则需要被唤醒,有指定时间可以中途被唤醒。
- sleep方法不会释放锁对象,wait方法会释放锁对象。
- sleep方法可以在任意类任意方法中调用,wait方法必须通过锁对象在同步代码块或同步方法中调用。
示例代码:
需求: 为person类赋值并输出。
案例图解:
public class Demo {
public static void main(String[] args) {
Person p = new Person();
Setname s = new Setname(p);
Usename u = new Usename(p);
s.start();
u.start();
}
}
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Setname extends Thread {
Person p;
public Setname() {
}
public Setname(Person p) {
this.p = p;
}
public void run(){
int i = 0;
while(true){
if(i % 2 == 0){
p.setName("张三");
p.setAge(18);
}else{
p.setName("lisi");
p.setAge(20);
}
synchronized (p){
try {
p.notify();
p.wait();
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Usename extends Thread {
Person p;
public Usename(Person p) {
this.p = p;
}
public void run(){
while(true) {
synchronized (p){
if (p.getName() == null) {
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(p.getName() + " = " + p.getAge());
try {
p.notify();
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}