JavaSE个人复习式整理知识点之异常、线程

1 异常

1.1 异常概念

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

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

在Java等⾯向对象的编程语⾔中,异常本身是⼀个类产⽣异常就是创建异常对象并抛出了⼀个异常对象。Java处理异常的⽅式是中断处理

异常指的并不是语法错误,语法错了,编译不通过,不会产⽣字节码⽂件,根本不能运⾏。

1.2 异常体系

异常机制其实是帮助我们找到程序中的问题,异常的根类是 java.lang.Throwable ,其下有两个⼦类: java.lang.Error 与 java.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异常。在运⾏时期,检查异常。在编译时期,运⾏异常不会编译器检测(不报错)。(如数学异常)在这里插入图片描述

1.4 异常的产⽣过程解析

先运⾏下⾯的程序,程序会产⽣⼀个数组索引越界异常ArrayIndexOfBoundsException 。我们通过图解来解析下异常产⽣的过程。\

⼯具类:

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

测试类:

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

上述程序执⾏过程图解:在这里插入图片描述

2 异常的处理

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运⾏效果分析:
在这里插入图片描述

注意:如果产⽣了问题,我们就会throw将问题描述类即异常进⾏抛出,也就是将问题返回给该⽅法的调⽤者。
那么对于调⽤者来说,该怎么处理呢?⼀种是进⾏捕获处理,另⼀种就是继续将问题声明出去,使⽤throws声明处理。

2.2 Objects⾮空判断

还记得我们学习过⼀个类Objects吗,曾经提到过它由⼀些静态的实⽤⽅法组成,这些⽅法是null-safe(空指针安全的)或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();
        }
    }
}

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。

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永远会执⾏。

2.6 异常注意事项

  • 多个异常使⽤捕获⼜该如何处理呢?
  1. 多个异常分别处理。
  2. 多个异常⼀次捕获,多次处理。
  3. 多个异常⼀次捕获⼀次处理。
    ⼀般我们是使⽤⼀次捕获多次处理⽅式,格式如下:
try {
	编写可能会出现异常的代码
} catch(异常类型A e) { 当try中出现A类型异常,就⽤该catch来捕获.
	处理异常的代码
// 记录⽇志/打印异常信息/继续抛出异常
} catch(异常类型B e) { 当try中出现B类型异常,就⽤该catch来捕获.
	处理异常的代码
// 记录⽇志/打印异常信息/继续抛出异常
}

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

  • 运⾏时异常被抛出可以不处理。即不捕获也不声明抛出。
  • 如果finally有return语句,永远返回finally中的结果,避免该情况.。
  • 如果⽗类抛出了多个异常,⼦类重写⽗类⽅法时,抛出和⽗类相同的异常或者是⽗类异常的⼦类或者不抛出异常。
  • ⽗类⽅法没有抛出异常,⼦类重写⽗类该⽅法时也不可抛出异常。此时⼦类产⽣该异常,只能捕获处理,不能声明抛出。

3 ⾃定义异常

3.1 概述

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

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

什么是⾃定义异常类:

在开发中根据⾃⼰业务的异常情况来定义异常类。

⾃定义⼀个业务逻辑异常:RegisterException。⼀个注册异常类。

异常类如何定义:
  1. ⾃定义⼀个编译期异常:⾃定义类,并继承于 java.lang.Exception
  2. ⾃定义⼀个运⾏时期的异常类:⾃定义类,并继承于java.lang.RuntimeException

3.2 ⾃定义异常的练习

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

⾸先定义⼀个登陆异常类RegisterException:

// 业务逻辑异常
public class RegisterException extends Exception {
    /**
     * 空参构造
     */
    public RegisterException() {
    }
    /**
     * @param message 表示异常提示
     */
    public RegisterException(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(RegisterException e) {
            // 处理异常
            e.printStackTrace();
        }
    }
    // 判断当前注册账号是否存在
    // 因为是编译期异常,⼜想调⽤者去处理 所以声明该异常
    public static boolean checkUsername(String uname) throws LoginException
    {
        for (String name : names) {
            if (name.equals(uname)) {//如果名字在这⾥⾯ 就抛出登陆异常
                throw new RegisterException("亲" + name + "已经被注册了!");
            }
        }
        return true;
    }
}

4 多线程

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

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

4.1 并发与并⾏

  • 并发:指两个或多个事件在同⼀个时间段内发⽣。
  • 并⾏:指两个或多个事件在同⼀时刻发⽣(同时发⽣)。

