第一章 异常
异常的概述和体系
异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止
- 在
Java
等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java
处理异常的方式是中断处理。 Java
会为常见的代码异常都设计一个类来代表Java
中异常继承的根类是:Throwable
Error
: 错误的意思,严重错误Error
,无法通过处理的错误,一旦出现,程序员无能为力了,
- 只能重启系统,优化项目。
- 比如内存奔溃,
JVM
本身的奔溃。这个程序员无需理会。
Exception
:才是异常类,它才是开发中代码在编译或者执行的过程中可能出现的错误,
- 它是需要提前处理的。以便程序更健壮!
Exception
异常的分类:
- 编译时异常:继承自
Exception
的异常或者其子类,编译阶段就会报错,必须程序员处理的。否则代码编译就不能通过 - 运行时异常: 继承自
RuntimeException
的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现,运行时异常可以处理也可以不处理,编译阶段是不会出错的,但是运行阶段可能出现,还是建议提前处理
常见的运行时异常面试热点
继承自RuntimeException
的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过
- 数组索引越界异常:
ArrayIndexOutOfBoundsException
- 空指针异常 :
NullPointerException
直接输出没有问题。但是调用空指针的变量的功能就会报错 - 类型转换异常:
ClassCastException
- 迭代器遍历没有此元素异常:
NoSuchElementException
- 数学操作异常:
ArithmeticException
- 数字转换异常:
NumberFormatException
编译时异常
编译时异常:继承自Exception
的异常或者其子类,没有继承RuntimeException
- “编译时异常是编译阶段就会报错”,
- 必须程序员编译阶段就处理的。否则代码编译就报错
编译时异常的作用是什么:
- 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒
- 提醒程序员这里很可能出错,请检查并注意不要出bug
第二章 异常的处理
异常的产生、处理的默认过程
- 默认会在出现异常的代码那里自动的创建一个异常对象:
ArithmeticException
。 - 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给
JVM
虚拟机。 - 虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
- 直接从当前执行的异常点干掉当前程序。
- 后续代码没有机会执行了,因为程序已经死亡。
编译时异常处理机制
方法一
- 在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机。
- JVM虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。
抛出异常格式:
方法 throws 异常1 , 异常2 , ..{
}
建议抛出异常的方式:代表可以抛出一切异常,
方法 throws Exception{
}
虽然可以解决代码编译时的错误,但是一旦运行时真的出现异常,程序还是会立即死亡
方法二
在出现异常的地方自己处理,谁出现谁处理
try{
// 监视可能出现异常的代码!
}catch(异常类型1 变量){
// 处理异常
}catch(异常类型2 变量){
// 处理异常
}...
第二种方式,可以处理异常,并且出现异常后代码也不会死亡。这种方案还是可以的。但是从理论上来说,这种方式不是最好的,上层调用者不能直接知道底层的执行情况
try{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM-dd HH:mm:ss");
Date d = sdf.parse(time);
System.out.println(d);
InputStream is = new FileInputStream("D:/meinv.png");
} catch (FileNotFoundException e) {
System.err.println("文件根本不存在!");
} catch (ParseException e) {
System.err.println("解析有问题,请检查代码!");
}
方法三
在出现异常的地方吧异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理规范做法
这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡,这是
理论上最好的方案
public static void main(String[] args) {
System.out.println("程序开始。。。。");
try {
parseDate("2013-03-23 10:19:23");
System.out.println("功能成功执行!!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("功能执行失败!!");
}
System.out.println("程序结束。。。。。");
}
// 可以拦截所以异常!
public static void parseDate(String time) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(time);
System.out.println(d);
InputStream is = new FileInputStream("D:/meinv.png");
}
运行时异常的处理机制
运行时异常在编译阶段是不会报错,在运行阶段才会出错。运行时异常在编译阶段不处理也不会报错,但是运行时如果出错了程序还是会死亡。所以运行时异常也建议要处理。
运行时异常是自动往外抛出的,不需要我们手工抛出。
运行时异常的处理规范:直接在最外层捕获处理即可,底层会自动抛出
public static void main(String[] args) {
System.out.println("程序开始。。。。");
try{
chu(10 , 0);
System.out.println("操作成功!");
}catch (Exception e){
e.printStackTrace();
System.out.println("操作失败!");
}
System.out.println("程序结束。。。。");
}
public static void chu(int a , int b) {
System.out.println( a / b );
}
finally关键字
用在捕获处理的异常格式中的,放在最后面
无论代码是出现异常还是正常执行,最终一定要执行这里的代码
try{
// 可能出现异常的代码!
}catch(Exception e){
e.printStackTrace();
}finally{
// 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!!
}
finally
的作用: 可以在代码执行完毕以后进行资源的释放操作。
try{
//System.out.println(10/0);
is = new FileInputStream("D:/cang.png");
System.out.println(10 / 0 );
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("==finally被执行===");
// 回收资源。用于在代码执行完毕以后进行资源的回收操作!
try {
if(is!=null)is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
异常的注意事项
- 运行时异常被抛出可以不处理。可以自动抛出,编译时异常必须处理.按照规范都应该处理!
- 重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小
- 方法默认都可以自动抛出运行时异常!
throws RuntimeException
可以省略不写!! - 当多异常处理时,捕获处理,前边的异常类不能是后边异常类的父类。
- 在
try/catch
后可以追加finally
代码块,其中的代码一定会被执行,通常用于资源回收操作。
第三章 自定义异常
自定义编译时异常
- 定义一个异常类继承
Exception
- 重写构造器
- 在出现异常的地方用
throw new
自定义对象抛出 - 编译时异常是编译阶段就报错,提醒更加强烈,一定需要处理
public class ItheimaAgeIllegalException extends Exception {
public ItheimaAgeIllegalException() {
}
public ItheimaAgeIllegalException(String message) {
super(message);
}
public ItheimaAgeIllegalException(String message, Throwable cause) {
super(message, cause);
}
public ItheimaAgeIllegalException(Throwable cause) {
super(cause);
}
public ItheimaAgeIllegalException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
自定义运行时异常
- 定义一个异常类继承
RuntimeException
- 重写构造器
- 在出现异常的地方用
throw new
自定义对象抛出 - 提醒不强烈,编译阶段不报错,运行时才可能出现
public class ItheimaAgeIllegalRuntimeException extends RuntimeException {
public ItheimaAgeIllegalRuntimeException() {
}
public ItheimaAgeIllegalRuntimeException(String message) {
super(message);
}
public ItheimaAgeIllegalRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public ItheimaAgeIllegalRuntimeException(Throwable cause) {
super(cause);
}
public ItheimaAgeIllegalRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
异常的作用:
- 可以处理代码问题,防止程序出现异常后的死亡
- 提高了程序的健壮性和安全性
try{
Scanner sc = new Scanner(System.in);
System.out.println("请您输入您的年年龄:");
int age = sc.nextInt();
System.out.println("您是:"+age);
break;
}catch (Exception e){
System.err.println("您的年龄是瞎输入的!");
}
第四章 多线程
进程与线程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
进程的三个特征:
-
动态性 : 进程是运行中的程序,要动态的占用内存,CPU和网络等资源。
-
独立性 : 进程与进程之间是相互独立的,彼此有自己的独立内存区域。
-
并发性 : 假如
CPU
是单核,同一个时刻其实内存中只有一个进程在被执行。CPU
会分时轮询切换依次为每个进程服务,因为切换的速度非常快,给我们的感觉这些进程在同时执行,这就是并发性。
线程的作用
- 可以提高程序的效率,线程也支持并发性,可以有更多机会得到CPU。
- 多线程可以解决很多业务模型。
- 大型高并发技术的核心技术。
- 设计到多线程的开发可能都比较难理解。
线程常用方法
线程开启我们需要用到了java.lang.Thread
类,API中该类中定义了有关线程的一些方法,具体如下:
构造方法:
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法:
public void setName(String name)
:给当前线程取名字public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。
线程的创建方式一-继承方式
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
public class ThreadDemo {
// 启动后的ThreadDemo当成一个进程。
// main方法是由主线程执行的,理解成main方法就是一个主线程
public static void main(String[] args) {
// 3.创建一个线程对象
Thread t = new MyThread();
// 4.调用线程对象的start()方法启动线程,最终还是执行run()方法!
t.start();
for(int i = 0 ; i < 100 ; i++ ){
System.out.println("main线程输出:"+i);
}
}
}
// 1.定义一个线程类继承Thread类。
class MyThread extends Thread{
// 2.重写run()方法
@Override
public void run() {
// 线程的执行方法。
for(int i = 0 ; i < 100 ; i++ ){
System.out.println("子线程输出:"+i);
}
}
}
- 线程的启动必须调用
start()
方法,否则当成普通类处理
- 如果线程直接调用
run()
方法,相当于变成了普通类的执行,此时只有主线程在执行他们start()
方法底层其实是给CPU注册当前线程,并且触发run()
方法执行- 建议线程先创建子线程,主线程的任务放在之后,否则主线程永远是先执行完
线程创建方式二-实现方式
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
public class ThreadDemo {
public static void main(String[] args) {
// 3.创建一个线程任务对象(注意:线程任务对象不是线程对象,只是执行线程的任务的)
Runnable target = new MyRunnable();
// 4.把线程任务对象包装成线程对象.且可以指定线程名称
// Thread t = new Thread(target);
Thread t = new Thread(target,"1号线程");
// 5.调用线程对象的start()方法启动线程
t.start();
Thread t2 = new Thread(target);
// 调用线程对象的start()方法启动线程
t2.start();
for(int i = 0 ; i < 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}
// 1.创建一个线程任务类实现Runnable接口。
class MyRunnable implements Runnable{
// 2.重写run()方法
@Override
public void run() {
for(int i = 0 ; i < 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}
匿名内部类方式
这种方式是实现方式的匿名内部类写法,代码更加简洁
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
// }; //---这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
线程创建方式三-实现Callable接口
- 定义一个线程任务类实现Callable接口 , 申明线程执行的结果类型。
- 重写线程任务类的call方法,这个方法可以直接返回执行的结果。
- 创建一个Callable的线程任务对象。
- 把Callable的线程任务对象包装成一个未来任务对象。
- 把未来任务对象包装成线程对象。
- 调用线程的start()方法启动线程
这样做的优点是:
- 线程任务类只是实现了Callable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源(后面内容)
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现Runable或Callable线程任务对象。(后面了解)
- 能直接得到线程执行的结果!
public class ThreadDemo {
public static void main(String[] args) {
// 3.创建一个Callable的线程任务对象
Callable call = new MyCallable();
// 4.把Callable任务对象包装成一个未来任务对象
// -- public FutureTask(Callable<V> callable)
// 未来任务对象是啥,有啥用?
// -- 未来任务对象其实就是一个Runnable对象:这样就可以被包装成线程对象!
// -- 未来任务对象可以在线程执行完毕之后去得到线程执行的结果。
FutureTask<String> task = new FutureTask<>(call);
// 5.把未来任务对象包装成线程对象
Thread t = new Thread(task);
// 6.启动线程对象
t.start();
for(int i = 1 ; i <= 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => " + i);
}
// 在最后去获取线程执行的结果,如果线程没有结果,让出CPU等线程执行完再来取结果
try {
String rs = task.get(); // 获取call方法返回的结果(正常/异常结果)
System.out.println(rs);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 1.创建一个线程任务类实现Callable接口,申明线程返回的结果类型
class MyCallable implements Callable<String>{
// 2.重写线程任务类的call方法!
@Override
public String call() throws Exception {
// 需求:计算1-10的和返回
int sum = 0 ;
for(int i = 1 ; i <= 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => " + i);
sum+=i;
}
return Thread.currentThread().getName()+"执行的结果是:"+sum;
}
}
第五章 线程安全
线程安全问题:多个线程同时操作同一个共享资源的时候可能会出现线程安全问题
同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock) {
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
}
}
同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象 是 谁调用这个方法 就是谁
* 隐含 锁对象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
使用如下:
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}