【多线程及高并发 二】线程基础及线程中断同步

👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列、MySQL系列、Redis系列、Leetcode算法系列、GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦❤️
✨时间是条环形跑道,万物终将归零,亦得以圆全完美

线程基础及线程中断同步

    • 进程/线程/虚拟线程
    • 线程状态
    • 线程使用方式
      • 继承 Thread
      • 实现 Runnable 接口
      • 实现 Callable 接口
    • 线程机制
      • 基础方法
        • setDaemon
        • yield
        • sleep
      • 线程中断
        • interrupt
        • interrupted
      • 线程同步
        • synchronized
        • ReentranLock
      • 线程协作


多线程及高并发系列

  • 【多线程及高并发 一】内存模型及理论基础

线程(Thread):线程是进程内的执行单元,它是操作系统调度的最小单位。一个进程可以包含多个线程,它们共享进程的资源。线程之间可以并发执行,共享内存空间,因此可以更高效地完成多个任务。Java 中的线程由 Java 虚拟机(JVM)进行管理和调度

多线程的作用:

  1. 提高程序性能:通过利用多核处理器或多处理器系统的并行性,多线程可以同时执行多个任务,从而提高程序的处理能力和执行效率
  2. 提升用户体验:在需要进行耗时操作的情况下,使用多线程可以避免长时间的阻塞,保持用户界面的响应性
  3. 实现异步编程:多线程可以实现异步编程模型,通过在后台执行任务,提供更好的用户体验和系统响应能力
  4. 支持并发处理:在服务器端应用程序中,多线程可以帮助同时处理多个客户端请求,提高系统的并发性和吞吐量
  5. 充分利用资源:多线程可以充分利用计算机的硬件资源,例如 CPU、内存和磁盘等

多线程编程也带来了一些挑战,例如线程安全性数据同步共享资源管理等问题。Java 提供了丰富的并发编程工具和库,如线程类(Thread)、线程池(ThreadPoolExecutor)、锁(Lock)、原子类(Atomic)等,帮助开发人员更方便地进行多线程编程。

进程/线程/虚拟线程

  • 进程(Process):进程是计算机系统中运行的程序的实例。它是资源分配的最小单位,包括内存空间、文件句柄、打开的网络连接等
  • 操作系统线程(OS Thread):线程由操作系统管理,是进程内的执行单元,它是操作系统调度的最小单位
  • 平台线程(Platform Thread):Java.Lang.Thread类的每个实例,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射
  • 虚拟线程(Virtual Thread):一种轻量级,由 JVM 管理的线程。对应的实例java.lang.VirtualThread这个类
  • 载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程

虚拟线程(Virtual Thread)它不与特定的操作系统线程相绑定。它在平台线程上运行 Java 代码,但在代码的整个生命周期内不独占平台线程。**这意味着许多虚拟线程可以在同一个平台线程上运行他们的 Java 代码,共享同一个平台线程。详见虚拟线程原理及性能分析

线程状态

线程状态转换

  • 新建状态(New):当线程对象被创建时,它处于新建状态。此时线程尚未启动,还未调用start()方法。
  • 运行状态(Runnable):在线程启动后,它进入运行状态。线程可以在多个线程中竞争处理器资源,但具体的执行顺序由调度器决定。
  • 阻塞状态(Blocked):线程在某些条件下暂停执行,进入阻塞状态。常见的情况包括等待获取锁、等待I/O操作完成、等待其他线程的通知等。当条件满足时,线程会重新进入就绪状态。
  • 等待状态(Waiting):线程进入等待状态是出于某些条件的需要,线程会主动停止执行,直到满足特定的条件才会被唤醒。线程可以通过调用Object.wait()、Thread.join()或LockSupport.park()等方法进入等待状态。
  • 超时等待状态(Timed Waiting):线程在特定的时间范围内等待,如果在指定的时间内未满足条件,线程会自动唤醒。线程可以通过调用Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)或LockSupport.parkNanos()等方法进入超时等待状态。
  • 终止状态(Terminated):线程执行完任务或者由于异常等原因终止执行,进入终止状态。一旦线程进入终止状态,它将不再执行

线程使用方式

