前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

线程

作者头像
胖虎
发布2020-11-24 10:17:59
2570
发布2020-11-24 10:17:59
举报
文章被收录于专栏:晏霖晏霖

点击上方“晏霖”,选择“置顶或者星标”

曾经有人关注了我

后来他有了女朋友

并发编程在开发一般的项目确实接触很少,也导致平时学习的机会也不多,基本上都是碰到相关问题时会查一些相关资料去解决,所以很少会对这本分知识有系统和全面的了解。虽说对于一般开发人员接触较少,但是我们也是深刻认识到这部分的内容对于面试的出场率基本是100%,如果把这部分内容啃下来,你的java基础知识储备会上升一个台阶。

本章内容概念性很强,理解起来会很抽象,我们可以在较难理解的概念上抽象出一个模型在大脑里,这样可以免去死记硬背的痛苦

2.1线程

了解并发首先我们要了解线程,一切并发都源于线程。先来了解下什么是线程。

百度百科中,线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

如果我是对线程不熟悉对话,上面概念性对叙述其实对理解线程并没有帮助,所以我们用其他模型联想一下。

世间万物都是由每个基本的事件完成的,例如一个正常的人需要呼吸、血液循环、细胞的死亡与再生、吃饭、工作,完成这些每一个事件成为线程,而这些事件又可以同时进行,这就叫并发。

刚刚提到了进程,那么什么是进程?

简单的说进程是资源分配的基本单位,他是应用程序的运行实例,每次启动一个程序就意味着开启了一个进程,并为这个进程分配资源,而线程又是进程的最小执行单位,一个进程拥有多个线程,这些线程会拥有各自的计数器、堆、栈和局部变量等属性,并且可以访问系统共享的内存数据。每一个线程必须有一个父进程,这些拥有相同父进程的其它线程共享该进程所拥有的全部资源。

2.1.2守护线程

在Java里线程一共分为两种,User Thread(用户线程)、Daemon Thread(守护线程) ,我们平时工作中常说的线程基本都是属于用户线程,我们很少对守护线程进行操作的。当一个JVM启动时,他会自动创建一些后台线程,并创建一个主线程来运行main方法,这些后台程序就是守护线程,我们经常接触最典型守护线程的应用就是GC(垃圾收集器)。

用户线程和守护线程在工作中几乎没有任何区别,只要是线程就必须要分配资源和一个父进程,唯一的区别就是只要JVM存在一个用户线程,守护线程就全部工作,直到最后一个用户线程结束了生命,守护线程才会跟着JVM一同结束。

在计算机的世界存在绝对的忠诚,只要你在我就一直陪着你,直到你死去,就像守护线程和用户线程。

既然守护线程这么忠诚,我们可以不可以主动创建呢?答案是可以的,如示例代码清单。

代码清单2-1 Daemon.java

代码语言:javascript
复制
@Slf4j
public class Daemon {
    public static void main(String[] args) {
        Thread t1 = new Thread(new DaemonRunner());
        Thread t2 = new Thread(new commonRunner());
        // 设定 daemonThread 为 守护线程,default false(非守护线程)
        t1.setDaemon(true);
        // 验证当前线程是否为守护线程,返回 true 则为守护线程
        System.err.println(t1.isDaemon());
        t1.start();
        t2.start();
    }

    static class commonRunner implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i < 5; i++) {
                log.info("用户线程第:{}次执行", i);
            }
        }
    }

    static class DaemonRunner implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                log.info("守护线程第:{}次执行", i);
            }
        }
    }
}

运行Daemon程序,可以看出t1线程运行结束后,t2线程随之终止。

2.1.3如何判断使用多线程

我们清楚了线程与进程的关系,就不难知道系统创建一个进程,必须为该进程分配独立的内存空间,和大量相关资源,但创建线程代价就会小很多,多个线程共享该进程的虚拟空间,包括进程中的代码、共有数据等,所以线程之间更容易实现通信,因此使用多线程来实现多任务处理比多进程效率高,再加上Java语言提供很多支持多线程的功能,简化了多线程编程,所以我们经常会用多线程处理一些逻辑。

现如今我们所接触到到服务器几乎都是多核处理器,大大提升了运行效率,这是不是意味着在一个进程中创建很多线程,运行效率也会成直线上升呢?当然是否定的。解释这个问题之前我们要了解什么是CPU密集型(CPU-bound)、IO密集型(I/O bound)。

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

