java(七)【异常、线程】

主要内容

  • 异常

    • 异常是在程序开发的过程中可能出现的错误。数组越界异常,空指针异常。迭代器
      • 有时候是程序员的技术问题引起。
    • 异常一旦出现且不处理的话,程序会死亡!!
    • 异常应该避免,但可能也是无法绝对避免,所以应该提前处理异常。
    • 研究异常,认识异常,避免异常,处理异常体现:体现的是程序的健壮性和安全!!
  • 多线程

    • 重点内容,设计到多线程的开发都显得有点难理解。

    • 多线程是Java的经典技术之一。

      多线程是未来高并发技术的基石,也是基础!! !

教学目标

  • 能够辨别程序中异常和错误的区别

    • 错误是无法解决的,出现了就要重启环境,JVM奔溃
    • 异常才是程序在编译或者执行的过程中可能出现的问题。是我们应该避免且处理的。
  • 说出异常的分类

    • 编译时异常:继承自Exception, 编译阶段就报错,必须处理。
    • 运行时异常:继承自RuntimeException。编译阶段不报错,运行阶段才可能出现!
  • 说出虚拟机处理异常的方式

    • 打印异常信息,干掉程序!
  • 列举出常见的三个运行期异常

    1.数组索引越界异常: ArrayIndexOutOfBoundsException
    2.空指针异常 : NullPointerException
      直接输出没有问题。但是调用空指针的变量的功能就会报错!!
    3.类型转换异常:ClassCastException
    4.迭代器遍历没有此元素异常:NoSuchElementException
    5.数学操作异常:ArithmeticException
    6.数字转换异常: NumberFormatException
    
  • 能够使用try…catch关键字处理异常

    自己捕获异常和处理异常的格式:捕获处理
        try{
            // 可能出现异常的代码!
        }catch(异常类型1 变量){
            // 处理异常
        }catch(异常类型2 变量){
            // 处理异常
        }...
    
    捕获处理异常企业级写法:
         try{
             // 可能出现异常的代码!
         }catch (Exception e){
            e.printStackTrace(); // 直接打印异常栈信息
         }
         可以捕获处理一切异常类型!
    
  • 能够使用throws关键字处理异常

    • 直接在方法上申明抛出!
  • 能够自定义异常类

    自定义异常:
        自定义编译时异常.
            a.定义一个异常类继承Exception.
            b.重写构造器。
            c.在出现异常的地方用throw new 自定义对象抛出!
            编译时编译阶段就报错,提醒更加强烈,一定需要处理!!
    
        自定义运行时异常.
            a.定义一个异常类继承RuntimeException.
            b.重写构造器。
            c.在出现异常的地方用throw new 自定义对象抛出!
            提醒不强烈,编译阶段不报错!!
    
  • 能够处理自定义异常类

    • 一样的。要么自己捕获处理,要么抛出去。
  • 说出进程的概念

    • 进程是运行中的程序。
  • 说出线程的概念

    • 线程是属于进程的 ,一个进程可以包含多个线程。
  • 能够理解并发与并行的区别

    • 并发:是一堆线程来抢占CPU执行自己!!
    • 并行:同时有多个线程执行!!
  • 能够开启新线程

    • 调用start()方法。
  • 能够描述Java中多线程运行原理

    • 并发执行,出现随机性。
  • 能够使用继承类的方式创建多线程

    a.定义一个线程类继承Thread类。
    b.重写Thread类的run()方法
    c.创建线程类的对象。
    d.调用线程类对象的start()方法启动线程
    
  • 能够使用实现接口的方式创建多线程

    a.定义一个线程任务类实现Runnable接口。重写run()方法
    b.创建一个线程任务对象
    c.把线程任务对象包装成一个线程对象
        -- public Thread(Runnable target)
    d.调用线程对象的start()方法启动线程。
    
  • 能够说出实现接口方式的好处

    实现接口以后,线程任务对象可以继续继承其他类或者继续实现其他接口,以后的功能可以扩展。
    实现接口适合做线程池。
    适合做资源共享操作
    

第一章 异常

1.1 异常概念

异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:

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

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.

1.2 异常体系

异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception

在这里插入图片描述

Throwable体系:

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

Throwable中的常用方法:

  • public void printStackTrace():打印异常的详细信息。

    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

  • public String getMessage():获取发生异常的原因。

    提示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

出现异常,不要紧张,把异常的简单类名,拷贝到API中去查。

在这里插入图片描述

1.3 异常分类

我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。

异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?

  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
  • 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)

在这里插入图片描述

总结
目标:异常的概念和体系。

什么是异常?
     异常是程序在"编译"或者"执行"的过程中可能出现的问题。
     异常是应该尽量提前避免的。
     异常可能也是无法做到绝对避免的,异常可能有太多情况了,开发中只能提前干预!!
     异常一旦出现了,如果没有提前处理,程序就会退出JVM虚拟机而终止,开发中异常是需要提前处理的。

     研究异常并且避免异常,然后提前处理异常,体现的是程序的安全, 健壮性!!!

     Java会为常见的代码异常都设计一个类来代表。

