Skip to content

特殊文件、日志技术、多线程

  • 特殊文件
    • Properties文件 注意格式
    • XML文件
      • XML作用 🍐
      • XML解析 🚀 了解即可
  • 日志文件
    • 日志作用 🍐
    • Logback框架 🚀
  • 多线程
    • 多线程创建方式 🍐 ✏️ ❤️

前置知识

  1. 能使用字节流或者工具类读取文件
  2. 能使用HashMap存储双列数据
  3. 使用过sout快捷键
  4. 听说过三个臭皮匠顶个诸葛亮的典故

一、属性文件 🚩 🍐

1️⃣ 1.1 特殊文件概述

普通的文本文件,没有任何规律可言,不方便程序对文件中的数据信息处理 不方便解析。

特殊的文本文件 格式固定,方便解析:

  • 后缀为.properties的文件,称之为属性文件,它可以很方便的存储一些类似于键值对的数据。经常当做软件的配置文件使用。
  • xml文件能够表示更加复杂的数据关系,比如要表示多个用户的用户名、密码、家乡、性别等。在后面,也经常当做软件的配置文件使用。

特殊的文件,主要学习以下的三点

2️⃣ 1.2 Properties属性文件

Properties是Map接口下面的一个实现类,所以Properties也是一种双列集合,用来存储键值对 。

Properties格式:

  1. 属性文件后缀以.properties结尾
  2. 属性文件里面的每一行都是一个键值对,键和值中间用=隔开。比如: admin=123456
  3. #表示这样是注释信息,是用来解释这一行配置是什么意思。
  4. 每一行末尾不要习惯性加分号,以及空格等字符;不然会把分号,空格会当做值的一部分。
  5. 键不能重复,值可以重复

点击查看图示

Properties核心作用:

Properties类的对象,用来表示属性文件,可以用来读取属性文件中的键值对。


使用Properties读取属性文件中的键值对,需要用到的方法如下。

Properties读取属性文件的步骤如下

  1. 创建一个Properties的对象出来(键值对集合,空容器)
  2. 调用load(字符输入流/字节输入流)方法,开始加载属性文件中的键值对数据到properties对象中去
  3. 调用getProperty(键)方法,根据键取值

点击查看代码

代码如下:

java
/**
 * 目标:掌握使用Properties类读取属性文件中的键值对信息。
 */
public class PropertiesTest1 {
    public static void main(String[] args) throws Exception {
        // 1、创建一个Properties的对象出来(键值对集合,空容器)
        Properties properties = new Properties();
        System.out.println(properties);

        // 2、开始加载属性文件中的键值对数据到properties对象中去
        properties.load(new FileReader("properties-xml-log-app\\src\\users.properties"));
        System.out.println(properties);

        // 3、根据键取值
        System.out.println(properties.getProperty("赵敏"));
        System.out.println(properties.getProperty("张无忌"));

        // 4、遍历全部的键和值。
        //获取键的集合
        Set<String> keys = properties.stringPropertyNames();
        for (String key : keys) {
            //再根据键获取值
            String value = properties.getProperty(key);
            System.out.println(key + "---->" + value);
        }

        properties.forEach((k, v) -> {
            System.out.println(k + "---->" + v);
        });
    }
}

使用Properties往属性文件中写键值对,需要用到的方法如下

往Properties属性文件中写键值对的步骤如下

  1. 先准备一个.properties属性文件,按照格式写几个键值对
  2. 创建Properties对象出来,
  3. 调用setProperty存储一些键值对数据
  4. 调用store(字符输出流/字节输出流, 注释),将Properties集合中的键和值写到文件中
    • 注意:第二个参数是注释,必须得加;

先准备一个users.properties属性文件,如下图所示

接下来,编写代码读取上面的属性文件。代码如下:

点击查看代码