简言之CPU密集型会消耗掉大量的CPU资源,例如需要大量的计算、图片处理、视频渲染、仿真之类的。涉及到网络、磁盘IO的都是IO密集型,相反这个时候CPU利用率并不高。

因此我们可以对刚刚的问题下一个结论,当一个CPU密集型程序多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率,切换线程需要浪费资源的,如果线程数超过核心CPU数,运行效率反而下降。因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。反过来对于一个涉及到网络、磁盘IO程序来说,这种操作并不“烧脑”,CPU占用率较低,这个时候适合使用多线程。

在实际编码中网络传输,文件的写入写出,导入导出等都适合使用多线程,在一些处理算法时不太建议使用多线程,除非能控制一个最佳线程数。

当然我们也要具体问题具体分析,万物都不是绝对的,数据库的IO密集型和CPU密集型是相对概念。一个查询对一个CUP很多很快的服务器而言,可能是IO密集型,对一个装备高速磁盘阵列的服务器而言可能变成CPU密集型。

2.1.4线程状态

Java中每一个线程是有状态的,在给定一个时刻,线程只能处于一种状态。线程一共有6种状态,如表2-1所示。在package java.lang;下Thread.class中明确列出一个枚举方法列举出线程状态,如代码清单2-2.

代码清单2-2 Thread.class

代码语言:javascript
复制
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

表2-1 Java线程状态

状态

描述

NEW

线程被创建,但是没有调用start()方法。

RUNNABLE

可运行程序中的线程,状态正在Java虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。

BLOCKED

处于阻塞状态的线程正在等待监视器锁定

WAITING

等待线程状态,例如调用了Object.wait(),或者Thread.join()正在等待指定的线程终止。

TIMED_WAITING

具有指定等待时间的等待线程的线程状态,由于调用具有指定正等待时间的方法,如Thread.sleep、Object.wait等。

TERMINATED

终止线程的线程状态,线程已完成执行。

下面我们用代码分别使线程表现出以上6种形式,示例如代码清单2-3所示。

代码清单2-3 ThreadState.class