Java中有三种常见的线程使用方式:

  1. 继承 Thread 类:适合简单的线程任务,不需要额外的线程控制
  2. 实现 Runnable 接口:适合于需要执行的任务不需要返回结果的情况
  3. 实现 Callable 接口:适合需要执行任务并且获取返回结果的情况

实现RunnableCallable接口的类实际上是任务,最后还需要通过Thread来执行

继承 Thread

创建一个继承自Thread类的子类,重写其run()方法。然后可以创建该子类的实例,并调用start()方法来启动线程

这种方式适合简单的线程任务,不需要额外的线程控制

public class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

实现 Runnable 接口

创建一个实现了Runnable接口的类,重写其run()方法。然后可以创建一个Thread对象,将该实现类的实例作为参数传递给Thread构造函数。最后调用Thread对象的start()方法来启动线程

这种方式适合于需要执行的任务不需要返回结果的情况

public class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

实现 Callable 接口

创建一个实现了Callable接口的类,重写其call()方法。然后可以创建一个FutureTask对象,将该实现类的实例作为参数传递给FutureTask构造函数,最后通过FutureTask对象可以获取任务执行的结果

这种方式适合需要执行任务并且获取返回结果的情况

public class MyCallable implements Callable<String> {
    public String call() {
        // 线程执行的代码
        return "Hello, World!";
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

线程机制

在 Java 中,Thread 类提供了方法及工具来控制线程的行为

  • 基础方法:setDaemon(boolean on)yield()sleep(long millis)
  • 线程中断:interrupted()
  • 线程同步: synchronizedReentranLock
  • 线程协助:join()wait()notify()notifyAll()

基础方法

setDaemon

作用:将线程设置为守护线程(daemon thread)或用户线程(user thread)

  • 守护线程是在后台提供服务的线程。当所有的用户线程结束时,守护线程也会自动结束。典型的守护线程包括垃圾回收线程(Garbage Collector)
  • 用户线程是在前台执行的线程,不会影响 JVM 的关闭。守护线程的存在并不会阻止 JVM 退出
public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        daemonThread.setDaemon(true); // 设置为守护线程
        daemonThread.start();

        System.out.println("Main Thread is exiting");
    }
}

守护线程daemonThread,它会不停地输出一条消息。直到主线程(即 main 方法)退出时,守护线程也会随之自动结束

yield

作用:提示调度器当前线程愿意放弃当前的 CPU 时间片,让其他具有相同优先级的线程执行。

调用yield()方法不会导致线程进入阻塞状态,而是将线程从运行状态转换为就绪状态

yield()方法是对线程调度器的一个建议,它在一定程度上提高了线程之间的公平性

public class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1: " + i);
                Thread.yield();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2: " + i);
                Thread.yield();
            }
        });

        thread1.start();
        thread2.start();
    }
}

尽可能交替执行输出,但不保证,因为yield()的线程也可能又被CPU调度

sleep

作用:使当前线程暂停执行指定的时间,进入阻塞状态。
参数:millis指定线程休眠的时间(以毫秒为单位)

sleep()方法会暂时释放 CPU,使得其他线程有机会执行。在指定的时间过去后,线程会重新进入就绪状态,等待重新获得 CPU 执行

public class SleepExample {
    public static void main(String[] args) {
        System.out.println("Before sleep");
        
        try {
            Thread.sleep(3000); // 休眠 3 秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("After sleep");
    }
}

线程中断

interrupt

Java 提供了 Thread 类的interrupt()方法来中断线程的执行。

调用interrupt()方法会将线程的中断标志位设置为true

  • 如果线程处于阻塞状态(如调用了sleep()wait()join()方法),会抛出InterruptedException异常并清除中断标志位
  • 如果线程处于非阻塞状态,在适当的时机需要检查线程的中断标志位,并采取相应的处理逻辑
interrupted

作用:静态方法,检查当前线程的中断状态,并重置中断标志位。通常在当前线程需要处理中断状态时使用

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread2 = new MyThread2();
        thread2.start();
        thread2.interrupt();
    }
}

如果一个线程的run()方法执行一个无限循环,并且没有执行sleep()等会抛出 InterruptedException 的操作,那么调用线程的interrupt()方法就无法使线程提前结束。此时需要interrupted()方法来判断线程是否处于中断状态,从而提前结束线程