java
public class PropertiesTest2 {
    public static void main(String[] args) throws Exception {
        // 1、创建Properties对象出来,先用它存储一些键值对数据
        Properties properties = new Properties();
        properties.setProperty("张无忌", "minmin");
        properties.setProperty("殷素素", "cuishan");
        properties.setProperty("张翠山", "susu");

        // 2、把properties对象中的键值对数据存入到属性文件中去
        properties.store(new FileWriter("properties-xml-log-app/src/users2.properties")
                         , "i saved many users!");

    }
}

运行上面的代码,user2.properties 配置文件打开效果如下图所示。

二、XML文件 🚩 🍐 🚀

1️⃣ 2.1 XML文件概述 🍐

相关信息

首先,我们来认识一下,什么是XML?

XML是可扩展的标记语言,意思是它是由一些标签组成 的,而这些标签是自己定义的。本质上一种数据格式,可以用来表示复杂的数据关系。

XML文件有如下的特点:

  • XML中的<标签名> 称为一个标签或者一个元素,一般是成对出现的。
  • XML中的标签名可以自己定义(可扩展) ,但是必须要正确的嵌套
  • XML中只能有一个根标签 。
  • XML标准中可以有属性
  • XML必须第一行有一个文档声明 ,格式是固定的<?xml version="1.0" encoding="UTF-8"?>
  • XML文件必须是以.xml为后缀结尾

如下图所示

xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 注释:以上抬头声明必须放在第一行,必须有 -->
<!--  根标签只能有一个 -->
<users>
    <user id="1" desc="第一个用户">
        <name>张无忌</name>
        <sex>男</sex>
        <地址>光明顶</地址>
        <password>minmin</password>
    </user>
    <people>很多人</people>
    <user id="2">
        <name>敏敏</name>
        <sex>女</sex>
        <地址>光明顶</地址>
        <password>wuji</password>
    </user>
</users>

上面XML文件中的数据格式是最为常见的,标签有属性、文本、还有合理的嵌套。XML文件中除了写以上的数据格式之外,还有一些特殊的字符不能直接写。

  • <,>,& 等这些符号不能出现在标签的文本中,因为标签格式本身就有<>,会和标签格式冲突。

    如果标签文本中有这些特殊字符,需要用一些占位符代替。

    txt
    &lt;  表示 <
    &gt;  表示 >
    &amp; 表示 &
    &apos; 表示 '
    &quot; 表示 "
    xml
    <data> 3 &lt; 2 &amp;&amp; 5 &gt; 4 </data>
  • 如果在标签文本中,出现大量的特殊字符,不想使用特殊字符,此时可以用CDATA区,格式如下

    xml
    <data1>
        <<img src="./part11/CDATA[
       		3 < 2 && 5 > 4
        ]]>
    </data1>

2️⃣ 2.2 XML解析 🍐 🚀

使用程序读取XML文件中的数据,称之为XML解析

不需要我们自己写IO流代码去读取xml文件中的数据,直接使用现成的XML解析框架(如:DOM4J)

由于DOM4J是第三方提供的,所以需要把第三方提供的Jar包导入到自己的项目中来,才可以使用。具体步骤如下:

DOM4J解析XML文件的思想是: 文档对象模型(意思是把整个XML文档、每一个标签、每一个属性都等都当做对象来看待)。

  • Dowument对象表示整个XML文档
  • Element对象表示标签(元素)
  • Attribute对象表示属性
  • 标签中的内容就是文本

DOM4J解析XML需要用到的方法如下图所示

XML解析的过程,是从根元素开始,从外层往里层解析。 我们先把Document对象,和根元素获取出来

java
public class Dom4JTest1 {
    public static void main(String[] args) throws Exception {
        // 1、创建一个Dom4J框架提供的解析器对象
        SAXReader saxReader = new SAXReader();

        // 2、使用saxReader对象把需要解析的XML文件读成一个Document对象。
        Document document =
                saxReader.read("properties-xml-log-app\\src\\books.xml");

        // 3、从文档对象中解析XML文件的全部数据了
        Element root = document.getRootElement();
        System.out.println(root.getName());
    }
}

准备好的books.xml文件