在这里插入图片描述

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

⽽在多个 CPU 系统中,则这些可以并发执⾏的程序便可以分配到多个处理器上(CPU),实现多任务并⾏执⾏,即利⽤每个处理器来处理⼀个可以并发执⾏的程序,这样多个程序便可以同时执⾏。⽬前电脑市场上说的多核 CPU,便是多核处理器,核越多,并⾏处理的程序越多,能⼤⼤的提⾼电脑运⾏的效率。

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

4.2 线程与进程

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

简⽽⾔之:⼀个程序运⾏后⾄少有⼀个进程,⼀个进程中可以包含多个线程

我们可以再电脑底部任务栏,右键 --> 打开任务管理器,可以查看当前任务的进程:

进程在这里插入图片描述

在这里插入图片描述

线程调度:

  • 分时调度
    所有线程轮流使⽤ CPU 的使⽤权,平均分配每个线程占⽤ CPU 的时间。
  • 抢占式调度
    优先让优先级⾼的线程使⽤ CPU,如果线程的优先级相同,那么会随机选择⼀个(线程随机性),Java使⽤的为抢占式调度。
    • 设置线程的优先级在这里插入图片描述
    • 抢占式调度详解
      ⼤部分操作系统都⽀持多进程并发运⾏,现在的操作系统⼏乎都⽀持同时运⾏多个程序。⽐如:现在我们上课⼀边使⽤编辑器,⼀边使⽤录屏软件,同时还开着画图板,dos窗⼝等软件。此时,这些程序是在同时运⾏,“感觉这些软件好像在同⼀时刻运⾏着”。
      实际上,CPU(中央处理器)使⽤抢占式调度模式在多个线程间进⾏着⾼速的切换。对于 CPU 的⼀个核⽽⾔,某个时刻,只能执⾏⼀个线程,⽽ CPU 的在多个线程间切换速度相对我们的感觉要快,看上去就是在同⼀时刻运⾏。
      其实,多线程程序并不能提⾼程序的运⾏速度,但能够提⾼程序运⾏效率,让 CPU 的使⽤率更⾼。在这里插入图片描述

4.3 创建线程类

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 < 10; i++) {
            System.out.println("main线程!" + i);
        }
    }
}

⾃定义线程类:

public class MyThread extends Thread {
    // 定义指定线程名称的构造⽅法
    public MyThread(String name) {
        // 调⽤⽗类的String参数的构造⽅法,指定线程的名称
        super(name);
    }
    /**
     * 重写run⽅法,完成该线程执⾏的逻辑
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + ":正在执⾏!" + i);
        }
    }
}

5 多线程详解

5.1 多线程原理

刚刚我们已经写过⼀版多线程的代码,很多同学对原理不是很清楚,那么现在我们先画个多线程执⾏时序图来体现⼀下多线程程序的执⾏流程。 代码如下: ⾃定义线程类:

public class MyThread extends Thread {
    /*
     * 利⽤继承中的特点
     * 将线程名称传递 进⾏设置
     */
    public MyThread(String name) {
        super(name);
    }
    /*
     * 重写run⽅法
     * 定义线程要执⾏的代码
     */
    public void run() {
        for (int i = 0; i < 20; i++) {
            //getName()⽅法 来⾃⽗亲
            System.out.println(getName() + i);
        }
    } 
}

测试类:

public class Demo {
    public static void main(String[] args) {
        System.out.println("这⾥是main线程");
        MyThread mt = new MyThread("⼩强");
        mt.start(); // 开启了⼀个新的线程
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财:" + i);
        }
    } 
}

在这里插入图片描述

程序启动运⾏ main 时候,java 虚拟机启动⼀个进程,主线程 main 在 main() 调⽤时候被创建。随着调⽤ mt 的对象的 start ⽅法,另外⼀个新的线程也启动了,这样,整个应⽤就在多线程下运⾏。

通过这张图我们可以很清晰的看到多线程的执⾏流程,那么为什么可以完成并发执⾏呢?我们再来讲⼀讲原理。

多线程执⾏时,到底在内存中是如何运⾏的呢?以上个程序为例,进⾏图解说明:

多线程执⾏时,在栈内存中,其实每⼀个执⾏线程都有⼀⽚⾃⼰所属的栈内存空间。进⾏⽅法的压栈和弹栈。在这里插入图片描述

当执⾏线程的任务结束了,线程⾃动在栈内存中释放了。但是当所有的执⾏线程都结束了,那么进程就结束了。