线程同步

synchronized

Java 中的synchronized关键字用于实现线程同步,确保在同一时间只有一个线程可以进入被 synchronized 修饰的方法或代码块,从而防止多个线程同时访问共享资源,避免出现数据竞争和并发访问的问题

synchronized原理分析、锁位置、锁状态及锁升级详解见【多线程及高并发 二】volatile & synchorized 详解

  1. 修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁
  2. 修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

synchronized关键字在JDK 1.5 前本质上是一把悲观锁。JDK 1.5 之后进行了优化,引入了锁升级的概念。在多线程竞争不激烈的情况下,锁会从无锁状态逐渐升级为偏向锁、轻量级锁,最后升级为重量级锁,以提高性能

ReentranLock

ReentrantLock是 Java 提供的可重入锁(Reentrant Lock)实现,它是在java.util.concurrent.locks包中的一个类。与传统的synchronized关键字相比,ReentrantLock提供了更多可编程的灵活性和功能,例如可重入性、公平性、条件变量和更精细的线程控制

  1. 可重入性:ReentrantLock 是可重入锁,意味着同一个线程可以多次获取同一个锁而不会造成死锁
  2. 加锁和解锁:使用 ReentrantLock,可以使用lock()方法进行加锁,使用unlock()方法进行解锁
  3. 公平性和非公平性:ReentrantLock 可以构造为公平锁或非公平锁。公平锁会按照线程请求锁的顺序分配锁,而非公平锁则允许插队,可能会导致某些线程长时间等待
  4. Condition条件变量:ReentrantLock 提供了与 CONDITION 相关联的条件变量,用于实现更复杂的线程通信和同步
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int count = 0;

    public void increment() {
        lock.lock(); // 加锁
        try {
            count++;
            System.out.println("Incremented: " + count);
            condition.signalAll(); // 唤醒等待的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public void decrement() throws InterruptedException {
        lock.lock(); // 加锁
        try {
            while (count == 0) {
                condition.await(); // 等待条件满足
            }
            count--;
            System.out.println("Decremented: " + count);
        } finally {
            lock.unlock(); // 解锁
        }
    }
    
    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        
        Thread incrementThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        Thread decrementThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    example.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        incrementThread.start();
        decrementThread.start();
    }
}

在上述示例中创建了一个ReentrantLock对象lock和一个与之关联的Condition对象condition

  • increment()方法使用lock加锁,递增count的值,并唤醒等待的线程
  • decrement()方法使用lock加锁,当count为 0 时,调用condition.await()方法等待条件满足,否则递减count的值

线程协作

在Java中,Thread类提供了几个用于线程间协作的方法,包括join()wait()notify()notifyAll()。这些方法用于实现线程的等待、唤醒和协调操作

Thread 类的wait()notify()notifyAll()方法是与对象的监视器(monitor)相关联的,而不是直接与 Thread 类相关联。这些方法是基于对象的锁机制实现的,用于线程间的协调和通信

join()方法用于等待调用线程完成其执行,然后再继续执行当前线程

  • 调用某个线程的join()方法会使当前线程进入阻塞状态,直到被调用线程执行完毕
  • 如果在join()方法中指定了超时时间,当前线程最多会等待指定的时间,然后继续执行
  • join()方法通常与多线程的任务分割和结果合并中使用

wait()notify()notifyAll()这三个方法是用于线程间的等待和唤醒机制,需要在同步代码块或同步方法中使用。被唤醒的线程会重新竞争对象的锁,一旦获得锁,就可以继续执行

  • wait()方法使当前线程进入等待状态,放弃对象的锁,并等待其他线程调用相同对象的notify()notifyAll()方法来唤醒它
  • notify()方法唤醒在相同对象上调用wait()方法并进入等待状态的单个线程。
  • notifyAll()方法唤醒在相同对象上调用wait()方法并进入等待状态的所有线程

wait()方法在调用前需要先获得锁,否则会抛出IllegalMonitorStateException异常