异常的体系:
     Java中异常继承的根类是:Throwable。

         Throwable(根类,不是异常类)
      /              \
    Error           Exception(异常,需要研究和处理)
                    /            \
                   编译时异常     RuntimeException(运行时异常)


    Error : 错误的意思,严重错误Error,无法通过处理的错误,一旦出现,程序员无能为力了,
        只能重启系统,优化项目。
        比如内存奔溃,JVM本身的奔溃。这个程序员无需理会。

    Exception:才是异常类,它才是开发中代码在编译或者执行的过程中可能出现的错误,
        它是需要提前处理的。以便程序更健壮!

Exception异常的分类:
     1.编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错,
            必须程序员处理的。否则代码编译就不能通过!!

     2.运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在
            运行时阶段可能出现,运行时异常可以处理也可以不处理,编译阶段是不会出错的,
            但是运行阶段可能出现,还是建议提前处理!!
小结:
    异常是程序在编译或者运行的过程中可能出现的错误!!
    异常分为2类:编译时异常,运行时异常。
        -- 编译时异常:继承了Exception,编译阶段就报错,必须处理,否则代码不通过。
        -- 运行时异常:继承了RuntimeException,编译阶段不会报错,运行时才可能出现。
    异常一旦真的出现,程序会终止,所以要研究异常,避免异常,处理异常,程序更健壮!!

空指针异常:变量为空指针的变量,引用类型没有指向任何变量,无任何指向,输出是输出没有地址,故直接输出没有问题

报错的原因都能在api上搜的到

在这里插入图片描述

抛出异常不代表系统会自动处理该异常,该报错照样报错


①运行时候的异常:

运行时异常的概念:
继承自RuntimeException的异常或者其子类,
编译阶段是不会出错的,它是在运行时阶段可能出现的错误,
运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!!

1.数组索引越界异常: ArrayIndexOutOfBoundsException。
2.空指针异常 : NullPointerException。
直接输出没有问题。但是调用空指针的变量的功能就会报错!!
3.类型转换异常:ClassCastException。
4.迭代器遍历没有此元素异常:NoSuchElementException。
5.数学操作异常:ArithmeticException。
6.数字转换异常: NumberFormatException。

小结:
运行时异常继承了RuntimeException ,编译阶段不报错,运行时才可能会出现错误!

代码:

public static void main(String[] args) {
    System.out.println("程序开始。。。。。。");
    /** 1.数组索引越界异常: ArrayIndexOutOfBoundsException。*/
    int[] arrs = {10 ,20 ,30};
    System.out.println(arrs[2]);
    // System.out.println(arrs[3]); // 此处出现了数组索引越界异常。代码在此处直接执行死亡!

    /** 2.空指针异常 : NullPointerException。直接输出没有问题。但是调用空指针的变量的功能就会报错!! */
    String name = null ;
    System.out.println(name); // 直接输出没有问题
    // System.out.println(name.length());  // 此处出现了空指针异常。代码在此处直接执行死亡!

    /** 3.类型转换异常:ClassCastException。 */
    Object o = "齐天大圣";
    //Integer s = (Integer) o;  // 此处出现了类型转换异常。代码在此处直接执行死亡!


    /** 5.数学操作异常:ArithmeticException。 */
    // int c = 10 / 0 ; // 此处出现了数学操作异常。代码在此处直接执行死亡!


    /** 6.数字转换异常: NumberFormatException。 */
    String num = "23aa";
    Integer it = Integer.valueOf(num); // 此处出现了数字转换异常。代码在此处直接执行死亡!
    System.out.println(it+1);

    System.out.println("程序结束。。。。。。");
}

②编译时候的异常:

目标:常见的编译时异常认识。

编译时异常:继承自Exception的异常或者其子类,没有继承RuntimeException
           "编译时异常是编译阶段就会报错",
           必须程序员编译阶段就处理的。否则代码编译就报错!!

编译时异常的作用是什么:
        是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒!
        提醒程序员这里很可能出错,请检查并注意不要出bug。

        编译时异常是可遇不可求。遇到了就遇到了呗。
小结:
    编译时异常是编译阶段就会报错的,继承了Exception,编译时
    异常是可遇不可求。遇到了就遇到了呗。

    编译时异常编译阶段必须处理,否则代码编译不通过!!

1.4 异常的产生过程解析

先运行下面的程序,程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生的过程。

工具类

public class ArrayTools {
    // 对给定的数组通过给定的角标获取元素。
    public static int getElement(int[] arr, int index) {
        int element = arr[index];
        return element;
    }
}

测试类