5.2 Thread类

在上⼀章内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中⽤到了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 接⼝⽅式,⽅式⼀我们上⼀章已经完成,接下来讲解⽅式⼆实现的⽅式。

5.3 创建线程⽅式⼆

采⽤ 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() ⽅法。

5.4 Thread 和 Runnable 的区别

如果⼀个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接⼝的话,则很容易的实现资源共享。

总结:

实现 Runnable 接⼝⽐继承 Thread 类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同⼀个资源。
  2. 可以避免 java 中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独⽴。
  4. 线程池只能放⼊实现 Runable 或 Callable 类线程,不能直接放⼊继承 Thread 的类。

扩充:在 java 中,每次程序运⾏⾄少启动2个线程。⼀个是 main 线程,⼀个是垃圾收集线程。因为每当使⽤ java 命令执⾏⼀个类的时候,实际上都会启动⼀个 JVM,每⼀个 JVM 其实就是在操作系统中启动了⼀个进程。

5.5 匿名内部类⽅式实现线程的创建

使⽤线程的内匿名内部类⽅式,可以⽅便的实现每个线程执⾏不同的线程任务操作。
使⽤匿名内部类的⽅式实现 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() {
                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);
        }
    }
}

6 线程安全

6.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) {
                    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. 相同的票数,⽐如2这张票被卖了两回。
  2. 不存在的票,⽐如0票与-1票,是不存在的。

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

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

6.2 线程同步

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

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

根据案例简述:

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

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

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

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

6.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) {
                        e.printStackTrace();
                    }
                    // 获取当前线程对象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在卖: " + ticket--);
                }
            }
        }
    }
}

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

6.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) {
                e.printStackTrace();
            }
            // 获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在卖:" + ticket--);
        }
    }
}

6.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) {
                    e.printStackTrace();
                }
                // 获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticket--);
            }
            lock.unlock();
        }
    }
}

7 线程状态

7.1 线程状态概述

当线程被创建并启动以后,它既不是⼀启动就进⼊了执⾏状态,也不是⼀直处于执⾏状态。在线程的⽣命周期中,有⼏种状态呢?在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

这⾥先列出各个线程状态发⽣的条件,下⾯将会对每种状态进⾏详细解析

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调⽤start⽅法。
Runnable(可运⾏)线程可以在java虚拟机中运⾏的状态,可能正在运⾏⾃⼰代码,也可能没有,这取决于操作系统处理器
Blocked(锁阻塞)当⼀个线程试图获取⼀个对象锁,⽽该对象锁被其他的线程持有,则该线程进⼊Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(⽆限等待)⼀个线程在等待另⼀个线程执⾏⼀个(唤醒)动作时,该线程进⼊Waiting状态。进⼊这个状态后是不能⾃动唤醒的,必须等待另⼀个线程调⽤notify或者notifyAll⽅法才能够唤醒。
TimedWaiting(计时等待)同waiting状态,有⼏个⽅法有超时参数,调⽤他们将进⼊TimedWaiting状态。这⼀状态将⼀直保持到超时期满或者接收到唤醒通知。带有超时参数的常⽤⽅法有Thread.sleep、Object.wait。
Teminated(被终⽌)因为run⽅法正常退出⽽死亡,或者因为没有捕获的异常终⽌了run⽅法⽽死亡。

我们不需要去研究这⼏种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这⼏个状态呢,新建与被终⽌还是很容易理解的,我们就研究⼀下线程从Runnable(可运⾏)状态与⾮运⾏状态之间的转换问题。

7.2 Timed Waiting(计时等待)

Timed Waiting 在 API 中的描述为:⼀个正在限时等待另⼀个线程执⾏⼀个(唤醒)动作的线程处于这⼀状态。单独的去理解这句话,真是⽞之⼜⽞,其实我们在之前的操作中已经接触过这个状态了,在哪⾥呢?

在我们写卖票的案例中,为了减少线程执⾏太快,现象不明显等问题,我们在 run ⽅法中添加了sleep 语句,这样就强制当前正在执⾏的线程休眠(暂停执⾏),以“减慢线程”。

其实当我们调⽤了 sleep ⽅法之后,当前执⾏的线程就进⼊到“休眠状态”,其实就是所谓的Timed Waiting(计时等待),那么我们通过⼀个案例加深对该状态的⼀个理解。

实现⼀个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出⼀个字符串

代码:

public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("-------" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print(" 线程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new MyThread().start();
    }
}