public class ThreadCooperationExample {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread 1 starts");
                    Thread.sleep(2000);
                    System.out.println("Thread 1 notifies");
                    lock.notify(); // 唤醒等待的线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread 2 starts");
                    lock.wait(); // 等待被唤醒
                    System.out.println("Thread 2 continues");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start();
        thread2.start();

        thread1.join(); // 等待 thread1 执行完毕
        thread2.join(); // 等待 thread2 执行完毕

        System.out.println("Main thread finishes");
    }
}

在上述示例中创建了两个线程 thread1 和 thread2。thread1 在执行过程中调用lock.notify()方法唤醒等待的线程,而 thread2 在执行过程中调用lock.wait()方法等待被唤醒。main 线程使用join()方法等待 thread1 和 thread2 执行完毕后再继续执行


参考资料:

  1. Java 并发编程实战
  2. 虚拟线程原理及性能分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/269579.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloud和Dubbo有哪些区别

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; Spring ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 介绍 Spring Cloud&#xff1a; Dubbo&#xff1a; 选择&#xff1a; 区别 结语 我的其他博客 前言 构建分布式系统是…

Netty组件基础

Netty入门简介 netty是一个异步、基于事件驱动的网络应用框架&#xff0c;用于快速开发可维护、高性能的网络服务器和客户端。 Netty优势 Netty解决了TCP传输问题&#xff0c;如黏包、半包问题&#xff0c;解决了epoll空轮询导致CPU100%的问题。并且Netty对API进行增强&#xf…

Playbook中jinja2模板的使用

本章主要介绍playbook中如何使用jinja2模板 什么是jinja2模板在jinja2模板文件中写if判断语句在jinja2模板文件在写for循环语句handlers的使用 可以使用copy模块把本地的一个文件拷贝到远端机器&#xff0c;下面再次复习一下 本章实验都在demo4文件夹下操作&#xff0c;跟之…

【RocketMQ】Console页面报错:rocketmq remote exception,connect to xxx failed.

现象 console报错&#xff0c;无法连接该节点&#xff0c;把该节点杀掉&#xff0c;还是继续报错&#xff0c;重启之后&#xff0c;报错的端口变成11911。 分析 正常一个broker会启动三个端口&#xff0c;不同版本的规律不太一样&#xff0c;4.X版本是&#xff1a; 配置文件…

软件工程经济学习题 答案(不保证对错,找不到答案)

一、资金等值计算 1.某IT企业今年向银行贷款20万元以购置一台设备。若银行贷款利率为10%&#xff0c;规定10年内等额偿还&#xff0c;试求每年的偿还金额。 2.某软件企业向银行贷款200万元&#xff0c;按年利率为8%进行复利计息&#xff0c;试求该企业第5年末连本带利一次偿还银…

计算机视觉技术-使用图像增广进行训练

让我们使用图像增广来训练模型。 这里&#xff0c;我们使用CIFAR-10数据集&#xff0c;而不是我们之前使用的Fashion-MNIST数据集。 这是因为Fashion-MNIST数据集中对象的位置和大小已被规范化&#xff0c;而CIFAR-10数据集中对象的颜色和大小差异更明显。 CIFAR-10数据集中的前…

Cost Calculator Builder PRO v3.1.46 已注册 – WordPress 插件

成本计算器生成器 PRO v3.1.46&#xff1a;WordPress 插件全解析 一、插件概述 "成本计算器生成器 PRO v3.1.46"是一款强大的WordPress插件&#xff0c;专为需要创建报价、价格和项目估算表的用户设计。这款插件集成了众多高级功能&#xff0c;可帮助用户高效地管理…

LVM逻辑卷管理

传统磁盘存在的问题&#xff1a; 1.当分区不够用时&#xff0c;无法扩展大小。 2.当磁盘分区不够用时&#xff0c;只能通过添加硬盘的方式&#xff0c;但是新添加的硬盘只能当作独立的系统文件存在。 所以如果生产环境的数据库的数据目录满了&#xff0c;只能通过添加新的硬…

计算机毕业设计------SSM网上超市购物商城管理系统

项目介绍 本项目分为前后台&#xff0c;分为普通用户、管理员两种角色。前台普通用户登录&#xff0c;后台管理员登录&#xff1b; 管理员角色包含以下功能&#xff1a; 登录页面,用户查看,一级分类管理,二级分类管理,商品管理,查看订单,留言管理等功能。 用户角色包含以下功…