public class ExceptionDemo {
    public static void main(String[] args) {
        int[] arr = { 34, 12, 67 };
        intnum = ArrayTools.getElement(arr, 4)
        System.out.println("num=" + num);
        System.out.println("over");
    }
}

上述程序执行过程图解:

在这里插入图片描述

总结

异常的流程

在这里插入图片描述

因为栈是先进后出,先执行main方法,但是main方法的错误信息是最后打印的

报错的地方按alt+enter可以抛出异常


接抛Exception异常,就可以代表一切异常

在这里插入图片描述

第二章 异常的处理

Java异常处理的五个关键字:try、catch、finally、throw、throws

2.1 抛出异常throw

在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。

在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?

  1. 创建一个异常对象。封装一些提示信息(信息可以自己编写)。

  2. 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw就可以完成。throw 异常对象。

    throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:

throw new 异常类名(参数);

例如:

throw new NullPointerException("要访问的arr数组不存在");

throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

学习完抛出异常的格式后,我们通过下面程序演示下throw的使用。

public class ThrowDemo {
    public static void main(String[] args) {
        //创建一个数组 
        int[] arr = {2,4,52,2};
        //根据索引找对应的元素 
        int index = 4;
        int element = getElement(arr, index);

        System.out.println(element);
        System.out.println("over");
    }
    /*
     * 根据 索引找到数组中对应的元素
     */
    public static int getElement(int[] arr,int index){ 
       	//判断  索引是否越界
        if(index<0 || index>arr.length-1){
             /*
             判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
             这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 
              */
             throw new ArrayIndexOutOfBoundsException("哥们,角标越界了```");
        }
        int element = arr[index];
        return element;
    }
}

注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。

2.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

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

声明异常格式:

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }	

声明异常的代码演示:

public class ThrowsDemo {
    public static void main(String[] args) throws FileNotFoundException {
        read("a.txt");
    }

    // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。

public class ThrowsDemo2 {
    public static void main(String[] args) throws IOException {
        read("a.txt");
    }

    public static void read(String path)throws FileNotFoundException, IOException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
        if (!path.equals("b.txt")) {
            throw new IOException();
        }
    }
}
总结

编译时异常

方法一:

直接抛异常

  目标:异常的产生默认的处理过程解析。(自动处理的过程!)
   (1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException。
   (2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。
   (3)虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据。
   (4)直接从当前执行的异常点干掉当前程序。
   (5)后续代码没有机会执行了,因为程序已经死亡。

小结:
    异常一旦出现,会自动创建异常对象,最终抛出给虚拟机,虚拟机
    只要收到异常,就直接输出异常信息,干掉程序!!

    默认的异常处理机制并不好,一旦真的出现异常,程序立即死亡!

代码:

public static void parseDate(String time) throws Exception{

2.4 捕获异常try…catch

如果异常出现的话,会立刻终止程序,所以我们得处理异常:

  1. 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
  2. 在方法中使用try-catch的语句块来处理异常。

try-catch的方式就是捕获异常。

  • 捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。

捕获异常语法如下:

try{
     编写可能会出现异常的代码
}catch(异常类型  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

**try:**该代码块中编写可能产生异常的代码。

**catch:**用来进行某种异常的捕获,实现对捕获到的异常进行处理。

注意:try和catch都不能单独使用,必须连用。

演示如下:

public class TryCatchDemo {
    public static void main(String[] args) {
        try {// 当产生异常时,必须有处理方式。要么捕获,要么声明。
            read("b.txt");
        } catch (FileNotFoundException e) {// 括号中需要定义什么呢?
          	//try中抛出的是什么异常,在括号中就定义什么异常类型
            System.out.println(e);
        }
        System.out.println("over");
    }
    /*
     *
     * 我们 当前的这个方法中 有异常  有编译期异常
     */
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

如何获取异常信息:

Throwable类中定义了一些查看方法:

  • public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

  • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。

包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

在开发中呢也可以在catch将编译期异常转换成运行期异常处理。

多个异常使用捕获又该如何处理呢?

  1. 多个异常分别处理。
  2. 多个异常一次捕获,多次处理。
  3. 多个异常一次捕获一次处理。

一般我们是使用一次捕获多次处理方式,格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型A  e){try中出现A类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型B  e){try中出现B类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

总结

方法二:

要捕获异常只需要,try{代码},catch语句能用报红的地方按ait+enter来进行自动生成

在这里插入图片描述


多个异常,catch来拦截异常的顺序不做要求

在这里插入图片描述

多个异常可以并列捕获,然后用一个打印自身的异常日志信息

在这里插入图片描述

可以拦截一切异常,并打印自身的异常日志信息

在这里插入图片描述

代码:

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 (Exception e) {
            e.printStackTrace(); // 打印异常栈信息
        }
目标:编译时异常的处理方式二。

方式二:在出现异常的地方自己处理,谁出现谁处理。

自己捕获异常和处理异常的格式:捕获处理
     try{
        // 监视可能出现异常的代码!
     }catch(异常类型1 变量){
        // 处理异常
     }catch(异常类型2 变量){
        // 处理异常
     }...

监视捕获处理异常企业级写法:
     try{
         // 可能出现异常的代码!
     }catch (Exception e){
        e.printStackTrace(); // 直接打印异常栈信息
     }
     Exception可以捕获处理一切异常类型!

小结:
    第二种方式,可以处理异常,并且出现异常后代码也不会死亡。
    这种方案还是可以的。
    但是从理论上来说,这种方式不是最好的,上层调用者不能直接知道底层的执行情况!

方法三

方法出现异常,就抛给调用者,调用者再用try catch来捕获异常进行处理

在这里插入图片描述

流程

在这里插入图片描述

目标:编译时异常的处理方式三。

方式三: 在出现异常的地方把异常一层一层的抛出给最外层调用者,
        最外层调用者集中捕获处理!!(规范做法)

小结:
    编译时异常的处理方式三:底层出现的异常抛出给最外层调用者集中捕获处理。
    这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡,这是
    理论上最好的方案。

    虽然异常有三种处理方式,但是开发中只要能解决你的问题,每种方式都又可能用到!!

代码:

 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");
    }

运行时异常

运行时异常默认会抛出RuntimeException

运行时候的异常是不应该出现,是自己的能力的问题,成员能自己避免;编译时候的异常是设计上的提醒,担心你们技术上的不行

运行时候的异常处理直接在代码处try catch即可

在这里插入图片描述

代码:

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 );
}
目标:运行时异常的处理机制。

运行时异常在编译阶段是不会报错,在运行阶段才会出错。
运行时异常在编译阶段不处理也不会报错,但是运行时如果出错了程序还是会死亡
所以运行时异常也建议要处理。

运行时异常是自动往外抛出的,不需要我们手工抛出。

运行时异常的处理规范:直接在最外层捕获处理即可,底层会自动抛出!!

小结:
     运行时异常编译阶段不报错,可以处理也可以不处理,建议处理!!
     运行时异常可以自动抛出,不需要我们手工抛出。
     运行时异常的处理规范:直接在最外层捕获处理即可,底层会自动抛出!!

2.5 finally 代码块

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

什么时候的代码必须最终执行?

当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

finally的语法:

try…catch…finally:自身需要处理异常,最终还得关闭资源。

注意:finally不能单独使用。

比如在我们之后学习的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。

finally代码参考如下:

public class TryCatchDemo4 {
    public static void main(String[] args) {
        try {
            read("a.txt");
        } catch (FileNotFoundException e) {
            //抓取到的是编译期异常  抛出去的是运行期 
            throw new RuntimeException(e);
        } finally {
            System.out.println("不管程序怎样,这里都将会被执行。");
        }
        System.out.println("over");
    }
    /*
     *
     * 我们 当前的这个方法中 有异常  有编译期异常
     */
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

在这里插入图片描述

总结

很多时候都需要做非空校验

将变量设置为全局变量,各处调用

有返回值的类型一定要返回相应类型的值

在这里插入图片描述

return 0;//返回0,0作为特殊的标记,与其他返回值区分开

return true与return false 返回值作为是否继续执行的依据

return语句执行,方法就已经结束了,而加finally语句,会先执行finally语句,再执行return语句

在这里插入图片描述

在finally语句中加return,结果永远是finally语句中的返回值,会覆盖所有前面的return值,不推荐使用

在这里插入图片描述

如果是虚拟机退出语句,则不会执行finally语句

在这里插入图片描述

目标:finally关键字

用在捕获处理的异常格式中的,放在最后面。
    try{
        // 可能出现异常的代码!
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        // 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!!
    }
    try: 1次。
    catch:0-N次  (如果有finally那么catch可以没有!!)
    finally: 0-1次

finally的作用: 可以在代码执行完毕以后进行资源的释放操作。
什么是资源?资源都是实现了Closeable接口的,都自带close()关闭方法!!

2.6 异常注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
  • 如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。
  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
  • 当多异常处理时,捕获处理,前边的类不能是后边类的父类
  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。
总结

如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集:子类的异常返回一定是和父类的异常相同,或者是更小

在这里插入图片描述

当多异常处理时,捕获处理,前边的类不能是后边类的父类:Exception是ParseException的父类,所以报错

在这里插入图片描述

第三章 自定义异常

3.1 概述

为什么需要自定义异常类:

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。

在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?

什么是自定义异常类:

在开发中根据自己业务的异常情况来定义异常类.

自定义一个业务逻辑异常: LoginException。一个登陆异常类。

异常类如何定义:

  1. 自定义一个编译期异常: 自定义类 并继承于java.lang.Exception
  2. 自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException

3.2 自定义异常的练习

要求:我们模拟登陆操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

首先定义一个登陆异常类LoginException:

// 业务逻辑异常
public class LoginException extends Exception {
    /**
     * 空参构造
     */
    public LoginException() {
    }

    /**
     *
     * @param message 表示异常提示
     */
    public LoginException(String message) {
        super(message);
    }
}

模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。

public class Demo {
    // 模拟数据库中已存在账号
    private static String[] names = {"bill","hill","jill"};
   
    public static void main(String[] args) {     
        //调用方法
        try{
              // 可能出现异常的代码
            checkUsername("nill");
            System.out.println("注册成功");//如果没有异常就是注册成功
        }catch(LoginException e){
            //处理异常
            e.printStackTrace();
        }
    }

    //判断当前注册账号是否存在
    //因为是编译期异常,又想调用者去处理 所以声明该异常
    public static boolean checkUsername(String uname) throws LoginException{
        for (String name : names) {
            if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
                throw new LoginException("亲"+name+"已经被注册了!");
            }
        }
        return true;
    }
}
总结

重写构造器

将自定义异常往上抛,再调用方法的地方进行try catch

在这里插入图片描述

目标:自定义异常(了解)

引入:Java已经为开发中可能出现的异常都设计了一个类来代表.
    但是实际开发中,异常可能有无数种情况,Java无法为
    这个世界上所有的异常都定义一个代表类。
    假如一个企业如果想为自己认为的某种业务问题定义成一个异常
    就需要自己来自定义异常类.

需求:认为年龄小于0岁,大于200岁就是一个异常。

自定义异常:
    自定义编译时异常.
        a.定义一个异常类继承Exception.
        b.重写构造器。
        c.在出现异常的地方用throw new 自定义对象抛出!
        编译时异常是编译阶段就报错,提醒更加强烈,一定需要处理!!

    自定义运行时异常.
        a.定义一个异常类继承RuntimeException.
        b.重写构造器。
        c.在出现异常的地方用throw new 自定义对象抛出!
        提醒不强烈,编译阶段不报错!!运行时才可能出现!!
小结:
    自定义异常是程序员自己定义的异常
    继承Exception/RuntimeException,重写构造器。
    在出现异常的地方用throw new 自定义异常对象抛出!

运行时异常:

代码:

ItheimaAgeIllegalRuntimeException.java

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);
    }
}

