异常与多线程
Posted lf-637
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了异常与多线程相关的知识,希望对你有一定的参考价值。
一、异常
1. 概述
异常:指的是程序在执行过程中,出现的非正常的情况,最终导致JVM虚拟机的非正常停止
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理
异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行
2. 异常体系
异常机制其实是帮助我们找到程序中的问题,Java把异常当作对象来处理,并定义一个基类 java.lang.Throwable
作为所有异常的超类。
Throwable中的常用方法:
public void printStackTrace():打印异常的详细信息。
包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace
public String getMessage()`:获取发生异常的原因
提示给用户的时候,就提示错误原因
出现异常,不要紧张,把异常的简单类名,拷贝到API中去查。
在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error
和异常Exception
- Exception:用户程序可能捕捉的异常情况或者说是程序可以处理的异常
- 编译期异常,进行编译(写代码)java程序出现的问题
- RuntimeException:运行期异常,java程序运行过程中出现的问题
- 异常就相当于程序得了一个小毛病。异常处理好,程序可以继续执行
- Error:不希望被程序捕获或者是程序无法处理的错误
- 错误就相当于程序得了大病,必须修改源代码,程序才能继续执行
Java异常层次结构图:
从图中可以看出所有异常类型都是内置类Throwable
的子类,因而 Throwable
在异常类的层次结构的顶层。
异常类 Exception
又分为运行时异常( RuntimeException )
和非运行时异常。
3. 异常之间的区别与联系
Error
Error 类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。
比如说:
- Java虚拟机运行错误,当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError 。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止
- 还有发生在虚拟机试图执行应用时,如类定义错误( NoClassDefFoundError )、链接错误( LinkageError )。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。
对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用 Error 的子类描述
Exception
在 Exception 分支中有一个重要的子类 RuntimeException (运行时异常)
,该类型的异常自动为编写的程序定义如:
ArrayIndexOutOfBoundsException (数组下标越界)、NullPointerException (空指针异常)ArithmeticException (算术异常)、 MissingResourceException (丢失资源)、ClassNotFoundException (找不到类)
等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。此异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;
而RuntimeException
之外的异常我们统称为非运行时异常,类型上属于Exception
类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException 、 SQLException
等以及用户自定义的 Exception
异常,一般情况下不自定义检查异常。
Error 和 Exception 的区别:
- Error 通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程
- Exception 通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常
4. Java异常处理机制
java异常处理本质:抛出异常和捕获异常
抛出异常
要理解抛出异常
- 首先要明白什么是异常情形,它是指阻止当前方法或作用域继续执行的问题
- 其次把异常情形和普通问题相区分
- 普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。
- 对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情
- 抛出异常后,会有几件事随之发生。
- 首先,是像创建普通的java对象一样将使用 new 在堆上创建一个异常对象
- 然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用
- 此时异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去
捕获异常
在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止
注意:
- 对于 运行时异常 、 错误 和 检查异常 ,Java技术所要求的异常处理方式有所不同
- 由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。
- 对于方法运行中可能出现的 Error ,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数 Error 异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常
对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常
二、异常的处理
Java异常处理的五个关键字:try、catch、finally、throw、throws
1. throw关键字
作用:
- 在java中提供了一个throw关键字,它用来抛出一个指定的异常对象。
格式:
throw new xxxException("异常产生的原因");
例如:
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
注意:
- throw关键字必须写在方法的内部
- throw关键字后边new的对象必须是Exception或者Exception的子类对象
- throw关键字抛出指定的异常对象,我们就必须处理这个异常对象
- throw关键字后边创建的是
RuntimeException
或者是RuntimeException
的子类对象,我们可以不处理,默认交给JVM
处理(打印异常对象,中断程序) - throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么
try...catch
- throw关键字后边创建的是
- 以后我们首先必须对方法传递过来的参数进行合法性校验
- 若参数不合法,就必须使用抛出异常的方式告知方法的调用者,床底的参数有问题
下面是一个例子:
public class Demo03Throw {
public static void main(String[] args) {
//int[] arr = null;
int[] arr = new int[3];
int e = getElement(arr,3);
System.out.println(e);
}
/*
定义一个方法,获取数组指定索引处的元素
参数:
int[] arr
int index
注意:
NullPointerException是一个运行期异常,我们不用处理,默认交给JVM处理
ArrayIndexOutOfBoundsException是一个运行期异常,我们不用处理,默认交给JVM处理
*/
public static int getElement(int[] arr,int index){
/*
对传递过来的参数数组,进行合法性校验
如果数组arr的值是null
那么我们就抛出空指针异常,告知方法的调用者"传递的数组的值是null"
*/
if(arr == null){
throw new NullPointerException("传递的数组的值是null");
}
/*
对传递过来的参数index进行合法性校验
如果index的范围不在数组的索引范围内
那么我们就抛出数组索引越界异常,告知方法的调用者"传递的索引超出了数组的使用范围"
*/
if(index<0 || index>arr.length-1){
throw new ArrayIndexOutOfBoundsException("传递的索引超出了数组的使用范围");
}
int ele = arr[index];
return ele;
}
}
注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。
那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续将问题声明出去,使用throws声明处理。
2. Objects非空判断
我们学习过一个类Objects,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的)。在它的源码中,对对象为null的值进行了抛出异常操作。
public static <T> T requireNonNull(T obj)
:查看指定引用对象不是null。
查看源码发现这里对为null的进行了抛出异常操作:
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
2.3 声明异常throws
throws关键字:异常处理的第一种方式,交给别人处理
- 关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
作用:
- 当方法内部抛出异常对象的时候,那么我们就必须处理这个异常对象
- 可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),最终交给JVM处理-->中断处理
使用格式:在方法声明时使用
修饰符 返回值类型 方法名(参数列表) throws AAAExcepiton,BBBExcepiton...{
throw new AAAExcepiton("产生原因");
throw new BBBExcepiton("产生原因");
...
}
注意:
- throws关键字必须写在方法声明处
- throws关键字后边声明的异常必须是Exception或者是Exception的子类
- 方法内部如果抛出了多个异常对象,那么throws后边必须也声明多个异常
- 如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可
- 调用了一个声明抛出异常的方法,我们就必须的处理声明的异常
- 要么继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM
- 要么try...catch自己处理异常声明异常
throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。
public class Demo05Throws {
/*
FileNotFoundException extends IOException extends Excepiton
如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可
*/
public static void main(String[] args) throws Exception {
readFile("c:\\a.tx");
System.out.println("后续代码");
}
/*
定义一个方法,对传递的文件路径进行合法性判断
如果路径不是"c:\\a.txt",那么我们就抛出文件找不到异常对象,告知方法的调用者
注意:
FileNotFoundException是编译异常,抛出了编译异常,就必须处理这个异常
可以使用throws继续声明抛出FileNotFoundException这个异常对象,让方法的调用者处理
*/
public static void readFile(String fileName) throws FileNotFoundException,IOException{
if(!fileName.equals("c:\\a.txt")){
throw new FileNotFoundException("传递的文件路径不是c:\\a.txt");
}
/*
如果传递的路径,不是.txt结尾
那么我们就抛出IO异常对象,告知方法的调用者,文件的后缀名不对
*/
if(!fileName.endsWith(".txt")){
throw new IOException("文件的后缀名不对");
}
System.out.println("路径没有问题,读取文件");
}
}
4. 捕获异常try…catch
如果出现异常立刻终止程序,所以我们需要处理异常:
- 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
- 在方法中使用try-catch的语句块来处理异常。
try-catch的方式就是捕获异常,是异常处理的第二种方式,自己处理异常
- 捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。
格式:
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
//try:该代码块中编写可能产生异常的代码。
//catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。
注意:
- try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
- 如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑,继续执行try...catch之后的代码
- 如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的代码,继续执行try...catch之后的代码
- try和catch都不能单独使用,必须连用
演示如下:
public class Demo01TryCatch {
public static void main(String[] args) {
try{
//可能产生异常的代码
readFile("d:\\a.tx");
System.out.println("资源释放");
}catch (IOException e){//try中抛出什么异常对象,catch就定义什么异常变量,用来接收这个异常对象
//异常的处理逻辑,异常异常对象之后,怎么处理异常对象
//System.out.println("catch - 传递的文件后缀不是.txt");
//System.out.println(e.getMessage());//文件的后缀名不对
//System.out.println(e.toString());//重写Object类的toString java.io.IOException: 文件的后缀名不对
//System.out.println(e);//java.io.IOException: 文件的后缀名不对
e.printStackTrace();
}
System.out.println("后续代码");
}
/*
如果传递的路径,不是.txt结尾
那么我们就抛出IO异常对象,告知方法的调用者,文件的后缀名不对
*/
public static void readFile(String fileName) throws IOException {
if(!fileName.endsWith(".txt")){
throw new IOException("文件的后缀名不对");
}
System.out.println("路径没有问题,读取文件");
}
}
如何获取异常信息:
Throwable类中定义了一些查看方法
-
public String getMessage()
,获取异常的描述信息、原因提示给用户,即返回此异常的简短描述 -
public String toString()
,重写Object类的toString方法,获取异常的类型和异常描述信息 -
public void printStackTrace()
,JVM打印异常对象,默认此方法,打印的异常信息是最全面的
? 包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
5. finally 代码块
finally:有一些特定的代码无论异常是否发生都需要执行,若因为异常,会引发程序跳转导致有些语句执行不到。而finally就是解决这个问题,在finally代码块中存放的代码都是一定会被执行的。
什么时候的代码必须最终执行?
当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。
格式:
try{
可能产生异常的代码
}catch(定义一个异常的变量,用来接收try中抛出的异常对象){
异常的处理逻辑,异常异常对象之后,怎么处理异常对象
一般在工作中,会把异常的信息记录到一个日志中
}
...
catch(异常类名 变量名){
}finally{
无论是否出现异常都会执行
}
注意:
- finally不能单独使用,必须和try一起使用
- finally一般用于资源释放(资源回收),无论程序是否出现异常,最后都要资源释放(IO)
- 如果finally中有return语句程序永远返回finally中的结果。需避免这种情况发生。不要在final语句中使用return
比如在我们之后学习的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。
finally代码参考如下:
public class Demo02TryCatchFinally {
public static void main(String[] args) {
try {
//可能会产生异常的代码
readFile("c:\\a.tx");
} catch (IOException e) {
//异常的处理逻辑
e.printStackTrace();
} finally {
//无论是否出现异常,都会执行
System.out.println("资源释放");
}
}
/*
如果传递的路径,不是.txt结尾
那么我们就抛出IO异常对象,告知方法的调用者,文件的后缀名不对
*/
public static void readFile(String fileName) throws IOException {
if(!fileName.endsWith(".txt")){
throw new IOException("文件的后缀名不对");
}
System.out.println("路径没有问题,读取文件");
}
}
只有在try或者catch中调用退出JVM的相关方法,finally才不会执行,否则会永远执行
6. 异常注意事项
-
多个异常使用捕获处理方法如下
- 多个异常分别处理。
- 多个异常一次捕获,多次处理。
- 多个异常一次捕获一次处理。
一般是使用一次捕获多次处理方式,格式如下:
try{ 编写可能会出现异常的代码 }catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获. 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 }catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获. 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 }
注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。
-
运行时异常被抛出可以不处理。即不捕获也不声明抛出。
-
如果finally有return语句,永远返回finally中的结果,避免该情况.
子父类异常
- 如果父类抛出了多个异常,子类重写父类方法时有三种情况
- 抛出和父类相同的异常
- 抛出父类异常的子类
- 不抛出异常
- 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常
- 此时子类产生该异常,只能捕获处理,不能声明抛出
注意:
- 父类异常时什么样,子类异常就什么样
三、自定义异常
1. 概述
Java中有很多种异常类,分别表示某一种具体的异常情况。但实际中总是有些异常情况是没有定义好的,此时我们可以根据遇到的的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题等等。
定义:
- java提供的异常类不够我们使用,需要自己定义一些异常类
- 自定义一个编译期异常: 自定义类 并继承于
java.lang.Exception
。 - 自定义一个运行时期的异常类:自定义类 并继承于
java.lang.RuntimeException
格式:
public class XXXExcepiton extends Exception | RuntimeException{
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
注意:
- 自定义异常类一般都是以Exception结尾,说明该类是一个异常类
- 自定义异常类,必须的继承Exception或者RuntimeException
- 继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try...catch
- 继承RuntimeException:那么自定义的异常类就是一个运行期异常,无需处理,交给虚拟机处理(中断处理)
例子:
要求:模拟注册操作,如果用户名已存在,则抛出异常并提示:该用户名已经被注册
分析:
- 使用数组保存已经注册过的用户名(数据库)
- 使用Scanner获取用户输入的注册的用户名(前端,页面)
- 定义一个方法,对用户输入的中注册的用户名进行判断
- 遍历存储已经注册过用户名的数组,获取每一个用户名
- 使用获取到的用户名和用户输入的用户名比较
- true:
- 用户名已经存在,抛出RegisterException异常,告知用户"该用户名已经被注册";
- false:
- 继续遍历比较
- 如果循环结束了,还没有找到重复的用户名,提示用户"恭喜您,注册成功!";
- true:
public class RegisterException {
// 1.使用数组保存已经注册过的用户名(数据库)
static String[] usernames = {"张三","李四","王五"};
public static void main(String[] args) /*throws RegisterException*/ {
//2.使用Scanner获取用户输入的注册的用户名(前端,页面)
Scanner sc = new Scanner(System.in);
System.out.println("请输入您要注册的用户名:");
String username = sc.next();
checkUsername(username);
}
//3.定义一个方法,对用户输入的中注册的用户名进行判断
public static void checkUsername(String username) /*throws RegisterException*/ {
//遍历存储已经注册过用户名的数组,获取每一个用户名
for (String name : usernames) {
//使用获取到的用户名和用户输入的用户名比较
if(name.equals(username)){
//true:用户名已经存在,抛出RegisterException异常,告知用户"该用户名已经被注册";
try {
throw new RegisterException("该用户名已经被注册");
} catch (RegisterException e) {
e.printStackTrace();
return; //结束方法
}
}
}
//如果循环结束了,还没有找到重复的用户名,提示用户"恭喜您,注册成功!";
System.out.println("恭喜您,注册成功!");
}
}
第四章 多线程
Java给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。
- 多线程能满足程序员编写非常有效率的程序来达到充分利用CPU的目的,因为CPU的空闲时间能够保持在最低限度。
1. 并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
2. 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行。
- 一个线程不能独立的存在,它必须是进程的一部分
- 一个进程中是可以有多个线程的,这个应用程序也称之为多线程程序。
- 一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程调度:
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
3. 创建线程类
Java提供了三种创建线程方法:
- 通过实现Runnable接口
- 通过继承Thread类本身
- 通过 Callable 和 Future 创建线程
创建多线程程序的第一种方式:创建Thread类的子类
Java使用java.lang.Thread
是描述线程的类,所有的线程对象都必须是Thread类或其子类的实例。我们想要实现多线程程序,就必须继承Thread类
实现步骤:
- 创建一个Thread类的子类
- 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
- 创建Thread类的子类对象
- 用Thread类中的方法start方法,开启新的线程,执行run方法
- void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法
- 结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其 run 方法)
- 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动
java程序属于抢占式调度,那个线程的优先级高,那个线程优先执行;同一个优先级,随机选择一个执行
代码如下:
测试类:
public class Demo01Thread {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用Thread类中的方法start方法,开启新的线程,执行run方法
mt.start();
for (int i = 0; i <20 ; i++) {
System.out.println("main:"+i);
}
}
}
自定义线程类:
//1.创建一个Thread类的子类
public class MyThread extends Thread{
//2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println("run:"+i);
}
}
}
以上是关于异常与多线程的主要内容,如果未能解决你的问题,请参考以下文章
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第5节 线程池_2_线程池的代码实现
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_3_线程安全问题产生的原理
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第4节 等待唤醒机制_8_等待唤醒机制代码实现_包子类&包子铺类