Frequency-domain MLPs are More EffectiveLearners in Time Series Forecasting

本论文来自于 37th Conference on Neural Information Processing Systems (NeurIPS 2023) Abstract 时间序列预测在金融、交通、能源、医疗等不同行业中发挥着关键作用。虽然现有文献设计了许多基于 RNN、GNN 或 Transformer 的复杂架构&#xff08;注意力机制的计算太占用资…

探索前端构建可视化应用的思路

一、前言 前端社区里&#xff0c;低代码/无代码是被讨论的火热赛道。简单来说低代码就是通过编写少量代码的方式完成应用的开发及上线&#xff0c;而无代码是低代码的子集&#xff0c;不需要编写代码通过配置的方式即可完成整个应用的开发。目前集团内部的低代码平台已经有很多…

【JMeter入门】—— JMeter介绍

1、什么是JMeter Apache JMeter是Apache组织开发的基于Java的压力测试工具&#xff0c;用于对软件做压力测试。它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 &#xff08;Apache JMeter是100%纯JAVA桌面应用程序&#xff09; Apache JMeter可以用于对静…

ElasticSearch入门介绍和实战

目录 1.ElasticSearch简介 1.1 ElasticSearch&#xff08;简称ES&#xff09; 1.2 ElasticSearch与Lucene的关系 1.3 哪些公司在使用Elasticsearch 1.4 ES vs Solr比较 1.4.1 ES vs Solr 检索速度 2. Lucene全文检索框架 2.1 什么是全文检索 2.2 分词原理之倒排索引…

OpenCV之图像匹配与定位

利用图像特征的keypoints和descriptor来实现图像的匹配与定位。图像匹配算法主要有暴力匹配和FLANN匹配&#xff0c;而图像定位是通过图像匹配结果来反向查询它们在目标图片中的具体坐标位置。 以QQ登录界面为例&#xff0c;将整个QQ登录界面保存为QQ.png文件&#xff0c;QQ登…

App测试时常用的adb命令你都掌握了哪些呢?

adb 全称为 Android Debug Bridge&#xff08;Android 调试桥&#xff09;&#xff0c;是 Android SDK 中提供的用于管理 Android 模拟器或真机的工具。 adb 是一种功能强大的命令行工具&#xff0c;可让 PC 端与 Android 设备进行通信。adb 命令可执行各种设备操作&#xff0…

天软特色因子看板 (2023.12 第14期)

该因子看板跟踪天软特色因子A06008聪明钱因子(beta))&#xff0c;该因子为以分钟行情价量信息为基础&#xff0c;识别聪明钱交易&#xff0c;用以刻画机构交易行为 值越大&#xff0c;越反映其悲观情绪&#xff0c;反之&#xff0c;反映其乐观情绪。 今日为该因子跟踪第14期&am…

ACM模式Java输入输出模板

输入输出练习网站&#xff1a;https://kamacoder.com/ Java读写模板 Scanner 方式一&#xff1a;Scanner&#xff08;效率不高&#xff09; public class Main {public static void main(String[] args) {// 第一个方式ScannerScanner sc new Scanner(System.in);String s …

Python画皮卡丘

代码&#xff1a; import turtledef getPosition(x, y):turtle.setx(x)turtle.sety(y)print(x, y)class Pikachu:def __init__(self):self.t turtle.Turtle()t self.tt.pensize(3)t.speed(9)t.ondrag(getPosition)def noTrace_goto(self, x, y):self.t.penup()self.t.goto(…

WebGL开发建筑和设计教育应用

使用 WebGL 开发建筑和设计教育应用可以为学生提供沉浸式的三维体验&#xff0c;使他们能够在虚拟环境中探索建筑结构、材料和设计理念。以下是开发建筑和设计教育应用的一般步骤&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&…

【开源】基于JAVA的学校热点新闻推送系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 新闻类型模块2.2 新闻档案模块2.3 新闻留言模块2.4 新闻评论模块2.5 新闻收藏模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 新闻类型表3.2.2 新闻表3.2.3 新闻留言表3.2.4 新闻评论表3.2.5 新闻收藏表 四、系统展…