代码语言:javascript
复制
@Slf4j
public class ThreadState {
    private static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new NewRunner());
        log.info(t1.getState().toString());

        Thread t2 = new Thread(new NewRunner());
        t2.start();
        log.info(t2.getState().toString());

        Thread t3 = new Thread(new BlockedRunner());
        t3.start();
        Thread t3_1 = new Thread(new BlockedRunner());
        t3_1.start();
        Thread.sleep(1000);
        log.info(t3_1.getState().toString());

        Thread t4 = new Thread(new WaitingRunner());
        t4.start();
        Thread.sleep(100);
        log.info(t4.getState().toString());
        LockSupport.unpark(t4);

        Thread t5 = new Thread(new TimeWaitingRunner());
        t5.start();
        Thread.sleep(100);
        log.info(t5.getState().toString());

        Thread t6 = new Thread(new TerminatedRunner());
        t6.start();
        Thread.sleep(100);
        log.info(t6.getState().toString());
    }


    static class NewRunner implements Runnable {
        @Override
        public void run() {
        }
    }

    static class RunnableRunner implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class BlockedRunner implements Runnable {
        @Override
        public void run() {
            synchronized (object) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class WaitingRunner implements Runnable {
        @Override
        public void run() {
            LockSupport.park();
        }
    }

    static class TimeWaitingRunner implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(8000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class TerminatedRunner implements Runnable {
        @Override
        public void run() {
        }
    }
}

运行示例代码,打印终端分别是上述6种线程状态。这里有必要说一下上述代码中

当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。当该线程处于这种等待的时候,其状态即为WAITING,park()这种等待是没有时间限制的。线程的状态不是固定的,随着程序执行可以在不同状态中切换,Java线程状态图如2-1示。

图2-1 Java状态图切换

2.1.5线程优先级与控制

线程在操作系统中通过调度器来决定调度哪些线程执行,在Java里是通过一个线程调度器来监控程序启动后进去就绪状态的所有线程。一般来说,线程调度器采用时间片轮转算法使多个线程轮转获得CPU的时间片,当线程的时间片用完了就会发生线程调度,并等待下次分配。线程分配的时间片多少也决定了线程使用处理器资源的多少,但有时候我们想让一些线程优先执行,那么我们可以将他的优先级调高一下,这样它们获得的时间片会多一些。多个线程处于就绪状态时,若这些线程的优先级相同,则线程调度器会按时间片轮转方式或独占方式来分配线程的执行时间。

Java中通过一个整型的变量来设置优先级,优先级范围从1~10来表示,分为三个级别:

低优先级:1~4,其中类变量Thread.MIN_PRORITY最低,数值为1;

默认优先级:如果一个线程没有指定优先级,默认优先级为5,由类变量Thread.NORM_PRORITY表示;

高优先级:6~10,类变量Thread.MAX_PRORITY最高,数值为10。

设置优先级也有一个技巧,我们针对频繁阻塞(I/O操作)的线程尽量设置较高优先级,而偏于计算和算法的(占用CPU操作)的线程则可以设置较低优先级,确保处理器不会被独示例如代码清单2-4所示。

代码清单2-4 Priority.java

代码语言:javascript
复制
@Slf4j
public class Priority {

    public static void main(String[] args) {
        PriorityRunner thread1 = new PriorityRunner("thread1");
        PriorityRunner thread2 = new PriorityRunner("thread2");
        PriorityRunner thread3 = new PriorityRunner("thread3");
        thread1.setPriority(MIN_PRIORITY);
        thread3.setPriority(MAX_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
    }

    static class PriorityRunner extends Thread {
        public PriorityRunner(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i=1;i<4;i++){
               log.info(this.getName()+"循环了:" + i + "次");
            }
        }
    }
}

运行结果如下:由结果可以看出,由于高优先级线程先执行的概率比低优先级的线程高,所以高优先级的线程总先比低优先级执行完并且线程的优先级和代码的执行顺序没有关系。这种设置线程优先级的方法不能作为程序正确性的依赖,因为不同的操作系统下设置同样的优先级,执行结果可能会不同,操作系统有时也会忽略你在java里面设置的优先级。

代码语言:javascript
复制
21:55:50.310 [thread2] INFO com.px.book.Priority - thread2循环了:1次

21:55:50.310 [thread3] INFO com.px.book.Priority - thread3循环了:1次

21:55:50.310 [thread1] INFO com.px.book.Priority - thread1循环了:1次

21:55:50.314 [thread3] INFO com.px.book.Priority - thread3循环了:2次

21:55:50.314 [thread1] INFO com.px.book.Priority - thread1循环了:2次

21:55:50.314 [thread3] INFO com.px.book.Priority - thread3循环了:3次

21:55:50.314 [thread1] INFO com.px.book.Priority - thread1循环了:3次

21:55:50.314 [thread2] INFO com.px.book.Priority - thread2循环了:2次

21:55:50.315 [thread2] INFO com.px.book.Priority - thread2循环了:3次

Java也提供了API用来控制线程的执行,其中最为我们常见的方法就是Thread提供的join()方法。如果当程序中的线程执行了join()方法,当前调用线程被阻塞,直到被join方法的join线程执行完为止。换句话说A线程执行了thread.join()方法,就是当thread线程执行结束后A线程才能执行。这个方法不是常用,一般用在讲一个大方法划分许多小方法,每一个小方法分配一个线程,当这些线程都执行完后,再调用主线程执行,有点类似于ForkJoinPool。例如代码清单2-5所示。

代码清单2-4 Join.java

代码语言:javascript
复制
@Slf4j
public class Join {
    public static void main(String[] args) throws Exception {
        new JoinRunner("thread1").start();//子线程
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                JoinRunner j = new JoinRunner("thread" + i);//被join的子线程
                j.start();
                j.join();
            }
            log.info(Thread.currentThread().getName() + " " + i);//主线程等待主线程结束后执行
        }
    }

    static class JoinRunner extends Thread {
        public JoinRunner(String name) {
            super(name);
        }

        @Override
        public void run() {
            for (int i = 1; i < 4; i++) {
                log.info(this.getName() + "循环了:" + i + "次");
            }
        }
    }
}

示例代码输出如下:

代码语言:javascript
复制
22:35:18.296 [thread1] INFO com.px.book.Join - thread1循环了:1次

22:35:18.300 [thread1] INFO com.px.book.Join - thread1循环了:2次

22:35:18.300 [thread1] INFO com.px.book.Join - thread1循环了:3次

