线程并发库和线程池的作用

本文是我们名为Java Concurrency Essentials的学院课程的一部分。

在本课程中,您将深入探讨并发的魔力。 将向您介绍并发和并发代码的基础知识,并学习诸如原子性,同步和线程安全性的概念。 在这里查看 !

目录

1.有关线程的基本知识 2.创建和启动线程 3.睡觉和打断 4.连接线程 5.同步 6.原子访问

1.有关线程的基本知识

并发是程序同时执行多个计算的能力。 这可以通过将计算分布在计算机的可用CPU内核上,甚至在同一网络内的不同计算机上来实现​​。

为了更好地理解并行执行,我们必须区分进程和线程。 进程是操作系统提供的执行环境,它具有自己的一组私有资源(例如,内存,打开的文件等)。 相反, Threads是指生活在一个流程中并与该流程的其他线程共享资源(内存,打开的文件等)的流程。

在不同线程之间共享资源的能力使线程更适合于对性能有重要要求的任务。 尽管可以在同一计算机上甚至在同一网络内的不同计算机上运行的不同进程之间建立进程间通信,但是出于性能原因,通常会选择线程来并行化单台计算机上的计算。

在Java中,进程对应于正在运行的Java虚拟机(JVM),而线程位于同一个JVM中,并且可以由Java应用程序在运行时动态创建和停止。 每个程序至少有一个线程:主线程。 这个主线程是在每个Java应用程序启动期间创建的,它是调用程序的main()方法的那个线程。 从这一点开始,Java应用程序可以创建新的线程并使用它们。

下面的源代码对此进行了演示。 JDK类java.lang.Thread的静态方法currentThread()提供对当前Thread访问:

public class MainThread {public static void main(String[] args) {long id = Thread.currentThread().getId();String name = Thread.currentThread().getName();int priority = Thread.currentThread().getPriority();State state = Thread.currentThread().getState();String threadGroupName = Thread.currentThread().getThreadGroup().getName();System.out.println("id="+id+"; name="+name+"; priority="+priority+"; state="+state+"; threadGroupName="+threadGroupName);}}

从这个简单应用程序的源代码中可以看到,我们直接在main()方法中访问当前Thread ,并打印出有关它的一些信息:

id=1; name=main; priority=5; state=RUNNABLE; threadGroupName=main

输出揭示了有关每个线程的一些有趣信息。 每个线程都有一个标识符,该标识符在JVM中是唯一的。 线程的名称有助于在监视运行中的JVM的外部应用程序(例如调试器或JConsole工具)中找到某些线程。 当执行多个线程时,优先级决定下一个应该执行的任务。

关于线程的真相是,并非所有线程都真正同时执行,而是将每个CPU内核上的执行时间分成小片,并将下一个时间片分配给具有最高优先级的下一个等待线程。 JVM的调度程序根据线程的优先级确定下一个要执行的线程。

在优先级旁边,线程还具有状态,可以是以下状态之一:

  • 新:尚未启动的线程处于此状态。
  • 可运行:在Java虚拟机中执行的线程处于此状态。
  • BLOCKED:处于等待监视器锁定状态的被阻塞线程处于此状态。
  • 等待:无限期等待另一个线程执行特定操作的线程处于此状态。
  • TIMED_WAITING:正在等待另一个线程执行操作的线程最多达到指定的等待时间,该线程处于此状态。
  • 终止:退出的线程处于此状态。

上面示例中的主线程当然处于RUNNABLE状态。 像BLOCKED这样的状态名称已经在这里表明线程管理是高级主题。 如果处理不当,线程可能会相互阻塞,进而导致应用程序挂起。 但是我们稍后会谈到。

最后但并非最threadGroup是,线程的属性threadGroup指示线程是按组管理的。 每个线程都属于一组线程。 JDK类java.lang.ThreadGroup提供了一些方法来处理整个Threads组。 通过这些方法,我们可以例如中断组中的所有线程或设置其最大优先级。

2.创建和启动线程

现在,我们已经仔细研究了线程的属性,是时候创建和启动我们的第一个线程了。 基本上,有两种方法可以用Java创建线程。 第一个是编写一个扩展JDK类java.lang.Thread类:

public class MyThread extends Thread {public MyThread(String name) {super(name);}@Overridepublic void run() {System.out.println("Executing thread "+Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {MyThread myThread = new MyThread("myThread");myThread.start();}}

从上面可以看到,类MyThread扩展了Thread类并覆盖了run()方法。 虚拟机启动线程后,将执行run()方法。 由于虚拟机必须做一些工作才能设置线程的执行环境,因此我们无法直接调用此方法来启动线程。 相反,我们在类MyThread的实例上调用方法start() 。 当此类从其超类继承方法stop() ,该方法背后的代码告诉JVM为线程分配所有必需的资源并启动该线程。 当我们运行上面的代码时,我们看到输出“ Executing thread myThread”。 与我们的介绍示例相反,方法run()的代码不是在“主”线程中执行的,而是在我们自己的名为“ myThread”的线程中执行的。

创建线程的第二种方法是实现接口Runnable

public class MyRunnable implements Runnable {public void run() {System.out.println("Executing thread "+Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {Thread myThread = new Thread(new MyRunnable(), "myRunnable");myThread.start();}}

与子类化方法的主要区别在于,我们创建了java.lang.Thread的实例,并提供了将Runnable接口实现为Thread构造函数的参数的类的实例。 在此实例旁边,我们还传递了Thread的名称,以便从命令行执行程序时看到以下输出:“ Executing thread myRunnable”。

是否应使用子类化或接口方法,取决于您的喜好。 该接口是一种更轻便的方法,因为您要做的就是实现接口。 该类仍然可以是某些其他类的子类。 您还可以将自己的参数传递给构造函数,而Thread子类将您限制为Thread类带来的可用构造函数。

在本系列的后面部分,我们将了解线程池,并了解如何启动相同类型的多个线程。 在这里,我们将再次使用Runnable方法。

3.睡觉和打断

一旦启动了Thread ,它将一直运行直到run()方法结束。 在上面的示例中, run()方法所做的只是打印出当前线程的名称。 因此线程很快完成。

在现实世界的应用程序中,通常必须实现某种后台处理,在这种处理中,线程必须运行,直到例如已经处理了目录结构中的所有文件。 另一个常见的用例是有一个后台线程,如果发生任何事情(例如,已创建文件),则每隔n秒查看一次,并启动某种操作。 在这种情况下,您将必须等待n秒或毫秒。 您可以使用while循环来实现这一点,该循环的主体获取当前的毫秒数并查看下一秒的时间。 尽管这样的实现可行,但是由于您的线程占用了CPU并一次又一次地获取当前时间,因此浪费了CPU处理时间。

对于此类用例,更好的方法是调用java.lang.Thread类的sleep()方法,如以下示例所示:

public void run() {while(true) {doSomethingUseful();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}

sleep()的调用使当前Thread进入睡眠状态,而不消耗任何处理时间。 这意味着当前线程将从活动线程列表中删除自己,并且调度程序不会在第二次(以毫秒为单位)过去之前将其调度用于下一次执行。

请注意,传递给sleep()方法的时间只是调度程序的指示,而不是绝对准确的时间范围。 由于实际的调度,线程可能会提前几纳秒或几毫秒返回。 因此,您不应将此方法用于实时调度。 但是对于大多数使用情况,所达到的精度是足够的。

在上面的代码示例中,您可能已经注意到sleep()可能抛出的InterruptedException 。 中断是线程交互的一个非常基本的功能,可以理解为一个线程发送到另一个线程的简单中断消息。 接收线程可以通过调用Thread.interrupted()方法来显式询问它是否已被中断,或者在将其时间花在诸如sleep()之类的方法上时会隐式中断,该方法在发生中断的情况下会引发异常。

让我们用下面的代码示例仔细看一下中断:

public class InterruptExample implements Runnable {public void run() {try {Thread.sleep(Long.MAX_VALUE);} catch (InterruptedException e) {System.out.println("["+Thread.currentThread().getName()+"] Interrupted by exception!");}while(!Thread.interrupted()) {// do nothing here}System.out.println("["+Thread.currentThread().getName()+"] Interrupted for the second time.");}public static void main(String[] args) throws InterruptedException {Thread myThread = new Thread(new InterruptExample(), "myThread");myThread.start();System.out.println("["+Thread.currentThread().getName()+"] Sleeping in main thread for 5s...");Thread.sleep(5000);System.out.println("["+Thread.currentThread().getName()+"] Interrupting myThread");myThread.interrupt();System.out.println("["+Thread.currentThread().getName()+"] Sleeping in main thread for 5s...");Thread.sleep(5000);System.out.println("["+Thread.currentThread().getName()+"] Interrupting myThread");myThread.interrupt();}}

在main方法中,我们首先启动一个新线程,如果不中断它将会Hibernate很长时间(大约290.000年)。 为了在这段时间过去之前完成程序,通过在main方法中对其实例变量调用interrupt()来中断myThread 。 这会在sleep()调用中引起InterruptedException ,并在控制台上显示为“ Interrupted by exception!”。 记录了异常后,线程会进行一些繁忙的等待,直到设置了线程上的中断标志为止。 通过在线程的实例变量上调用interrupt()再次从主线程进行设置。 总的来说,我们在控制台上看到以下输出:

[main] Sleeping in main thread for 5s...
[main] Interrupting myThread
[main] Sleeping in main thread for 5s...
[myThread] Interrupted by exception!
[main] Interrupting myThread
[myThread] Interrupted for the second time.

此输出中有趣的是第3行和第4行。如果我们遍历代码,我们可能期望字符串“ Interrupted by exception!”。 在主线程再次开始Hibernate之前,将打印出“Hibernate5s…”。 但是从输出中可以看到,调度程序在再次启动myThread之前已经执行了主线程。 因此,在主线程开始Hibernate之后,myThread打印出接收到的异常。

当使用多个线程进行编程时,这是一个基本观察结果,即很难预测线程的日志记录输出,因为很难计算下一个执行的线程。 当您不得不处理更多的线程(如上例所示)的暂停没有被硬编码时,情况变得更加糟糕。 在这些情况下,整个程序会获得某种内部动力,这使得并发编程成为一项艰巨的任务。

4.连接线程

正如在上一节中所看到的,我们可以让我们的线程进入睡眠状态,直到被另一个线程唤醒。 您将不时使用的线程的另一个重要功能是线程等待另一个线程终止的能力。

假设您必须实施某种数字运算,可以将其分为几个并行运行的线程。 启动所谓的工作线程的主线程必须等待,直到其所有子线程都终止。 以下代码显示了如何实现此目的:

public class JoinExample implements Runnable {private Random rand = new Random(System.currentTimeMillis());public void run() {//simulate some CPU expensive taskfor(int i=0; i<100000000; i++) {rand.nextInt();}System.out.println("["+Thread.currentThread().getName()+"] finished.");}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[5];for(int i=0; i<threads.length; i++) {threads[i] = new Thread(new JoinExample(), "joinThread-"+i);threads[i].start();}for(int i=0; i<threads.length; i++) {threads[i].join();}System.out.println("["+Thread.currentThread().getName()+"] All threads have finished.");}}

在我们的main方法中,我们创建了一个由5个Threads的数组,它们全部一个接一个地启动。 启动它们后,我们在主Thread等待其终止。 线程本身通过计算一个随机数来模拟一些数字运算。 完成后,将打印“完成”。 最后,主线程确认其所有子线程的终止:

[joinThread-4] finished.
[joinThread-3] finished.
[joinThread-2] finished.
[joinThread-1] finished.
[joinThread-0] finished.
[main] All threads have finished.

您会发现,“完成”消息的顺序因执行而异。 如果您多次执行该程序,您可能会看到最先完成的线程并不总是相同的。 但是最后一条语句始终是等待其子级的主线程。

5.同步

正如我们在最后一个示例中所看到的,执行所有正在运行的线程的确切顺序取决于线程配置,例如优先级还取决于可用的CPU资源以及调度程序选择下一个线程执行的方式。 尽管调度程序的行为是完全确定的,但是很难预测在给定时间点的哪个时刻哪个线程执行。 这使得对共享资源的访问变得至关重要,因为很难预测哪个线程将是尝试访问它的第一个线程。 通常,对共享资源的访问是排他性的,这意味着在给定时间点只有一个线程应访问此资源,而没有任何其他线程干扰此访问。

一个并发访问独占资源的简单示例是一个静态变量,该变量增加一个以上线程:

public class NotSynchronizedCounter implements Runnable {private static int counter = 0;public void run() {while(counter < 10) {System.out.println("["+Thread.currentThread().getName()+"] before: "+counter);counter++;System.out.println("["+Thread.currentThread().getName()+"] after: "+counter);}}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[5];for(int i=0; i<threads.length; i++) {threads[i] = new Thread(new NotSynchronizedCounter(), "thread-"+i);threads[i].start();}for(int i=0; i<threads.length; i++) {threads[i].join();}}}

当我们仔细查看此简单应用程序的输出时,我们看到类似以下内容的内容:

[thread-2] before: 8
[thread-2] after: 9
[thread-1] before: 0
[thread-1] after: 10
[thread-2] before: 9
[thread-2] after: 11

在这里,线程2将当前值检索为8,然后将其递增,然后是9。这就是我们以前期望的值。 但是以下线程执行的内容可能使我们感到惊讶。 线程1将当前值输出为零,将其递增,然后为10。这怎么办? 当线程1读取变量计数器的值时,该值为0。然后上下文切换执行第二个线程,并且当线程1再次轮到该线程时,其他线程已经将计数器递增到9。结果是10。

此类问题的解决方案是Java中的同步关键字。 使用同步,您可以创建只能由线程访问的语句块,该线程获得了对同步资源的锁定。 让我们从上一个示例中更改run()方法,并为整个类引入一个同步块:

public void run() {while (counter < 10) {synchronized (SynchronizedCounter.class) {System.out.println("[" + Thread.currentThread().getName() + "] before: " + counter);counter++;System.out.println("[" + Thread.currentThread().getName() + "] after: " + counter);}}}

synchronized(SynchronizedCounter.class)语句就像一个屏障,在该屏障中,所有线程都必须停止并要求进入。 只有第一个获得资源锁的线程才被允许通过。 一旦离开同步块,另一个等待线程可以进入,依此类推。

在输出周围有同步块且输出上方计数器递增的情况下,如下例所示:

[thread-1] before: 11
[thread-1] after: 12
[thread-4] before: 12
[thread-4] after: 13

现在,您将只看到计数器变量加1之前和之后的后续输出。
可以以两种不同的方式使用synced关键字。 可以在上述方法中使用它。 在这种情况下,您必须提供一个被当前线程锁定的资源。 必须谨慎选择此资源,因为基于变量的范围,线程屏障的含义完全不同。

如果变量是当前类的成员,则所有线程都将与该类的实例同步,因为每个LocalSync实例都存在变量sync:

public class LocalSync {private Integer sync = 0;public void someMethod() {synchronized (sync) {// synchronized on instance level}}}

除了创建覆盖整个方法主体的块之外,您还可以添加与方法签名同步的关键字。 下面的代码与上面的代码具有相同的作用:

public class MethodSync {private Integer sync = 0;public synchronized void someMethod() {// synchronized on instance level}}

两种方法之间的主要区别在于,第一种方法的粒度更细,因为您可以使同步块比方法主体小。 请记住,同步块一次只能由一个线程执行,因此每个同步块都是潜在的性能问题,因为所有并发运行的线程可能必须等待直到当前线程离开该块。 因此,我们应始终尝试使块尽可能小。

大多数情况下,您将不得不同步对每个JVM仅存在一次的某些资源的访问。 常用的方法是使用类的静态成员变量:

public class StaticSync {private static Integer sync = 0;public void someMethod() {synchronized (sync) {// synchronized on ClassLoader/JVM level}}}

上面的代码同步在同一JVM中通过方法someMethod()运行的所有线程,因为静态变量在同一JVM中仅存在一次。 如您所知,一个类只有在由同一类加载器加载的情况下,才在一个JVM中是唯一的。 如果使用多个类加载器加载类StaticSync ,则静态变量将不止一次存在。 但是在大多数日常应用程序中,您不会有多个类加载器来两次加载同一类,因此您可以假定静态变量仅存在一次,因此同一JVM中的所有线程都必须等待障碍,直到它们获得锁。

6.原子访问

在上一节中,我们看到了当许多并发线程必须执行代码的特定部分但每个时间点仅一个线程应执行该代码时,如何同步对某些复杂资源的访问。 我们还看到,如果不同步对公共资源的访问,则对这些资源的操作会交织并可能导致非法状态。

Java语言提供了一些原子性的基本操作,因此可用于确保并发线程始终看到相同的值:

  • 对引用变量和原始变量(长整型和双精度型除外)的读写操作
  • 对所有声明为易失性的变量的读写操作

为了更详细地了解这一点,我们假设我们有一个HashMap填充了从文件读取的属性,以及一堆使用这些属性的线程。 显然,我们这里需要某种同步,因为读取文件和更新Map花费时间,并且在此期间将执行其他线程。

我们无法轻松地在所有线程之间共享此Map一个实例,并且无法在更新过程中使用此Map 。 这将导致Map状态不一致,该状态由访问线程读取。 有了上一节的知识,我们当然可以在映射的每次访问(读/写)周围使用一个同步块,以确保所有线程仅看到一个状态,而不是部分更新的Map 。 但是,如果必须非常频繁地从Map读取并发线程,则会导致性能问题。

为同步块中的每个线程克隆Map并让每个线程在单独的副本上工作也是一种解决方案。 但是每个线程都必须不时请求更新的副本,并且该副本占用内存,这在每种情况下都不可行。 但是有一个更简单的解决方案。

由于我们知道对引用的写操作是原子的,因此每次读取文件并在一个原子操作中更新线程之间共享的引用时,就可以创建一个新的Map 。 在此实现中,工作线程将永远不会读取不一致的Map因为使用一个原子操作更新了Map

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class AtomicAssignment implements Runnable {private static volatile Map<String, String> configuration = new HashMap<String, String>();public void run() {for (int i = 0; i < 10000; i++) {Map<String, String> currConfig = configuration;String value1 = currConfig.get("key-1");String value2 = currConfig.get("key-2");String value3 = currConfig.get("key-3");if (!(value1.equals(value2) && value2.equals(value3))) {throw new IllegalStateException("Values are not equal.");}try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}public static void readConfig() {Map<String, String> newConfig = new HashMap<String, String>();Date now = new Date();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS");newConfig.put("key-1", sdf.format(now));newConfig.put("key-2", sdf.format(now));newConfig.put("key-3", sdf.format(now));configuration = newConfig;}public static void main(String[] args) throws InterruptedException {readConfig();Thread configThread = new Thread(new Runnable() {public void run() {for (int i = 0; i < 10000; i++) {readConfig();try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}, "configuration-thread");configThread.start();Thread[] threads = new Thread[5];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new AtomicAssignment(), "thread-" + i);threads[i].start();}for (int i = 0; i < threads.length; i++) {threads[i].join();}configThread.join();System.out.println("[" + Thread.currentThread().getName() + "] All threads have finished.");}
}

上面的例子稍微复杂一点,但并不难理解。 共享的MapAtomicAssignment的配置变量。 在main()方法中,我们最初读取配置一次,然后向Map添加三个具有相同值的键(此处为当前时间,包括毫秒)。 然后,我们启动一个“配置线程”,该线程通过将当前时间戳始终添加到地图的三倍来模拟配置的读取。 然后,五个工作线程使用配置变量读取Map并比较三个值。 如果它们不相等,则抛出IllegalStateException。

您可以运行该程序一段时间,并且不会看到任何IllegalStateException 。 这是由于我们通过一次原子操作将新Map分配给共享配置变量的事实:

configuration = newConfig;

我们还可以在一个原子步骤中读取共享变量的值:

Map<String, String> currConfig = configuration;

由于这两个步骤都是原子步骤,因此我们将始终引用所有三个值相等的有效Map实例。 例如,如果更改run()方法的方式是直接使用配置变量,而不是先将其复制到本地变量,则很快就会看到IllegalStateExceptions因为配置变量始终指向“当前”配置。 当配置线程更改了它之后,对Map后续读取访问将已经读取新值,并将它们与旧Map中的值进行比较。

如果直接在配置变量上使用readConfig()方法而不是创建新的Map并通过一次原子操作将其分配给共享变量,则情况也是如此。 但是可能要花一些时间,直到看到第一个IllegalStateException为止。 这对于使用多线程的所有应用程序都是如此。 并发问题乍一看并不总是很明显,但是它们需要在重负载条件下进行一些测试才能出现。

翻译自: https://www.javacodegeeks.com/2015/09/introduction-to-threads-and-concurrency.html

线程并发库和线程池的作用

线程并发库和线程池的作用_线程和并发介绍相关推荐

  1. 线程并发库和线程池的作用_并发–顺序线程和原始线程

    线程并发库和线程池的作用 不久前,我参与了一个项目,该项目的报告流程如下: 用户会要求举报 报告要求将被翻译成较小的部分 基于零件/节的类型的每个零件的报告将由报告生成器生成 组成报告的各个部分将重新 ...

  2. java 线程的作用_线程的作用是什么

    线程(thread)是"进程"中某个单一顺序的控制流.也被称为轻量进程(lightweight processes).计算机科学术语,指运行中的程序的调度单位. 线程,有时被称为轻 ...

  3. java中的锁池和等待池是什么_线程的几个状态及方法,等待池和锁池的理解

    线程状态: 创建状态:创建一个线程实例 Thread thread = new Thread(); 就绪状态:在调用start()方法后,线程获取了除CPU的其他资源,处于就绪状态 执行状态:线程获取 ...

  4. java线程池饱和策略_线程池的饱和策略-调用者执行 | 学步园

    java并发编程实践阅读笔记之线程池的饱和策略 使用java的任务管理框架的线程池执行任务时,线程池的任务等待队列被填满时,饱和策略开始发挥作用.ThreadPollExecutor的饱和策略通过se ...

  5. java线程池有什么作用_java线程池的作用是什么?线程池介绍

    你知道java中线程池的作用是什么吗?那么究竟什么是线程池呢?都有哪些类型呢?让我们对以上的问题来进行详细的了解吧. 一.java线程池作用 第一个我们先来对它的作用进行一下简单的介绍,使用线程池的优 ...

  6. 浅谈线程池(上):线程池的作用及CLR线程池

    线程池是一个重要的概念.不过我发现,关于这个话题的讨论似乎还缺少了点什么.作为资料的补充,以及今后文章所需要的引用,我在这里再完整而又简单地谈一下有关线程池,还有.NET中各种线程池的基础.更详细的内 ...

  7. CLR线程池的作用与原理浅析

    线程池是一个重要的概念.不过我发现,关于这个话题的讨论似乎还缺少了点什么.作为资料的补充,以及今后文章所需要的引用,我在这里再完整而又简单地谈一下有关线程池,还有.NET中各种线程池的基础.更详细的内 ...

  8. ThreadPool 线程池的作用

    相关概念: 线程池可以看做容纳线程的容器: 一个应用程序最多只能有一个线程池: ThreadPool静态类通过QueueUserWorkItem()方法将工作函数排入线程池: 每排入一个工作函数,就相 ...

  9. 线程池和并行处理 、线程池的作用

    目录 线程池的作用: 为什么要用线程池: 线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的 使用线程池对线程进行统一分配.调优和监控,有以下好处: 1.降低资源消耗 ...

最新文章

  1. Python 基础教程(第2版) 中文版+英文原版下载
  2. 天通苑海鲜餐馆数据调查,很难想象消费越贵越受欢迎
  3. Java:Java编程实现导出二维码
  4. C++ Primer 第10章 pair类型
  5. html5+css3实战之-幽灵按钮
  6. python h5s文件 压缩_如何用python解压zip压缩文件
  7. 论文笔记《BERT》
  8. 前端学习笔记--百度2010校园招聘题目
  9. ORA29902执行ODCIIndexStart()例行程序中出错 SQL空间查询ST_Geometry配置
  10. 编译nginx源码包
  11. .styl格式的CSS样式文件是什么文件
  12. css设置字体颜色怎么设,css里面怎么设置字体颜色?
  13. linux嵌入式计算器绪论,毕业设计—嵌入式计算器
  14. windows命令行工具(转)
  15. 用python计算100以内所有奇数的和_用python脚本来计算100以内奇数或者偶数之和
  16. 智能养老监测系统设计
  17. H5网页头部的声明应该是用 lang=zh 还是 lang=zh-cn?
  18. .net Core 2.1 在 CentOS7下,运行含图片处理时发生异常,报The type initializer for ‘Gdip‘ threw an exception.异常
  19. 教你三相永磁同步电机隆博戈观测matlab搭建
  20. python 入门学习教程笔记-- BMR 计算器

热门文章

  1. jzoj3920-噪音【贪心,dp】
  2. 欢乐纪中A组赛【2019.8.20】
  3. P3435-[POI2006]OKR-Periods of Words【KMP】
  4. ssl1072-砝码称重【dp练习】
  5. 2021牛客暑期多校训练营4 E-Tree Xor(异或+思维+区间交 or Trie树)
  6. 【数位DP】好数(jzoj 1521)
  7. Wannafly挑战赛19
  8. 2017西安交大ACM小学期 敏感词汇[AC自动机]
  9. L3-002 堆栈 树状数组+二分答案
  10. 7、mybatis中的sql映射文件详解(2)