13天Java进阶笔记-day7-异常、线程

本文详细讲解了Java中异常的体系,包括异常的分类、编译时异常和运行时异常的区别与处理机制,自定义异常的定义和作用,以及多线程中的线程安全问题。涵盖异常的产生、传递、处理和资源管理,提升程序健壮性与并发处理能力。
摘要由CSDN通过智能技术生成

第一章 异常

异常的概述和体系

异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止

  • Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。
  • Java会为常见的代码异常都设计一个类来代表
  • Java中异常继承的根类是:Throwable
image-20220603140634079

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类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的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方法即可。

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的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(同步锁){
     需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(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();
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值