ExceptionDemo.java

public class ExceptionDemo {
    public static void main(String[] args) {

            checkAge(101);

    }

    public static void checkAge(int age) throws ItheimaAgeIllegalRuntimeException {
        if(age < 0 || age > 200){
            // 出现异常了!
            // throws:用在方法上,用于抛出方法中的异常。
            // throw:用在出现异常的地方,用于创建异常对象且立即从此处抛出!
            //throw new ItheimaAgeIllegalException("/ age is illegal!");
            throw new ItheimaAgeIllegalRuntimeException("/ age is illegal!");
        }else{
            System.out.println("年龄是:"+age);
        }
    }
}

自定义运行时异常不用在调用处try catch

异常的强大之处
总结

单独定义变量,这个变量的范围只在小括号内有效

异常可以在容易报错的地方添加,优化代码

在这里插入图片描述****

报错的部分由系统底层处理,自己写报错完执行的代码

拓展:异常的作用
    1.可以处理代码问题,防止程序出现异常后的死亡。
    2.提高了程序的健壮性和安全性。

第四章 多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

4.1 并发与并行

  • 并行:指两个或多个事件在同一时刻发生(同时执行)。
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。

在这里插入图片描述

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

4.2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

进程

在这里插入图片描述

线程

在这里插入图片描述

进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

**注意:**下面内容为了解知识点

1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程调度:

  • 分时调度

    ​ 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    ​ 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

总结

cpu有几核就代表有几个进程同时执行

线程与进程的关系

在这里插入图片描述

目标:多线程的概述。(并发编程)

什么是进程?
     程序是静止的,运行中的程序就是进程。
     进程的三个特征:
     1.动态性 : 进程是运行中的程序,要动态的占用内存,CPU和网络等资源。
     2.独立性 : 进程与进程之间是相互独立的,彼此有自己的独立内存区域。
     3.并发性 : 假如CPU是单核,同一个时刻其实内存中只有一个进程在被执行。
                CPU会分时轮询切换依次为每个进程服务,因为切换的速度非常
                快,给我们的感觉这些进程在同时执行,这就是并发性。

    并行:同一个时刻同时有多个在执行。

什么是线程?
     线程是属于进程的。一个进程可以包含多个线程,这就是多线程。
     线程是进程中的一个独立执行单元。
     线程创建开销相对于进程来说比较小。
     线程也支持“并发性”。

线程的作用:
     可以提高程序的效率,线程也支持并发性,可以有更多机会得到CPU。
     多线程可以解决很多业务模型。
     大型高并发技术的核心技术。
     设计到多线程的开发可能都比较难理解。