22:35:18.296 [main] INFO com.px.book.Join - main 0

22:35:18.302 [main] INFO com.px.book.Join - main 1

22:35:18.302 [main] INFO com.px.book.Join - main 2

22:35:18.302 [main] INFO com.px.book.Join - main 3

22:35:18.302 [main] INFO com.px.book.Join - main 4

22:35:18.302 [thread5] INFO com.px.book.Join - thread5循环了:1次

22:35:18.302 [thread5] INFO com.px.book.Join - thread5循环了:2次

22:35:18.302 [thread5] INFO com.px.book.Join - thread5循环了:3次

22:35:18.303 [main] INFO com.px.book.Join - main 5

22:35:18.303 [main] INFO com.px.book.Join - main 6

22:35:18.303 [main] INFO com.px.book.Join - main 7

22:35:18.303 [main] INFO com.px.book.Join - main 8

22:35:18.303 [main] INFO com.px.book.Join - main 9

我们很清楚的发现作为第一个子线程thread1执行3次结束后,被阻塞的5个主线程执行。当i==5时程序启动了被joi的子线程thread5,thread5执行3次结束后,被阻塞的5个主线程陆续执行。

我们再来介绍两个控制线程的方法,线程睡眠:sleep(),这是应该是我们很熟悉的方法。当线程执行sleep()方法时,线程会暂停,并进入阻塞状态,这其睡眠的时候,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,因此sleep()方法经常用来暂停程序的执行。

此外Thread中还提供了一个与sleep()方法相似的静态方法yield(),它可以让当前正在执行的线程暂停,和sleep的区别是它不会阻塞,而是把线程状态转为就绪,放到线程队列最后,让Java调度器重新调度一次,若当时队列为空(处于就绪状态的线程仅有一个),则yield()方法无效。

一般来说,重新调度之后与被暂停线程优先级相同的或比它优先级高的线程对象获取CPU使用权的可能性较大,但也有可能重新调度的仍是被暂停的同一个线程对象,换句话说,sleep()不会理会其他线程的优先级,但yield()方法只会给优先级相同或更高的优先级线程执行机会。sleep()方法比yield()方法有更好的可移植性,所以一般不建议使用yield()方法来控制并发线程的执行。

2.1.6线程通信

线程有自己独立的栈空间,可以让他像脚本一样按照规定的路线执行完毕。多线程的意义或前提就是线程间可以互相通信,配合完成,这才会带来巨大的效率。

我们用生产者-消费者模型举一个简单的例子来说明。当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

实现线程通信的方式也有很多,我们在网上可以看到用同步方法、加锁机制、volatile关键字、JUC相关的工具类等等,其实这些都是对的,由于很多内容在其他章节会有介绍所以本小结只做概念性的说明。

最简单也是最传统的线程通信就是等待/通知机制。这种机制可以用Object类提供的wait()、notify()、notifyAll()三个方法,这三个方法是任何对象都有的,而不属于线程的,但这三个方法必须由同步监视器对象来调用。

我来解释一下什么是同步监视器,如果某个对象不是线程安全的,我们可以用很多方法使其安全使用,例如我可以使该对象单线程访问(线程封闭),也可以通过一个锁来保护对象(Java监视器模式)。Java监视器模式是来自于Hoare对监视器机制对研究工作(Hoare,1974),但这种模式与真正但监视器类之间存在一些重要的差异。进入和退出同步代码块的字节指令也称为monitorenter和monitorexit,而Java的内置锁也称为监视器锁或监视器。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

说了这么多关于Java监视器模式的概念,大家肯定明白这不就是Java内置关键字synchronized。没错,但我介绍这么多概念的目的是,不想让大家一说到同步就和synchronized联想起来,他不是同步的代言人,同步有很多种方式,只是synchronized是最简单的一种,正确的说法synchronized是在Java中监视器模式的体现而已,而真正的监视器是synchronized中的参数或者修饰的方法和类,如2-5代码清单。

代码清单2-4 Synchronized.java

代码语言:javascript
复制
public class Synchronized {
    public static void main(String[] args) {
        //对该类的实例加锁
        synchronized (Synchronized.class) {
            method();
        }
    }

    static void method() {
    }
}

对于使用synchronized修饰的方法,使用synchronized(this),同步监视器就是该类的实例。如果使用synchronized修饰代码块,同步监视器就是synchronized后括号的对象,必须使用该对象调用wait()、notify()、notifyAll()这三个方法。