xml
<?xml version="1.0" encoding="UTF-8" ?>
<books>

	<book>
		<name>凡人修仙传</name>
		<author>亡语</author>
		<desc>讲述韩立修仙的故事</desc>
	</book>
	<book>
		<name>重生在黑马当班长</name>
		<author>嘿嘿</author>
		<desc>讲述在黑马学java的日子</desc>
	</book>

</books>

获取到XML文件的根元素之后,接下来,就可以用根元素在获取到它里面的子元素(包括子标签、表属性等)。需要用到的方法如下图所示

接下来,把上面的方法先一个一个的演示一下。

java
public class Dom4JTest1 {
    public static void main(String[] args) throws Exception {
        // 1、创建一个Dom4J框架提供的解析器对象
        SAXReader saxReader = new SAXReader();

        // 2、使用saxReader对象把需要解析的XML文件读成一个Document对象。
        Document document =
                saxReader.read("properties-xml-log-app\\src\\book.xml");

        // 3、从文档对象中解析XML文件的全部数据了
        Element root = document.getRootElement();

//        4.获得book
         List<Element> book = root.elements("book");

        for (Element element : book) {

            System.out.println("书名:"+element.element("name").getStringValue());
            System.out.println("作者:"+element.element("author").getStringValue());
            System.out.println("描述:"+element.element("desc").getStringValue());
            System.out.println();
        }



    }
}

4️⃣ 2.4 XML文件写入 🚀 了解

能不能往XML文件中写入数据呢? 答案是可以的。

DOM4J也提供了往XML文件中写标签的方法,但是用起来比较麻烦不建议使用。

我们自己使用StringBuilder按照标签的格式拼接,然后再使用BufferedWriter写到XML文件中去就可以了。