4.3 Thread类

线程常用api

线程开启我们需要用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

构造方法:

  • public Thread():分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,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接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。

线程休眠的方法sleep
public static void sleep(long time): 让当前线程休眠多少毫秒再继续执行。

代码

Thread.sleep(1000); // 让当前线程休眠1s.
线程取名字通过有参数构造器实现

创建线程时,线程的属性直接在参数中进行设置,按alt+Enter,让系统自动帮我们创建有参构造器

在这里插入图片描述

线程类中创建线程类的有参构造器,用super的方法将参数送到父类中,调用父类的有参构造器,父类Thread中有定义有参构造器,子类只需定义名称和类型,就是覆盖父类进行使用

在这里插入图片描述

用有参构造器可以简化代码

目标:通过Thread类的有参数构造器为当前线程对象取名字。
    -- public Thread()
    -- public Thread(String name):创建线程对象并取名字。
总结

如果一个继承类里的run方法,被多个继承对象共享,那么多个继承对象将会得到过各自的run方法的代码。互不干扰

在这里插入图片描述

获取主线程对象

public static void main(String[] args) {
   // 主线程的名称如何获取呢?
    // 这个代码在哪个线程中,就得到哪个线程对象。
    Thread m = Thread.currentThread();//获取主线程的对象,由于这个方法在main方法执行,所以获取的就是主线程的对象
    m.setName("最强线程main");
    System.out.println(m.getName()); // 获取线程名称
}

设置线程的名字最好在线程启动之前

// 创建一个线程对象
Thread t1 = new MyThread();
t1.setName("1号线程");
t1.start();

api中有static 修饰或者这没有,就代表api的调用方式的不同,有static修饰的就用类名调用;如果没有,表示为实名方法,就用对象调用,api中都表示特定的含义,public 代表权限,void代表返回值, static代表静态方法,(String name)代表入口参数类型

1.public void setName(String name):给当前线程取名字。
目标:线程的常用API.

Thread类的API:
    1.public void setName(String name):给当前线程取名字。
    2.public void getName():获取当前线程的名字。
        -- 线程存在默认名称,子线程的默认名称是:Thread-索引。
        -- 主线程的默认名称就是:main
    3.public static Thread currentThread()
        -- 获取当前线程对象,这个代码在哪个线程中,就得到哪个线程对象。
        main方法是由主线程执行的,理解成main方法就是一个主线程

4.4 创建线程方式一_继承方式

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

代码如下:

测试类:

public class Demo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 200; i++) {
			System.out.println("main线程!"+i);
		}
	}
}

自定义线程类:

public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
  	public MyThread() {
		//不指定线程的名字,线程有默认的名字Thread-0
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 200; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}
总结

类继承完Thread,然后直接输入run,系统就能自动重写run方法

在这里插入图片描述

run方法内部是线程的执行方法,在线程类的内部来进行创建

注意区分线程类和线程的区别:线程类可用于创建线程,继承Thread为线程类;而线程对象.start来执行线程

线程类

// 1.定义一个线程类继承Thread类。
class MyThread extends Thread{

线程对象

Thread t = new MyThread();//用多态的方法创建线程对象
// 4.调用线程对象的start()方法启动线程,最终还是执行run()方法!
t.start();

cpu抢到线程时,其他线程处于暂停状态,会保持当前的状态,直至其他线程再抢到cpu,然后继续线程的执行

类只能继承一个父类,单继承

目标:线程的创建方式一。

多线程是很有用的,我们在进程中创建线程的方式有三种:
    (1)直接定义一个类继承线程类Thread,重写run()方法,创建线程对象
        调用线程对象的start()方法启动线程。
    (2)定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把
        线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程。
    (3)实现Callable接口(拓展)。

a.继承Thread类的方式
    -- 1.定义一个线程类继承Thread类。
    -- 2.重写run()方法
    -- 3.创建一个新的线程对象。
    -- 4.调用线程对象的start()方法启动线程。

    继承Thread类的优缺点:
        优点:编码简单。
        缺点:线程类已经继承了Thread类无法继承其他类了,功能不能通过继承拓展(单继承的局限性)
小结:
    线程类是继承了Thread的类。
    启动线程必须调用start()方法。
    多线程是并发抢占CPU执行,所以在执行的过程中会出现并发随机性。
线程的使用注意事项
目标:线程的注意事项。

   1.线程的启动必须调用start()方法。否则当成普通类处理。
       -- 如果线程直接调用run()方法,相当于变成了普通类的执行,此时将只有主线程在执行他们!
       -- start()方法底层其实是给CPU注册当前线程,并且触发run()方法执行
   2.建议线程先创建子线程,主线程的任务放在之后。否则主线程永远是先执行完!
*/

如果线程对象.run()//系统会将run当作普通方法执行,而不是当作线程执行

建议线程先创建子线程,主线程的任务放在之后。否则主线程永远是先执行完!:代码是从main方法从上往下进行执行,没遇到线程,所以会优先执行main方法,与单线程一致;如果若是main方法执行的过程中遇到线程,主线程会和线程一起公平竞争cpu

4.5 创建线程的方式二_实现方式

采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

代码如下:

public class MyRunnable implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+" "+i);
		}
	}
}
public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "小强");//可以定义线程的名称和要包装的线程任务对象
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