通过案例可以发现,sleep⽅法的使⽤还是很简单的。我们需要记住下⾯⼏点:

  1. 进⼊ TIMED_WAITING 状态的⼀种常⻅情形是调⽤的 sleep ⽅法,单独的线程也可以调⽤,不⼀定⾮要有协作关系。
  2. 为了让其他线程有机会执⾏,可以将Thread.sleep()的调⽤放线程run()之内,这样才能保证该线程执⾏过程中会睡眠。
  3. sleep与锁⽆关,线程睡眠到期⾃动苏醒,并返回到Runnable(可运⾏)状态。

⼩提示:sleep()中指定的时间是线程不会运⾏的最短时间。因此,sleep()⽅法不能保证该线程睡眠到期后就开始⽴刻执⾏。

Timed Waiting 线程状态图:在这里插入图片描述

7.3 BLOCKED(锁阻塞)

Blocked 状态在 API 中的介绍为:⼀个正在阻塞等待⼀个监视器锁(锁对象)的线程处于这⼀状态。

我们已经学完同步机制,那么这个状态是⾮常好理解的了。⽐如,线程A与线程B代码中使⽤同⼀锁,如果线程A获取到锁,线程A进⼊到 Runnable 状态,那么线程B就进⼊到 Blocked 锁阻塞状态。

这是由 Runnable 状态进⼊ Blocked 状态。除此 Waiting 以及 Time Waiting 状态也会在某种情况下进⼊阻塞状态,⽽这部分内容作为扩充知识点带领⼤家了解⼀下。

Blocked 线程状态图:在这里插入图片描述

7.4 Waiting(⽆限等待)

Wating 状态在 API 中介绍为:⼀个正在⽆限期等待另⼀个线程执⾏⼀个特别的(唤醒)动作的线程处于这⼀状态。

那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进⾏⼀个简单深⼊的了解。我们通过⼀段代码来学习⼀下:

public class WaitingTest {
    public static Object obj = new Object();

    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (obj) {
                        try {
                            System.out.println(Thread.currentThread().getName() + "=== 获取到锁对象,调⽤wait⽅法,进⼊waiting状态,释放锁对象");
                            obj.wait(); // ⽆限等待
                            // obj.wait(5000); // 计时等待, 5秒 时间到,⾃动醒来
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执⾏了");
                    }
                }
            }
        }, "等待线程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //while (true) { // 每隔3秒 唤醒⼀次
                    try {
                        System.out.println(Thread.currentThread().getName() + "----- 等待3秒钟");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj) {
                        System.out.println(Thread.currentThread().getName() + "----- 获取到锁对象,调⽤notify⽅法,释放锁对象");
                        obj.notify();
                    }
                //}
            }
        }, "唤醒线程").start();
    }
}

通过上述案例我们会发现,⼀个调⽤了某个对象的 Object.wait ⽅法的线程会等待另⼀个线程调⽤此对象的 Object.notify() ⽅法 或 Object.notifyAll() ⽅法。

其实 waiting 状态并不是⼀个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间⼜存在协作关系。就好⽐在公司⾥你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是⼀起合作以完成某些任务。

当多个线程协作时,⽐如A,B线程,如果A线程在 Runnable(可运⾏)状态中调⽤了 wait() ⽅法那么A线程就进⼊了Waiting(⽆限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运⾏状态中调⽤了 notify() ⽅法,那么就会将⽆限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进⼊ Runnable(可运⾏)状态;如果没有获取锁对象,那么就进⼊到 Blocked(锁阻塞状态)。

Waiting 线程状态图:在这里插入图片描述

7.5 补充知识点

到此为⽌我们已经对线程状态有了基本的认识,想要有更多的了解,详情可以⻅下图:在这里插入图片描述

⼀条有意思的Tips:
我们在翻阅 API 的时候会发现 Timed Waiting(计时等待)与 Waiting(⽆限等待)状态联系还是很紧密的,⽐如Waiting(⽆限等待)状态中 wait ⽅法是空参的,⽽ TimedWaiting(计时等待)中 wait ⽅法是带参的。这种带参的⽅法,其实是⼀种倒计时操作,相当于我们⽣活中的⼩闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间再通知也就显得多此⼀举了,那么这种设计⽅案其实是⼀举两得。如果没有得到(唤醒)通知,那么线程就处于 Timed Waiting 状态,直到倒计时完毕⾃动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从 Timed Waiting状态⽴刻唤醒。

  • 4
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值