java
public class Dom4JTest2 {
    public static void main(String[] args) {
        // 1、使用一个StringBuilder对象来拼接XML格式的数据。
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n");
        sb.append("<book>\r\n");
        sb.append("\t<name>").append("从入门到跑路").append("</name>\r\n");
        sb.append("\t<author>").append("dlei").append("</author>\r\n");
        sb.append("\t<price>").append(999.99).append("</price>\r\n");
        sb.append("</book>");

        try (
                BufferedWriter bw = new BufferedWriter(new FileWriter("properties-xml-log-app/src/book.xml"));
                ){
            bw.write(sb.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

5️⃣ 2.5 XML约束 🚀了解

XML约束

XML约束指的是限制XML文件中的标签或者属性,只能按照规定的格式。提高规范性

比如我在项目中,想约束一个XML文件中的标签只能写<书>、<书名>、<作者>、<售价>这几个标签,如果写其他标签就报错。

约束技术:

一种是DTD约束已淘汰、一种是Schame约束下面作业可以体验一下效果。

作业练习

🚩 1. 按照下面的提示操作一下,体会一下约束的作用。

  • 第一步:在idea的src下创建一个pom.xml文件
  • 第二步:在pom.xml文件中,粘贴下列代码
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>javaproject</artifactId>
<groupId>com.huawei</groupId>
<version>1.0</version>
<modelVersion>4.0.0</modelVersion>

<!--   试着在这里写你自定一点标签 如<book>-->

</project>
  • 第三步:在上述代码深色地方,写一些自定义的标签,如:<book>,或者输入 <看是否有提示

三、日志技术 🚩 ✏️

1️⃣ 3.1 日志概述 🍐

工作场景

  • 系统系统能记住某些数据被谁操作,比如被谁删除了?
  • 想分析用户浏览系统的具体情况,比如挖掘用户的具体喜好
  • 当系统在开发中或者上线后出现了Bug,崩溃了,该通过什么去分析此时可以debug吗?,定位Bug?

点击查看解决方案

日志就可以帮我们解决以上的问题。

  • 日志就好比生活中的日记,日记可以记录生活中的点点滴滴;
  • 而程序中的日志,通常就是一个文件,里面记录了程序运行过程中产生的各种数据

日志技术有如下好处:

  1. 日志可以将系统执行的信息,方便的记录到指定位置,可以是控制台、可以是文件、可以是数据库中电脑关机不会消失。

  2. 日志可以随时以开关的形式控制启停非常灵活,无需侵入到源代码中去修改。

2️⃣ 3.2 日志的体系 🚀

日志框架和日志的体系

  1. 所谓日志框架就是由一些牛人或者第三方公司已经做好的实现代码,后来者就可以直接拿过去使用开发实用。🍐
  2. 日志框架有很多种,比如有JUL(java.util.logging)、Log4j、logback等。但是实现了同一套接口 一通百通 🍐

比较常用的日志框架,和日志接口的关系如下图所示

  1. Logback日志框架,在行业中最为广泛使用

Logback日志分为哪几个模块 🚀

3️⃣ 3.3 Logback快速入门

接下来,就带领小伙伴们快速使用一下Logback日志框架,

使用Logback记录几条日志信息 到文件中去 和打印在控制台 上。

Logback入门案例

由于Logback是第三方提供的技术,所以首先需要啊将Jar包引入到项目中,具体步骤如下

  1. 在资料中找到slftj-api.jar、logback-core.jar、logback-classes.jar 这三个jar包,复制一下
  2. 在当前模块下面新建一个lib文件夹,把刚刚复制的三个jar包都粘贴到此处
  3. 从资料中找到logback.xml配置文件(也可以复制创建文件),将此文件复制粘贴到src目录下(必须是src目录)

点击查看logback.xml代码

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        CONSOLE :表示当前的日志信息是可以输出到控制台的。
    -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.out</target>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
                %msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level]  %c [%thread] : %msg%n</pattern>
        </encoder>
    </appender>

    <!-- File是输出的方向通向文件的 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!--日志输出路径-->
        <file>D:/log/itheima-data.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>D:/log/itheima-data-%i-%d{yyyy-MM-dd}-.log.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
    </appender>

    <!--
        1、控制日志的输出情况:如,开启日志,取消日志
    -->
    <root level="debug">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE" />
    </root>
</configuration>
  1. 然后就可以开始写代码了,在代码中创建一个日志记录日对象

    java
    public static final Logger LOGGER = LoggerFactory.getLogger("当前类名");
  2. 开始记录日志,代码如下

    java
    public class LogBackTest {
        // 创建一个Logger日志对象
        public static final Logger LOGGER = LoggerFactory.getLogger("LogBackTest");
    
        public static void main(String[] args) {
            //while (true) {
                try {
                    LOGGER.info("chu法方法开始执行~~~");
                    chu(10, 0);
                    LOGGER.info("chu法方法执行成功~~~");
                } catch (Exception e) {
                    LOGGER.error("chu法方法执行失败了,出现了bug~~~");
                }
            //}
        }
    
        public static void chu(int a, int b){
            LOGGER.debug("参数a:" + a);
            LOGGER.debug("参数b:" + b);
            int c = a / b;
            LOGGER.info("结果是:" + c);
        }
    }

当我们运行程序时,就可以看到控制台记录的日志

同时在文件中,也有一份这样的日志信息。文件在哪里内,从配置文件中去找

打开D:/log/itheima-data.log看一下文件中是否有记录日志吧!!

关于Logback的快速入门我们就做完了。至于日志的配置文件中,其他的配置是什么含义我们下一节再继续学习。

课堂作业

🚩 按照上述步骤,完成课堂案例吧!

  1. 导入jar包(3个),导入xml配置文件,写一个类,书写一个main方法 快快开始把!!

运行main方法后,观察d盘的log目录下 是否存在日志文件

4️⃣ 3.4 日志配置文件 🚀

Logback提供了一个核心配置文件logback.xml,日志框架在记录日志时会读取配置文件中的配置信息,从而记录日志的形式。具体可以做哪些配置呢?

配置

  1. 可以配置日志输出的位置是文件、还是控制台
  2. 可以配置日志输出的格式
  3. 还可以配置日志关闭和开启、以及哪些日志输出哪些日志不输出。 实操一下
  • 控制日志往文件中输出,还是往控制台输出

  • 控制打开和关闭日志 👈

  • 控制日志的输出的格式

    日志格式是由一些特殊的符号组成,可以根据需要删减不想看到的部分。比如不想看到线程名那就不要[%thread]。但是不建议小伙伴们更改这些格式,因为这些都是日志很基本的信息。

5️⃣ 3.5 配置日志级别

  • 在哪里配置日志级别呢?如下图所示

  • Logback只输出大于或者等于核心配置文件配置的日志级别信息。小于配置级别的日志信息,不被记录。

相关信息

  • 配置的是trace,则trace、debug、info、warn、error级别的日志都被输出
  • 配置的是debug, 则debug、info、warn、error级别的日志被输出
  • 配置的是info,则info、warn、error级别的日志被输出

课堂作业

🚩 1. 思考一下,是否是级别越低越好,因为这样信息越多越全?

四、单元测试 🚩 ✏️

4.1 单元测试快速入门

所谓单元测试,就是针对最小的功能单元,编写测试代码对其进行正确性测试。

在main方法中写测试代码有如下的几个问题,如下图所示:

为了测试更加方便,有一些第三方的公司或者组织提供了很好用的测试框架,给开发者使用。这里给小伙伴们介绍一种Junit测试框架。

Junit是第三方公司开源出来的,用于对代码进行单元测试的工具(IDEA已经集成了junit框架)。相比于在main方法中测试有如下几个优点。👍


我们知道单元测试是什么之后,接下来带领小伙伴们使用一下。由于Junit是第三方提供的,所以我们需要把jar包导入到我们的项目中,才能使用,具体步骤如下图所示:

接下来,我们就按照上面的步骤,来使用一下.

点击查看验证代码

先准备一个类,假设写了一个StringUtil工具类,代码如下

java
public class StringUtil{
    public static void printNumber(String name){
        System.out.println("名字长度:"+name.length());
    }
}

接下来,写一个测试类,测试StringUtil工具类中的方法能否正常使用。

java
public class StringUtilTest{
    @Test
    public void testPrintNumber(){
        StringUtil.printNumber("admin");
        StringUtil.printNumber(null);
    }
}

写完代码之后,我们会发现测试方法左边,会有一个绿色的三角形按钮。点击这个按钮,就可以运行测试方法。

4.2 单元测试断言 🍐

接下来,我们学习一个单元测试的断言机制

所谓断言:意思是程序员可以预测程序的运行结果,检查程序的运行结果是否与预期一致。

点击查看代码

我们在StringUtil类中新增一个测试方法

java
 public static int getMaxIndex(String data){
     if(data == null){
         return -1;
     }
     return data.length();
 }

接下来,我们在StringUtilTest类中写一个测试方法

java
public class StringUtilTest{
    @Test
    public void testGetMaxIndex(){
       int index1 = StringUtil.getMaxIndex(null);
       System.out.println(index1);

       int index2 = StringUtil.getMaxIndex("admin");
       System.out.println(index2);

        //断言机制:预测index2的结果可能是4
        Assert.assertEquals("方法内部有Bug",4,index2);
    }
}

运行测试方法,结果如下图所示,表示我们预期值与实际值不一致

作业

🚩 1. 参考上面的单元测试的入门案例,练习一下吧?

  1. 测试方法test01 sout输出"我和java不得不说的故事" 观察运行颜色
  2. 测试方法test02 sout输出1/0 观察运行颜色
  3. 测试方法test03 使用断言 Assert.assertEquals("我估计有错",4,2); 观察运行后的颜色

4.3 Junit框架的常用注解 🚀 了解

小伙伴们,刚才我们以及学习了@Test注解,可以用来标记一个方法为测试方法,测试才能启动执行。

除了@Test注解,还有一些其他的注解,我们要知道其他注解标记的方法什么时候执行,以及其他注解在什么场景下可以使用。

接下来,我们演示一下其他注解的使用。我们在StringUtilTest测试类中,再新增几个测试方法。代码如下

java
public class StringUtilTest{
    @Before
    public void test1(){
        System.out.println("--> test1 Before 执行了");
    }
    @BeforeClass
    public static void test11(){
        System.out.println("--> test11 BeforeClass 执行了");
    }
    @After
    public void test2(){
        System.out.println("--> test2 After 执行了");
    }
    @AfterCalss
    public static void test22(){
        System.out.println("--> test22 AfterCalss 执行了");
    }
}

执行上面的测试类,结果如下图所示,观察执行结果特点如下

java
1.被@BeforeClass标记的方法,执行在所有方法之前
2.被@AfterCalss标记的方法,执行在所有方法之后
3.被@Before标记的方法,执行在每一个@Test方法之前
4.被@After标记的方法,执行在每一个@Test方法之后

应用场景

我们现在已经知道每一个注解的作用了,那他们有什么用呢?应用场景在哪里?

  1. 测试之前,需要加载准备数据
  2. 测试之后,需要释放内存

最后,我们再补充一点。前面的注解是基于Junit4版本的,再Junit5版本中对注解作了更新,但是作用是一样的换汤不换药。所以这里就不做演示了

五、多线程 🚩 ❤️ 🍐

多线程

接下来我们来学习一个全新而且非常重要的知识,叫做多线程

线程其实是程序中的一条执行路径

之前写过的程序,其实都是单线程程序,如上图代码,如果前面的for循环没有执行完,for循环下面的代码是不会执行的。

怎样的程序才是多线程程序呢?

  1. 如下图所示:
  • 12306网站就是支持多线程的,因为同时可以有很多人一起进入网站购票,而且每一个人互不影响。

  • 百度网盘,可以同时下载或者上传多个文件。

  • 单线程程序,就好比农村的路,同时只能一辆车通过,而4车道显然同时能通过多辆车

这些程序中其实就有多条执行路径,每一条执行执行路径就是一条线程,所以这样的程序就是多线程程序。

认识了什么是多线程程序,那如何使用Java创建线程呢? Java提供了几种创建线程的方式

多线程的创建方式 ✏️

1️⃣ 4.1 线程创建方式1

Java为开发者提供了一个类叫做Thread,此类的对象用来表示线程。创建线程并执行线程的步骤如下

java
1.定义一个子类继承Thread类,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(启动线程后,会自动执行run方法中的代码)

代码如下

java
public class MyThread extends Thread{
    // 2、必须重写Thread类的run方法
    @Override
    public void run() {
        // 描述线程的执行任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程

java
public class ThreadTest1 {
    // main方法是由一条默认的主线程负责执行。
    public static void main(String[] args) {
        // 3、创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        // 4、启动线程(自动执行run方法的)
        t.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

打印结果如下图所示,我们会发现MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样

最后我们还需要注意一点:不能直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。此时执行结果是这样的。

2️⃣ 4.2 线程创建方式2

接下来我们学习线程的第二种创建方式。Java为开发者提供了一个Runnable接口,该接口中只有一个run方法,意思就是通过Runnable接口的实现类对象专门来表示线程要执行的任务。具体步骤如下

java
1.先写一个Runnable接口的实现类,重写run方法(这里面就是线程要执行的代码)
2.再创建一个Runnable实现类的对象
3.创建一个Thread对象,把Runnable实现类的对象传递给Thread
4.调用Thread对象的start()方法启动线程(启动后会自动执行Runnable里面的run方法)

代码如下:先准备一个Runnable接口的实现类

java
/**
 * 1、定义一个任务类,实现Runnable接口
 */
public class MyRunnable implements Runnable{
    // 2、重写runnable的run方法
    @Override
    public void run() {
        // 线程要执行的任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程输出 ===》" + i);
        }
    }
}

再写一个测试类,在测试类中创建线程对象,并执行线程

java
public class ThreadTest2 {
    public static void main(String[] args) {
        // 3、创建任务对象。
        Runnable target = new MyRunnable();
        // 4、把任务对象交给一个线程对象处理。
        //  public Thread(Runnable target)
        new Thread(target).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出 ===》" + i);
        }
    }
}

运行上面代码,结果如下图所示**(注意:没有出现下面交替执行的效果,也是正常的)**

java
主线程main输出 ===1
主线程main输出 ===2
主线程main输出 ===3
子线程输出 ===1
子线程输出 ===2
子线程输出 ===3
子线程输出 ===4
子线程输出 ===5
主线程main输出 ===4
主线程main输出 ===5

3️⃣ 4.3 线程创建方式2—匿名内部类

小伙伴们注意了,现在这种写法不是新知识。只是将前面第二种方式用匿名内部类改写一下。因为小伙伴们在看别人写的代码时,有可能会看到这种写法。你知道是怎么回事就可以了。

刚刚我们学习的第二种线程的创建方式,需要写一个Runnable接口的实现类,然后再把Runnable实现类的对象传递给Thread对象。

现在我不想写Runnable实现类,于是可以直接创建Runnable接口的匿名内部类对象,传递给Thread对象。

代码如下

java
public class ThreadTest2_2 {
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();

        // 简化形式1:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();

        // 简化形式2:
        new Thread(() -> {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

4️⃣4.4 线程的创建方式3

接下来,我们学习线程的第三种创建方式。已经有两种了为什么还有要第三种呢? 这样,我们先分析一下前面两种都存在的一个问题。然后再引出第三种可以解决这个问题。

  • 假设线程执行完毕之后有一些数据需要返回,前面两种方式重写的run方法均没有返回结果。

    java
    public void run(){
        ...线程执行的代码...
    }
  • JDK5提供了Callable接口和FutureTask类来创建线程,它最大的优点就是有返回值。

    在Callable接口中有一个call方法,重写call方法就是线程要执行的代码,它是有返回值的

    java
    public T call(){
        ...线程执行的代码...
        return 结果;
    }

第三种创建线程的方式,步骤如下

java
1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Thread的start()方法启动线程(启动后会自动执行call方法)
call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中

6.调用FutrueTask对的get()方法获取返回结果

代码如下:先准备一个Callable接口的实现类

java

/**
 * 1、让这个类实现Callable接口
 */
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    // 2、重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "线程求出了1-" + n + "的和是:" + sum;
    }
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果

java
public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        // 3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1  = new FutureTask<>(call);
        // 5、把任务对象交给一个Thread对象
        new Thread(f1).start();


        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2  = new FutureTask<>(call2);
        new Thread(f2).start();


        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        String rs = f1.get();
        System.out.println(rs);

        String rs2 = f2.get();
        System.out.println(rs2);
    }
}

作业

🚩 1. 利用上述的3种创建方式的任意1种创建线程,输出100次我爱java

提示:可以创建100个线程,每个线程打印1次,也可以创建1个线程,单个线程打印100次,还可以创建10个线程,每个线程打印10次

🚩 2.参考上述MyCallable案例,完成一个表白100次,然后给一个结果:你是个好人的案例

提示:在call中运行100次后才返回:你是好人

多线程常用方法 🚩

下面我们演示一下getName()setName(String name)currentThread()sleep(long time)这些方法的使用效果。

点击查看演示代码

java
public class MyThread extends Thread{
    public MyThread(String name){
        super(name); //1.执行父类Thread(String name)构造器,为当前线程设置名字了
    }
    @Override
    public void run() {
        //2.currentThread() 哪个线程执行它,它就会得到哪个线程对象。
        Thread t = Thread.currentThread();
        for (int i = 1; i <= 3; i++) {
            //3.getName() 获取线程名称
            System.out.println(t.getName() + "输出:" + i);
        }
    }
}

再测试类中,创建线程对象,并启动线程

java
public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.setName(String name) //设置线程名称;
        t1.start();
        System.out.println(t1.getName());  //Thread-0

        Thread t2 = new MyThread("2号线程");
        // t2.setName("2号线程");
        t2.start();
        System.out.println(t2.getName()); // Thread-1

        // 主线程对象的名字
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        m.setName("最牛的线程");
        System.out.println(m.getName()); // main

        for (int i = 1; i <= 5; i++) {
            System.out.println(m.getName() + "线程输出:" + i);
        }
    }
}

执行上面代码,效果如下图所示,我们发现每一条线程都有自己了名字了。

点击查看join方法代码

java
public class ThreadTest2 {
    public static void main(String[] args) throws Exception {
        // join方法作用:让当前调用这个方法的线程先执行完。
        Thread t1 = new MyThread("1号线程");
        t1.start();
        t1.join();

        Thread t2 = new MyThread("2号线程");
        t2.start();
        t2.join();

        Thread t3 = new MyThread("3号线程");
        t3.start();
        t3.join();
    }
}

执行效果是1号线程先执行完,再执行2号线程;2号线程执行完,再执行3号线程;3号线程执行完就结束了。

我们再尝试,把join()方法去掉,再看执行效果。此时你会发现2号线程没有执行完1号线程就执行了效果是多次运行才出现的,根据个人电脑而异,可能小伙伴半天也出现不了也是正常的

总结

  1. 重点练习cureentThread 获取当前线程
  2. 练习sleep(毫秒) 让当前执行的线程休眠,俗称睡一下

并发和并行 🍐

先学习第一个补充知识点,并发和并行。在讲解并发和并行的含义之前,我们先来了解一下什么是进程、线程?

  • 正常运行的程序(软件)就是一个独立的进程
  • 线程是属于进程,一个进程中包含多个线程
  • 进程中的线程其实并发和并行同时存在(继续往下看)

我们可以打开系统的任务管理器看看(快捷键:Ctrl+Shfit+Esc),自己的电脑上目前有哪些进程。

知道了什么是进程和线程之后,接着我们再来学习并发和并行的含义。

首先,来学习一下什么是并发?

进程中的线程由CPU负责调度执行,但是CPU同时处理线程的数量是优先的,为了保证全部线程都能执行到,CPU采用轮询机制为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。(简单记:并发就是多条线程交替执行)

接下,再来学习一下什么是并行?

并行指的是,多个线程同时被CPU调度执行。如下图所示,多个CPU核心在执行多条线程

最后一个问题,多线程到底是并发还是并行呢?

其实多个线程在我们的电脑上执行,并发和并行是同时存在的。

线程的生命周期 🍐

接下来,我们学习最后一个有关线程的知识点,叫做线程的生命周期。所谓生命周期就是线程从生到死的过程中间有哪些状态,以及这些状态之间是怎么切换的。

为了让大家同好的理解线程的生命周期,先用人的生命周期举个例子,人从生到死有下面的几个过程。在人的生命周期过程中,各种状态之间可能会有切换,线程也是一样的。

接下来就来学习线程的生命周期。在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示

java
NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。

这几种状态之间切换关系如下图所示

点击查看演示代码

java

/**
 * 1.线程有6种状态,如果不记得,直接在Thread源码中,看State枚举
 * 2.wait方法会导致线程处于Waiting状态,直到被notify或者notifyAll唤醒, wait,notify,notifyAll必须在synchronized 包裹中执行
 * 3.当线程执行完后,会处于消亡状态
 */
public class ThreadStateTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {

              synchronized (Thread.class){
//                  Thread.class.wait(); //思考:如果使用wait 此时状态是什么?
                  Thread.sleep(2222); //思考:如果使用sleep 此时状态是什么?

              }

                System.out.println(Thread.currentThread().getName()+" 执行了");
            }
        });



        System.out.println("1.此时thread的状态为:"+thread.getState());
        thread.start();
        System.out.println("2.此时thread的状态为:"+thread.getState());
        Thread.sleep(1111);
        System.out.println("3.此时thread的状态为:"+thread.getState());
        Thread.sleep(1111);
        System.out.println("4.此时thread的状态为:"+thread.getState());

    }
}

用心去做高质量的内容网站,欢迎 star ⭐ 让更多人发现