用一个表解释这三个方法,如表2-2 所示。

表2-2 等待/通知相关方法

方法名

描述

wait()

导致线程等待进入WAITING状态,只能等待同步监视器调用notify()或notifyAll()来唤醒。

wait(long)

带毫秒参数,等待长达n毫秒后自动苏醒并会释放同步监视器的锁定。

wait(long,int)

更细粒度的控制,可达到纳秒。

notify()

唤醒在此同步监视器上等待的单个线程,如果有多个,只会选择其一而且是随机选择。

notifyAll()

唤醒在此同步监视器上等待的所有线程

2.1.7线程安全问题

线程安全是我们常碰见的事情,例如我们经常说stringBuff是线程安全的,而 stringBuild是线程不安全的,难道我们真的以为线程是有危险性的吗?其实不是的,线程的安不安全指的是如果单线程环境中,这个类或方法或对象能正确地工作,在多线程环境下则不能,或者预期结果与单线程不同或错误,则我们说这个类或方法或对象是线程不安全的。

线程安全性可能是非常复杂的,在没有充分同步的情况下,多线程操作执行顺序是不可预测的,甚至会产生奇怪的结果。下面我们就来演示一下多线程操作引发的不安全问题,如代码2-5ThreadUnsafe.java代码清单所示。

代码清单2-5 ThreadUnsafe.java

代码语言:javascript
复制
@Slf4j
public class ThreadUnsafe {
    private static int value = 0;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <= 10000; i++) {
            executorService.execute(() -> {
                addValue();
            });
        }
        executorService.shutdown();
        log.info(value + "");
    }

    private static void addValue() {
        value++;
    }
}

输出结果:13:40:05.589 [main] INFO com.px.book.ThreadUnsafe - 9991

很显然上面的执行结果不符合我们预期的结果,我们忽略的使用多线程进行复合操作带来的线程安全问题,引发这一问题的根本原因是由于多线程要共享内存地址空间,而且是并发运行,因此线程可能会访问或修改其他线程正在使用的变量。例如我开启了5个线程,线程1拿到value=10进行给10加1,此时线程1没有把运算结果重新写入内存导致线程2拿到的vcalue也是10,两个线程执行结束后value=11。线程会由于无法预料的数据变化而发生错误。当多个线程同时访问和修改相同变量时,将会在串行编程模型中引入非串行因素,而这种非串行性是很难分析的。要想多线程程序行为结果可预测,就必须对共享变量对访问操作进行协同,这样才不会在其他线程间产生干扰。Java里有了丰富的API和工具对多线程安全提供帮助。例如上面的错误问题可以把value声明成原子,如2-6代码清单。

代码清单2-5 ThreadSafe.java

代码语言:javascript
复制
@Slf4j
public class ThreadSafe {
    private static AtomicInteger value = new AtomicInteger(0);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <= 10000; i++) {
            executorService.execute(() -> {
                addValue();
            });
        }
        executorService.shutdown();
        log.info(value + "");
    }

    private static void addValue() {
        value.incrementAndGet();
    }
}

输出结果:14:00:47.058 [main] INFO com.px.book.ThreadSafe - 10000

胖虎

热 爱 生 活 的 人

终 将 被 生 活 热 爱

我在这里等你哟!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 晏霖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程安全是我们常碰见的事情,例如我们经常说stringBuff是线程安全的,而 stringBuild是线程不安全的,难道我们真的以为线程是有危险性的吗?其实不是的,线程的安不安全指的是如果单线程环境中,这个类或方法或对象能正确地工作,在多线程环境下则不能,或者预期结果与单线程不同或错误,则我们说这个类或方法或对象是线程不安全的。
相关产品与服务
图片处理
图片处理(Image Processing,IP)是由腾讯云数据万象提供的丰富的图片处理服务,广泛应用于腾讯内部各产品。支持对腾讯云对象存储 COS 或第三方源的图片进行处理,提供基础处理能力(图片裁剪、转格式、缩放、打水印等)、图片瘦身能力(Guetzli 压缩、AVIF 转码压缩)、盲水印版权保护能力,同时支持先进的图像 AI 功能(图像增强、图像标签、图像评分、图像修复、商品抠图等),满足多种业务场景下的图片处理需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档