Java基础知识(十) 多线程
Java基础知识
- 1. 什么是多线程?它与进程有什么区别?为什么要使用多线程
- 2. 同步和异步有什么区别
- 3. 如何实现Java多线程
- 4. run()方法与start()方法有什么区别
- 5. 多线程同步的实现方法有哪些
- 1. synchronized关键字
- 2. wait()方法与notify()方法
- 3. Lock
- 6. sleep()方法与wait()方法有什么区别
- 7. 终止线程的方法有哪些
- 8. synchronized与Lock有什么异同
- 9. 什么是守护线程
- 10. join()方法的作用是什么
上一篇:Java基础知识(九) 容器
1. 什么是多线程?它与进程有什么区别?为什么要使用多线程
线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在Java语言中,线程有4种状态:运行、就绪、挂起和结束。
进程是指一段正在执行的程序。而线程有时也不可被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间。
在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行,那么为什么要使用多线程呢?其实,多线程的使用为程序研发带来了巨大的便利,具体而言,有以下几个方面的内容:
- 使用多线程可以减少程序的响应时间。在单线程(单线程指的是程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序)的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
- 与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
- 多 CPU 或多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多 CPU 计算机上使用多线程能提高 CPU 的利用率。
- 使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分
成多个线程来执行。
2. 同步和异步有什么区别
在多线程的环境中,经常会碰到数据的共享问题,即当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则,程序的运行结果将会是不可预料的,在这种情况下就必须对数据进行同步,例如多个线程同时对同—数据进行写操作,即当线程 A 需要使用某个资源时,如果这个资源正在被线程 B 使用,同步机制就会让线程 A 一直等待下去,直到线程 B 结束对该资源的使用后,线程 A 才能使用这个资源,由此可见,同步机制能够保证资源的安全。要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证在同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的线程退出临界区时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。
Java 语言在同步机制中提供了语言级的支持,可以通过使用 aynchronized 关键字来实现同步、但该方法并非“万金油”,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。实现同步的方式有两种:一种是利用同步代码块来实现同步;另一种是利用同步方法来实现同步。异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等到输人输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率。
举个生活中的简单例子就可以区分同步与异步了。同步就是你喊我去吃饭,如果听到了,我就和你去吃饭;如果我没有听到,你就不停地喊,直到我告诉你听到了,我们才一起去吃饭。异步就是你喊我,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。
3. 如何实现Java多线程
Java中实现多线程一般有三种方式:
- 继承Thread类,重写run()方法
/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 10:15*/
public class TestThread1 {public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start();}
}
class MyThread extends Thread{@Overridepublic void run(){System.out.println(" 线程体0 " + this.getName());}
}
运行截图:
- 实现Runnable接口,并实现该接口的run()方法
/*** 描述:多线程测试** @author Ye* @version 1.0* @date 2021/8/13 10:30*/
public class TestThread2 {public static void main(String[] args) {MyThread1 thread = new MyThread1();Thread thread1 = new Thread(thread);thread1.run();}
}class MyThread1 implements Runnable{@Overridepublic void run() {System.out.println("线程体: " + this.getClass().getName());}
}
运行截图:
- 实现Callable接口,重写call()方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 10:44*/
public class TestThread3 {public static class CallableTest implements Callable<String>{@Overridepublic String call() throws Exception {return "线程回传信息!";}}public static void main(String[] args) {ExecutorService threadPool = Executors.newSingleThreadExecutor();// 启动线程Future<String> future = threadPool.submit(new CallableTest());try{System.out.println("等待线程完成");// 等待线程结束,并获取返回结果System.out.println(future.get());}catch (Exception e){e.printStackTrace();}}
}
运行截图:
4. run()方法与start()方法有什么区别
通常,系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态。而非运行状态,也就意味着这个线程可以被JVM 来调度执行。在调度过程中,JVM 通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。如果直接调用线程类的run()方法,这会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程,也就是说stant 方法()能够异步地调用 nun()方法,但是直接调用 run()方法却是同步的,因此也就无法达到多线程的目的。
由此可知,只有通过调用线程类的start()方法才能真正达到多线程的目的。下面通过一个例子来说run()方法与 start()方法的区别。
/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 11:14*/
public class TestThread4 {/*** start方法* */public static void test1(){System.out.println("test1: 开始");Thread thread = new ThreadDemo();thread.start();System.out.println("test1: 结束");}/*** run方法* */public static void test2(){System.out.println("test2: 开始");Thread thread = new ThreadDemo();thread.run();System.out.println("test2: 结束");}public static void main(String[] args) {test1();try{Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}System.out.println();test2();}
}class ThreadDemo extends Thread{@Overridepublic void run(){System.out.println("线程例子: 开始");try{Thread.sleep(1000);}catch (Exception e){e.printStackTrace();}System.out.println("线程例子: 结束");}}
运行截图:
5. 多线程同步的实现方法有哪些
Java主要提供了3种实现同步机制的方 法:
1. synchronized关键字
在Java中,每个对象都有一个对象锁与之关联,该锁表明对象在任何时候只允许被一个线程所拥有,当调用synchronized代码时,需要获取锁,执行代码,执行结束后释放锁。
- synchronized 方法。在方法的声明前加入synchronized关键字,示例如下:
public synchronized void mutiThreadAccess();
- synchronized块。synchronized块既可以把任意的代码段声明为synchronized,也可以指定上锁的对象,有非常高的灵活性。其用法如下:
synchronized(syncObject) {// 访问syncObject的代码}
2. wait()方法与notify()方法
当使用 synchronized 来修饰某个共享资源时,如果线程 A1 在执行 synchronized 代码,另外一个线程 A2 也要同时执行同一对象的同— synchronized 代码时,线程 A2 将要等到线程 A1 执行完成后,才能继续执行。在这种情况下可以使用 wait()方法和 notify()方法。在 synchronized 代码被执行期间,线程可以调用对象的 wait()方法,释放对象锁,进入等待状态,并且可以调用 notify()方法或 noifyAll()方法通知正在等待的其他线程。notify()方法仅唤醒一个线程(等待队列中的第一个线程)并允许它去获得锁,noifyAlI()方法唤醒所有等待这个对象的线程并允许它们去获得锁(并不是让所有唤醒线程都获取到锁,而是让它们去竞争)。
3. Lock
JDK 5 新增加了 Lock 接口以及它的一个实现类 ReentranLock (重人锁),Lock 也可以用来实现多线程的同步,具体而言,它提供了如下一些方法来实现多线程的同步:
- look()。以阻塞的方式获取锁,也就是说,如果获取到了锁,立即返回;如果别的线程持有锁,当前线程等待,直到获取锁后返回。
- tryLock()。以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回 true,否则,立即返回 false。
- tryLock(long timeout, TimeUnit unit)。如果获取了锁,立即返回 true,否则会等待参数给定的时间单元,在等待的过程中,如果获取了锁,就返回 true,如果等待超时,返回 false。
- lockInterruptibly()。如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获得锁,或者当前线程被别的线程中断(会收到 IntermuptedException 异常)。它与lock()方法最大的区别在于如果 lock()方法获取不到锁,会一直处于阻塞状态,且会忽略 interrupt()方法
6. sleep()方法与wait()方法有什么区别
sleep()是使线程暂停执行一段时间的方法。wait()也是一种使线程暂停执行的方法,例如,当线程交互时,如果线程对一个同步对象x 发出一个 wait()调用请求,那么该线程会暂停执行,被调对象进入等待状态,直到被唤醒或等待时间超时。具体而言,sleep()方法与 wait()方法的区别主要表现在以下几个方面:
- 原理不同。sleep()方法是 Thread 类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动“苏醒”,例如,当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法前面加上一个 sleep()方法,以便让自己每隔 1s 执行一次,该过程如同闹钟一样。而 wait()方法是 Object 类的方法,用于线程间的通信这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用 notify()方法(或 notifyAll 方法)时才“醒”来,不过开发人员也可以给它指定一个时间,自动“醒”来。与 wait()方法配套的方法还有 notify()方法和 notifyAll()方法。
- 对锁的处理机制不同。由于 sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用 sleep()方法并不会释放锁。而 wait()方法则不同,当调用 wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
- 使用区域不同。由于 wait()方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而 sleep()方法则可以放在任何地方使用。sleep()方法必须捕获异常,而 wait()、notity()以及 notifyall()不需要捕获异常。在 sleep的过程中,有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常。由于 sleep 不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用sleep()方法,而推荐使用 wait()方法。
7. 终止线程的方法有哪些
在Java 语言中,可以使用 stop()方法与 suspend()方法来终止线程的执行。当用Thread.stop()来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他线程将会“看”到这个不一致的状态,这可能会导致程序执行的不确定性,并且这种问题很难被定位。调用 suspend()方法容易发生死锁(死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进)。由于调用 suspend()方法不会释放锁,这就会导致一个问题:如果用一个 suspend 挂起一个有锁的线程,那么在锁恢复之前将不会被释放。如果调用 suspend()方法,线程将试图取得相同的锁,程序就会发生死锁,例如,线程 A 已经获取到了互斥资源 M 的锁,此时线程 A 通过 suspend()方法挂起线程 A 的执行,接着线程 B也去访问互斥资源 M,这时候就造成了死锁。鉴于以上两种方法的不安全性,Java 语言已经不建议使用以上两种方法来终止线程了。
那么,如何才能终止线程呢?一般建议采用的方法是让线程自行结束进入 Dead 状态。一个线程进入 Dead 状态,即执行完 run()方法,也就是说,如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束 run()方法的执行。在实现时,可以通过设置一个 flag 标志来控制循环是否执行,通过这种方法来让线程离开 run()方法从而终止线程。
/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 16:41*/
public class TestThread6 implements Runnable{private volatile Boolean flag;public void stop(){flag = false;}@Overridepublic void run() {while (flag){; //do something}}
}
当线程发生I/O阻塞,可以使用interrupt()方法安全退出
/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 16:16*/
public class TestThread5 {public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println(" 线程准备休眠 ");try{// 用休眠来模拟线程被阻塞Thread.sleep(5000);System.out.println(" 线程完成 ");}catch (Exception e){System.out.println(" 线程中断 ");}}});thread.start();thread.interrupt();}
}
运行截图:
8. synchronized与Lock有什么异同
Java 语言提供了两种锁机制来实现对某个共享资源的同步:synchronized 和 Lock。其中synchronized 使用 Object 对象本身的 notify、wait、noityAIl 调度机制,而 Lock 可以使用 Condition 进行线程之间的调度,完成 synchronized 实现的所有功能。
具体而言,二者的主要区别主要表现在以下几个方面的内容:
- 用法不一样。在需要同步的对象中加人 synchronized 控制,synchronized 既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而 Lock 需要显式地指定起始位置和终止位置。synchronized 是托管给 JVM 执行的,而 Look 的锁定是通过代码实现的,它有比synchronized 更精确的线程语义。
- 性能不一样。在 JDK 5 中增加了一个 Lack 接口的实现类 ReentrantLock。它不仅拥有synchronized 相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。它们的性能在不同的情况下会有所不同:在资源竞争不是很激烈的情况下,synchronized 的性能要优于Reentrantlock,但是在资源竞争很激烈的情况下,synchronized 的性能会下降得非常快,而ReentrantLock 的性能基本保持不变。
- 锁机制不一样。synchronized 获得锁和释放的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁。而 Lock 则需要开发人员手动去释放,并且必须在 finally 块中释放,否则会引起死锁问题的发生。此外,Lock 还提供了更强大的功能,它的 tryLock()方法可以采用非阻塞的方式去获取锁。
虽然 synchronized 与 Lock都可以用来实现多线程的同步,但是,最好不要同时使用这两种同步机制,因为 ReetrantLock 与 synchronized 所使用的机制不同,所以它们的运行是独立的,相当于两种类型的锁,在使用时互不影响。
示例如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 描述:测试锁机制** @author Ye* @version 1.0* @date 2021/8/13 17:15*/
public class TestThread7 {public static void main(String[] args) {final SyncTest syncTest = new SyncTest();Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0;i<5;i++){syncTest.addValueSync();try{Thread.sleep(20);}catch (Exception e){e.printStackTrace();}}}});Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0;i<5;i++){syncTest.addValueLock();try{Thread.sleep(20);}catch (Exception e){e.printStackTrace();}}}});thread.start();thread1.start();}
}class SyncTest{private int value = 0;Lock lock = new ReentrantLock();public synchronized void addValueSync(){this.value++;System.out.println(Thread.currentThread().getName() + ": " + value);}public void addValueLock(){try{lock.lock();value ++;System.out.println(Thread.currentThread().getName() + ": " + value);}finally {lock.unlock();}}
}
运行截图:
9. 什么是守护线程
Java提供了两种线程:守护线程与用户线程。守护线程又被称为“服务进程”“精灵线程”或“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个 JVM 中所有非守护线程的“保姆”。用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM 也就退出了。因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。
在 Java 语言中,守护线程一般具有较低的优先级,它并非只由 JVM 内部提供,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用start()方法启动线程之前调用对象的 setDaemon(true)方法,若将以上参数设置为 false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此,示例如下:
/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 19:19*/
public class TestThread8 {public static void main(String[] args) {System.out.println("******** 开始 **********");Thread thread = new ThreadDemo1();thread.setDaemon(true);thread.start();System.out.println("******** 结束 **********");}
}class ThreadDemo1 extends Thread{@Overridepublic void run(){System.out.println(Thread.currentThread().getName() + ": 开始");try{Thread.sleep(1000);}catch (Exception e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ": 结束");}
}
运行截图:
10. join()方法的作用是什么
在 Java 语言中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join 方法后面的代码。简单点说,就是将两个线程合并,用于实现同步功能。具体而言,可以通过线程 A 的 join()方法来等待线程 A 的结束,或者使用线程 A 的 join(2000)方法来等待线程 A 的结束,但最多只等待2s,示例如下:
/*** 描述:测试多线程** @author Ye* @version 1.0* @date 2021/8/13 19:40*/
public class TestThread9 {public static void main(String[] args) {Thread thread = new Thread(new ThreadImp());thread.start();try{thread.join(1000);if (thread.isAlive()){System.out.println(" 线程没有结束 ");}else {System.out.println(" 线程完成 ");}System.out.println(" 加入完成 ");}catch (Exception e){e.printStackTrace();}}
}class ThreadImp implements Runnable{@Overridepublic void run() {try{System.out.println(" 开始线程 ");Thread.sleep(5000);System.out.println(" 结束线程 ");}catch (Exception e){e.printStackTrace();}}
}
运行截图:
参考:《Java程序员面试笔试宝典》 何昊、薛鹏、叶向阳 编著
Java基础知识(十) 多线程相关推荐
- 第二十九节:Java基础知识-类,多态,Object,数组和字符串
前言 Java基础知识-类,多态,Object,数组和字符串,回顾,继承,类的多态性,多态,向上转型和向下转型,Object,数组,多维数组,字符串,字符串比较. 回顾 类的定义格式: [类的修饰符] ...
- java基础知识总结(三)
类 1. 内部类 1. 内部类分类 Java内部类详解 - 简书 (jianshu.com) java提高篇(十)-----详解匿名内部类 - chenssy - 博客园 (cnblogs.com) ...
- java语言基础总结ppt_我的java基础知识总结ppt
昨天加上今天,我把java基础知识总结的ppt做好了,其中包括: 1基础阶段所有项目展示 2.阶段自我总结,自己的提升和不足 3.后期学习规划 我在ppt里面把几个有代表性项目展示了出来,并且描述了我 ...
- 【转】Java基础知识整理
本博文内容参考相关博客以及<Java编程思想>整理而成,如有侵权,请联系博主. 转载请注明出处:http://www.cnblogs.com/BYRans/ PDF版下载链接:<Ja ...
- java基础知识之初识java
java基础知识之初识java JAVA基础课后总结 一 1.计算机程序 定义:程序(Program)是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合. 2.指令 定义:指令就是指示机 ...
- 《Java和Android开发实战详解》——1.2节Java基础知识
本节书摘来自异步社区<Java和Android开发实战详解>一书中的第1章,第1.2节Java基础知识,作者 陈会安,更多章节内容可以访问云栖社区"异步社区"公众号查看 ...
- java 基础知识总结
Java基础知识总结 写代码: 1,明确需求.我要做什么? 2,分析思路.我要怎么做?1,2,3. 3,确定步骤.每一个思路部分用到哪些语句,方法,和对象. 4,代码实现.用具体的java语言代码把思 ...
- Java基础知识回顾之七 ----- 总结篇
前言 在之前Java基础知识回顾中,我们回顾了基础数据类型.修饰符和String.三大特性.集合.多线程和IO.本篇文章则对之前学过的知识进行总结.除了简单的复习之外,还会增加一些相应的理解. 基础数 ...
- Java基础知识复习(一)
Java基础知识复习(一) 目录 Java简介 命名规则 八种基本的数据类型 字面量 类型转换 变量的形态 逻辑运算符 位运算 移位运算 习题知识点 目录 Java简介 Java是由Sun公司在199 ...
最新文章
- 2.2.1 mini-batch
- 蓝桥杯java第六届决赛第一题--分机号
- C# 文件搬运(从一个文件夹Copy至另一个文件夹)
- php interbase,PHP: Firebird/InterBase - Manual
- python3怎么安装gmpy2_python2/3 模块gmpy2在linux下安装
- linux+cp+-rdf,简单构建基于RDF和SPARQL的KBQA(知识图谱问答系统)
- Spring学习笔记-构造和Set方法注入Bean及集合和null值的注入
- Android 获取静态上下文(Application)
- Mysql权限控制 - 允许用户远程连接
- 最近为A公司提炼的经营理念之合作理念
- 数据挖掘十大经典算法(转载)
- 甘肃电大计算机考试题2007,甘肃电大2021年春季《C++语言程序设计(专)》形成性考核二满分...
- c语言商品管理系统文件,c语言商品管理系统(文件应用).doc
- 【LeetCode】【字符串】题号:*12. 整数转罗马数字
- 拓端tecdat|R语言动态图可视化:如何、创建具有精美动画的图
- 垂直的SeekBar:VerticalSeekBar
- 贪心科技机器学习训练营(九)
- 操作系统系列(三)——编译和链接
- 采用最终一致性解决微服务一致性问题
- 机器学习实战(Machine Learning in Action)学习笔记————05.Logistic回归
热门文章
- laravel框架的whereIn条件或者where条件里面的in条件怎么写
- 《Blood Vessel Segmentation in Fundus Images Based on Improved Loss Function》
- Animated之实例篇
- Java中,如何把ascii码转换成字符?
- 华为p10测试软件,华为p10内存测试软件
- flyingsaucer进行html文件转图片和pdf
- Burg法求解AR(p)模型参数(三)Levinson递推公式
- 精品软件 推荐 电子书转换器 EPUB to PDF Converter
- 支付回答——如何理解借记和贷记
- 遗传算法最通俗的讲解案例