Thread和Runnable的区别

总结:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池可以放入实现Runable或Callable类线程。
总结

用类来创建对象,是什么类就创建什么对象,是线程任务类就创建线程任务对象

同一个线程任务对象可以被包装成多个线程对象:MyRunnable是线程任务类

在这里插入图片描述

多个线程去共享同一资源:如上图,同一个线程任务对象,可以被多个线程对象所共享

线程任务代码与线程独立:如上图,创建线程任务对象为代码。而将线程任务对象包装成线程对象为线程,两者互相独立

Thread实现Runnable故可以将Runnable当作Thread来进行使用:

在这里插入图片描述

目标:线程的创建方式二。

多线程是很有用的,我们在进程中创建线程的方式有三种:
    (1)直接定义一个类继承线程类Thread,重写run()方法,创建线程对象
     调用线程对象的start()方法启动线程。
    (2)定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把
     线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程。
    (3)实现Callable接口(拓展)。

b.实现Runnable接口的方式。
     -- 1.创建一个线程任务类实现Runnable接口。
     -- 2.重写run()方法
     -- 3.创建一个线程任务对象。
     -- 4.把线程任务对象包装成线程对象
     -- 5.调用线程对象的start()方法启动线程。
Thread的构造器:
     -- public Thread(){}
     -- public Thread(String name){}
     -- public Thread(Runnable target){}
     -- public Thread(Runnable target,String name){}
实现Runnable接口创建线程的优缺点:
     缺点:代码复杂一点。
     优点:
       -- 线程任务类只是实现了Runnable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
       -- 同一个线程任务对象可以被包装成多个线程对象
       -- 适合多个多个线程去共享同一个资源(后面内容)
       -- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
       -- 线程池可以放入实现Runable或Callable线程任务对象。(后面了解)
          注意:其实Thread类本身也是实现了Runnable接口的。
       -- 不能直接得到线程执行的结果!

4.6 匿名内部类方式

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

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(){//重写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);
        }
   	}
}
总结

创建线程对象,参数为线程任务对象+重写的run方法

new的线程对象直接.start 来简化代码

在这里插入图片描述

线程创建的第三种方式
总结

父类不抛异常,子类继承父类时候,子类也不能抛异常:线程实现类实现Runnable,Runnable没抛异常,线程实现类重写的run方法也不能抛异常,而常见抛异常的地方是类继承异常类,此时,这个类才能抛异常

在这里插入图片描述

上图的线程重写的run方法由于是void,所以没有返回值

callable的泛型就是返回值类型

用多态创建Callable线程任务对象时,加泛型数据类型时(),数据类型一定要一致,不加泛型的数据类型也可以

在这里插入图片描述

未来任务对象最终继承于Runnable,故可以用Runnable来代替FutureTask

在这里插入图片描述
在这里插入图片描述

拓展:线程的创建方式三。(拓展)

多线程是很有用的,我们在进程中创建线程的方式有三种:
    (1)直接定义一个类继承线程类Thread,重写run()方法,创建线程对象
         调用线程对象的start()方法启动线程。
    (2)定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把
         线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程。
    (3)实现Callable接口(拓展)。

 c.线程的创建方式三: 实现Callable接口。
     -- 1,定义一个线程任务类实现Callable接口 , 申明线程执行的结果类型。
     -- 2,重写线程任务类的call方法,这个方法可以直接返回执行的结果。
     -- 3,创建一个Callable的线程任务对象。
     -- 4,把Callable的线程任务对象包装成一个未来任务对象。
     -- 5.把未来任务对象包装成线程对象。
     -- 6.调用线程的start()方法启动线程
 优缺点:
     优点:全是优点。
        -- 线程任务类只是实现了Callable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
        -- 同一个线程任务对象可以被包装成多个线程对象
        -- 适合多个多个线程去共享同一个资源(后面内容)
        -- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
        -- 线程池可以放入实现Runable或Callable线程任务对象。(后面了解)
        -- 能直接得到线程执行的结果!
     缺点:编码复杂。

代码

 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;
    }

第五章 线程安全

5.1 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)

需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。

模拟票:

public class Ticket implements Runnable {
    private int ticket = 100;
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while (true) {
            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--);
            }
        }
    }
}

测试类:

public class Demo {
	public static void main(String[] args) {
		//创建线程任务对象
		Ticket ticket = new Ticket();
		//创建三个窗口对象
		Thread t1 = new Thread(ticket, "窗口1");
		Thread t2 = new Thread(ticket, "窗口2");
		Thread t3 = new Thread(ticket, "窗口3");
		
		//同时卖票
		t1.start();
		t2.start();
		t3.start();
	}
}

结果中有一部分这样现象:

发现程序出现了两个问题:

  1. 相同的票数,比如5这张票被卖了两回。
  2. 不存在的票,比如0票与-1票,是不存在的。

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

总结

采用有参构造器来进行传值

private Account acc;//定义一个成员变量接收账户对象
public DrawThread(Account acc){//创建有参构造器,参数是传进来的值
this. acc=acc;//将外面传进来的账户传给调用者(线程)

然后创建对象,用线程用有参构造器的方式创建线程,参数为创建的对象

在这里插入图片描述

外面创建的账户最终会传到类所定义的对象,然后重合,主线程的acc–>线程类的acc


父类(Thread)有有参构造器,可以定义线程名称,故定义的名称来送往父类来应用父类的构造器

在这里插入图片描述


有参方法或构造器,参数就是方法或者构造器要加工的代码

public Account(String cardID, double moeny) {
    this.cardID = cardID;
    this.moeny = moeny;

因为调用drawMoney是线程的run方法中的acc,而传入acc的值就是存的钱(共享资源账户对象)

public void drawMoney(double moeny) {
    // 开始判断取钱逻辑
    // 1.先知道是谁来取钱
    String name = Thread.currentThread().getName();
    // 2.判断余额是否足够
    if(this.moeny >= moeny){
        System.out.println(name+"来取钱,余额足够,吐出"+moeny);
        // 3.更新余额
        this.moeny -= moeny;
        System.out.println(name+"来取钱后,余额剩余"+ this.moeny);
    }else{
        System.out.println(name+"来取钱,余额不足!");
    }
}
public class DrawThread extends Thread{
    // 定义一个成员变量接收账户对象
    private Account acc ;
    public DrawThread(Account acc,String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 小明 小红
        // 取钱100000
        acc.drawMoney(100000);//账户的取钱方法
    }
}
public class ThreadSafe {
    public static void main(String[] args) {
        // a.创建一个共享资源账户对象!
        Account acc = new Account("ICBC-110" , 100000);

5.2 线程同步

线程同步是为了解决线程安全问题。

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

5.3 同步代码块

  • 同步代码块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--);
				}
			}
		}
	}
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

总结
目标:线程同步_同步代码块

线程同步的作用:就是为了解决线程安全问题的方案。

线程同步解决线程安全问题的核心思想:
        让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

线程同步的做法:加锁
        是把共享资源进行上锁,每次只能一个线程进入访问完毕以后,其他线程才能进来。

线程同步的方式有三种:
    (1)同步代码块。
    (2)同步方法。
    (3)lock显示锁。

a.同步代码块。
     作用:把出现线程安全问题的核心代码给上锁,每次只能一个线程进入
          执行完毕以后自动解锁,其他线程才可以进来执行。

     格式:
          synchronized(锁对象){
                // 访问共享资源的核心代码
          }
          锁对象:理论上可以是任意的“唯一”对象即可。
          原则上:锁对象建议使用共享资源。
                -- 在实例方法中建议用this作为锁对象。此时this正好是共享资源!必须代码高度面向对象
                -- 在静态方法中建议用类名.class字节码作为锁对象。

可以将this当作锁。this代表synchronized(){

code

},code执行的主体(共享资源)

在这里插入图片描述

在静态方法中建议用类名.class字节码作为锁对象。

在这里插入图片描述

同步锁范围锁的越精细,效果越好,能够提高代码的运行效率

5.4 同步方法

  • 同步方法:使用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--);
        }
	}
}
总结
同步方法
    作用:把出现线程安全问题的核心方法给锁起来,
         每次只能一个线程进入访问,其他线程必须在方法外面等待。
    用法:直接给方法加上一个修饰符 synchronized.
    原理:  同步方法的原理和同步代码块的底层原理其实是完全一样的,只是
          同步方法是把整个方法的代码都锁起来的。
          同步方法其实底层也是有锁对象的:
              如果方法是实例方法:同步方法默认用this作为的锁对象。
              如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

5.5 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();//解锁
		}
	}
}
总结
lock显示锁。
     java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
     同步代码块/同步方法具有的功能Lock都有,除此之外更强大

     Lock锁也称同步锁,加锁与释放锁方法化了,如下:
          - `public void lock() `:加同步锁。
          - `public void unlock()`:释放同步锁。

总结:
     线程安全,性能差。
     线程不安全性能好。假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类。

运行账户类的时候同时会创建lock对象,故lock对象是唯一的;lock是接口,new的是实现类

在这里插入图片描述

上锁

代码

解锁

猜你喜欢

转载自blog.csdn.net/u013074761/article/details/106058912