线程系列目录

  1. Thread线程从零认识到深层理解——初识
  2. Thread线程从零认识到深层理解——六大状态
  3. Thread线程从零认识到深层理解——wait()与notify()
  4. Thread线程从零认识到深层理解——线程安全
  5. 线程池从零认识到深层理解——初识
  6. 线程池从零认识到深层理解——进阶

线程初识

  • 前言
  • 一、为什么要用多线程?
  • 二、线程的常见属性
  • 三、线程的常见方法
  • 四、如何使用线程
  • 总结
  • 面试扩展

博客创建时间:2020.09.28
博客更新时间:2021.05.25

注意:本系列博文源码分析取自于Android SDK=30,与网络上的一些源码可能不一样,可能他们分析的源码更旧,无需大惊小怪。


前言

在讲解线程知识的时候必不可少的需要线科普下什么是线程,线程与进程之间的关系。

1. 进程
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。进程是表示资源分配的基本单位,又是调度运行的基本单位。

2. 线程
线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位。
线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

线程是一条可以执行的路径。多线程就是同时有多条执行路径在同时(并行)执行。

进程与线程的关系

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。
  3. 处理CPU片分给线程,即真正在处理机上运行的是线程。
  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
  5. 我们常说的线程是一种轻量级进程(LWP),每个LWP都有一个内核级线程支持。

线程的调度问题,这里有一个很好的举例来说明:
如果把上课的过程比作进程,把老师比作CPU,那么可以把每个学生比作每个线程,所有学生共享这个教室(也就是所有线程共享进程的资源),上课时学生A向老师提出问题,老师对A进行解答。此时可能会有学生B对老师的解答不懂会提出B的疑问(注意:此时可能老师还没有对A同学的问题解答完毕),此时老师又向学生B解惑,解释完之后又继续回答学生A的问题,同一时刻老师只能向一个学生回答问题(即:当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换)


一、为什么要用多线程?

线程是最小的执行单位,当程序中有多个线程是,最明显的利好就是能提高程序的执行效率不用彼此等待。除此还有其他诸多优点:

  1. 更高的运行效率,——并行;
  2. 多线程是模块化的编程模型;
  3. 与进程相比,线程的创建和切换开销更小,通信更方便;
  4. 简化程序的结构,便于理解和维护;更高的资源利用率。
  5. 每个线程之间独立互不影响,即使一个线程阻塞也不会影响其他线程
    public class Thread implements Runnable {...private Runnable target ;/*** 线程优先级*/private int priority;/***如果线程中target不为null,则执行target.run()。* 否则run()无实际意义,需要要求Thread的子类必须重写run()方法*/@Overridepublic void run() {if (target != null) {target.run();}}...}

由代码可知,Thread本身实现了Runnable,但是它自身有一个Runnable成员Runnable ,在run方法中默认调用的是target.run()。


二、线程的常见属性

priority优先级

优先级的使用意义
当在某个线程中运行创建一个新的 Thread对象时, 新的线程的优先级默认等于创建线程的优先级,且是否是守护进程线程也同创建线程一致,如需设置优先需在创建时给定设置,后面不能再修改。

如果UI线程创建出了十几个工作线程,为了不让工作线程和主线程抢占CPU资源,需要工作线程的优先级进行降级,让CPU能够识别主次,提高主线程能够得到的系统资源。

优先级设定
每个线程都有优先级属性,具有较高优先级的线程优先于优先级较低的线程执行,每个线程可能会被标记为守护进程。

线程的优先级用数字来表示,范围从1~10,主线程的默认优先级为5,在线程构造初始化后,可以通过set方法设置具体值。
Thread.MIN_PRIORITY=1;Thread.MAX_PRIORITY=10;Thread.NORM_PRIORITY=5 )

三、线程的常见方法

构造函数
Thread的构造函数有很多种,我这里只列举几种常见的构造方法。

 public Thread() {init(null, null, "Thread-" + nextThreadNum(), 0);}public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);}public Thread(ThreadGroup group, Runnable target) {init(group, target, "Thread-" + nextThreadNum(), 0);}public Thread(String name) {init(null, null, name, 0);}public Thread(ThreadGroup group, Runnable target, String name,long stackSize) {init(group, target, name, stackSize);}

通过源码分析,最终都走向了init()这个方法,我们来进行源码分析。

 /*** Initializes a Thread.* 初始化一个线程** @param g         the Thread group  线程组* @param target    the object whose run() method gets called  调用run()方法的对象* @param name      the name of the new Thread    新线程的名称* @param stackSize the desired stack size for the new thread, or*                  zero to indicate that this parameter is to be ignored.*                  新线程的所需堆栈大小,或0表示该参数将被忽略* @param acc       the AccessControlContext to inherit, or*                  AccessController.getContext() if null*                  要继承的AccessControlContext;如果为null,则为* AccessController.getContext()*/private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;// 当前正在执行的线程,即创建该线程正在运行的线程Thread parent = currentThread();//如果未设置ThreadGroup,则默认与创建者线程在同一group中if (g == null) {g = parent.getThreadGroup();}//增加线程组中未启动线程的数量,如果ThreadGroup未destroyed则nUnstartedThreads++,即使该线程最终未启动也会nUnstartedThreads++g.addUnstarted();this.group = g;//如果创建者程序时守护程序线程,则它创建的线程也是守护程序线程,除非手动修改setDaemon(boolean on)this.daemon = parent.isDaemon();//默认新线程与父线程的优先级是一样的,如果想修改优先级需要额外调用setPriority(int newPriority)方法进行重新设定this.priority = parent.getPriority();this.target = target;init2(parent);//设置线程请求的堆栈大小this.stackSize = stackSize;//为下个Thread Id做准备 ++ threadSeqNumbertid = nextThreadID();}

根据分析init()方法可以获悉如下:

  1. 这是一个线程的初始化方法,创建Thread最终都会执行该方法
  2. 线程必须有线程名,如果创建者未定义线程名,则系统默认会取名"Thread-" + nextThreadNum(),如"Thread-643636"。后面的数字由线程的一个全局静态变量threadInitNumber决定,它会自增。
  3. 增加ThreadGroup 中未启动线程的数量,如果ThreadGroup未destroyed则nUnstartedThreads++,即使该线程最终未启动也会nUnstartedThreads++
  4. 在初始化方法中daemon、priority 等属性默认与父线程相同,不过Thread中提供了相关set方法,可以在线程创建完毕后自定义修改

run()方法
线程实现了Runnable接口,需要重写Runnable#run()方法。

在我们继承Thread重写run()方法时,常常看到super.run会不会感到奇怪,这个有啥用。其实这里调用的是Runnable对象target#run()。target是private的且只能通过Thread的构造函数进行赋值,所以如果在Thread创建的构造函数中未传target参数,其实super.run()无卵用。

private class TestThread : Thread() {override fun run() {super.run()println("TestThread =$name")}
}

我通过测试发现如果Thread的子类既重写了自身的run()方法,有实现了Runnable重写了Runnable的run()方法,则两个方法中的代码都会执行。

    @JvmStaticfun main(args: Array<String>) {object : Thread(Runnable {  println("执行Runnable中的run方法") }) {override fun run() {super.run()println("执行Thread中的run方法")}}.start()}运行结果:执行Runnable中的run方法执行Thread中的run方法

start()方法

    /*** Causes this thread to begin execution; the Java Virtual Machine* calls the <code>run</code> method of this thread.* <p>使该线程开始执行; Java虚拟机调用此线程的`run`方法。* It is never legal to start a thread more than once.* In particular, a thread may not be restarted once it has completed* execution.** @exception  IllegalThreadStateException  if the thread was already*               started.* @see        #run()* @see        #stop()*/public synchronized void start() {/*** This method is not invoked for the main method thread or "system"* group threads created/set up by the VM.* VM创建设置的main方法线程或系统group线程不会调用此方法* Any new functionality added to this method in the future may have to also be added to the VM.* 新的特性或许未来会在此增加* A zero status value corresponds to state "NEW".*/if (started)throw new IllegalThreadStateException();/** 通知组该线程即将开始,以便可以将其添加到组的线程列表中,并且该组的未启动计数可以减少nUnstartedThreads--*/group.add(this);started = false;try {// 使用Android特定的nativeCreate()方法创建/启动线程nativeCreate(this, stackSize, daemon);started = true;} finally {try {if (!started) {// start失败group.threadStartFailed(this);}} catch (Throwable ignore) {/* do nothing. If start0 threw a Throwable thenit will be passed up the call stack */}}}

通过start()方法的注释和代码分析,知道:

  1. 当线程的实例调用start()方法后,JVM会调用该线程的run()方法
  2. 一旦线程已经启动,started=true,如果再次调用start()方法将抛IllegalThreadStateException
  3. VM创建设置的main方法线程或系统group线程不会调用此方法
  4. 线程一旦执行完成,则无法再次调用start()启动。
  5. Thread的启动实际是由native方法 nativeCreate启动的,它是Android特有的显示提供的方法,其虚拟机中是调用的是start0()。
  6. 由于内存不够,线程数超过限制等原因,nativeCreate()调用可能失败,此时需要将该Thread从线程组列表中移出,nUnstartedThreads++

stop()方法

   @Deprecatedpublic final void stop() {/** The VM can handle all thread states stop0(new ThreadDeath());*/throw new UnsupportedOperationException();}

stop方法很简单,此方法最初旨在强制线程停止*并抛出{@code ThreadDeath}作为异常,本质上是不安全的。所以它已被@Deprecated标记,实际开发中请不要在使用该方法,如确实需要停止线程请使用中断的方式。线程中断在后面将会详细描述。

在旧的版本中stop()方法是这样的:

    @Deprecatedpublic final void stop() {stop(new ThreadDeath());}

旧版SDK中通过stop(new ThreadDeath())来停止线程,它实质调用了native方法中的stop0()方法,它的方法实质也是抛出了一个UnsupportedOperationException()异常。在调用stop()方法时抛出UnsupportedOperationException异常,意在强制停止线程是不安全和被认可的。


sleep

        /*** Causes the currently executing thread to sleep (temporarily cease* execution) for the specified number of milliseconds,* subject to the precision and accuracy of system timers and schedulers.* 使当前正在执行的线程在指定的毫秒数内进入睡眠状态(暂时停止执行),这取决于系统计时器和调度程序的精度和准确性* The thread does not lose ownership of any monitors.* 该线程不会失去任何锁的所有权。** @param millis 睡眠时间(以毫秒为单位)* @throws IllegalArgumentException 如果{@code millis}的值为负* @throws InterruptedException     if any thread has interrupted the current thread. The*                                  <i>interrupted status</i> of the current thread is*                                  cleared when this exception is thrown.*                                  如果有任何线程中断了当前线程。引发此异常时,将清除当前线程的中断状态。*/public static void sleep(long millis) throws InterruptedException {sleep(millis, 0);}/*** 在安卓中使用共享的native来实现sleep()方法*/@FastNativeprivate static native void sleep(Object lock, long millis, int nanos)throws InterruptedException;/*** Causes the currently executing thread to sleep (temporarily cease* execution) for the specified number of milliseconds plus the specified* number of nanoseconds, subject to the precision and accuracy of system* timers and schedulers.* 根据系统计时器和调度程序的精度和准确性,使当前正在执行的线程进入睡眠状态(暂时停止*执行)* 指定的毫秒数加上指定的*纳秒数。** @param millis 睡眠时间(以毫秒为单位)* @param nanos  {@code 0-999999}额外的纳秒睡眠时间* @throws IllegalArgumentException 如果{@code millis}的值为负,或者* {@code nanos}的值不*                                  在{@code 0-999999}范围内* @throws InterruptedException     如果有任何线程中断了当前线程。引发此异常时,将清除当前线程的中断状态。*/public static void sleep(long millis, int nanos)throws InterruptedException {if (millis < 0) {throw new IllegalArgumentException("millis < 0: " + millis);}if (nanos < 0) {throw new IllegalArgumentException("nanos < 0: " + nanos);}if (nanos > 999999) {throw new IllegalArgumentException("nanos > 999999: " + nanos);}if (millis == 0 && nanos == 0) {if (Thread.interrupted()) {throw new InterruptedException();}return;}final int nanosPerMilli = 1000000;long start = System.nanoTime();long duration = (millis * nanosPerMilli) + nanos;Object lock = currentThread().lock;// The native sleep(...) method actually performs a special type of wait,// 本机sleep(...)方法实际上执行一种特殊的等待// which may return early, so loop until sleep duration passes.// 它可能会提早返回,所以循环直到睡眠持续时间过去synchronized (lock) {while (true) {sleep(lock, millis, nanos);long now = System.nanoTime();long elapsed = now - start;if (elapsed >= duration) {break;}duration -= elapsed;start = now;millis = duration / nanosPerMilli;nanos = (int) (duration % nanosPerMilli);}}}}
  1. 使当前正在执行的线程在指定的毫秒数内进入睡眠状态(暂时停止执行),这取决于系统计时器和调度程序的精度和准确性。即sleep(1000),事实上算上代码执行时间可能它运行了1000.2ms。
  2. 执行sleep()方法后,线程处于TIMED_WAITING状态,但是该线程不会失去任何锁的所有权。当超过指定时间线程自动苏醒进入就绪状态
  3. Object.wait()方法调用后会释放对象锁,而线程的Thread.sleep()方法不会让出对象锁,只会让出CPU时间片。两者都会让线程进入WAITING状态

yield

   /*** 向调度程序提示当前线程愿意产生当前使用的处理器.调度程序可以随意忽略此提示** Yield是一种启发式尝试,旨在提高线程之间的相对进程否则会过度利用CPU** 应将其使用与详细的性能分析和基准测试结合使用,以确保它确实具有所需的效果。* 很少适合使用此方法。 *对于调试或测试目的可能是有用的,在某些情况下它可能有助于重现*由于竞争条件而引起的错误。* 在设计并发控制结构(例如* {@link java.util.concurrent.locks}包中的结构)时,它可能也很有用。*/public static native void yield();

yield()方法是一种native方法,一般很少有机会使用该方法。它是一种让步方法,调用该方法可使当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。

实际使用中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。


join

        /*** Waits at most {@code millis} milliseconds for this thread to* die. A timeout of {@code 0} means to wait forever.* 等待最多{@code millis}毫秒以使该线程死亡。 {@code 0}超时意味着永远等待。* 如ThreadB中执行ThreadA#join(50),则等待50ms后ThreadB死亡** <p> This implementation uses a loop of {@code this.wait} calls* conditioned on {@code this.isAlive}.* As a thread terminates the {@code this.notifyAll} method is invoked.* It is recommended that applications not use {@code wait}, {@code notify}, or* {@code notifyAll} on {@code Thread} instances.* 建议应用程序不要在{@code Thread}实例上使用{@code wait},{@code notify}或{@code notifyAll}。**/// 在单独的锁对象而不是此线程上同步public final void join(long millis) throws InterruptedException {synchronized (lock) {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {lock.wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}lock.wait(delay);now = System.currentTimeMillis() - base;}}}}/*** @param millis the time to wait in milliseconds* @param nanos  {@code 0-999999} additional nanoseconds to wait* @throws IllegalArgumentException 如果{@code millis}的值为负,或者{@code nanos}的值*                                  不在{@code 0-999999}范围内* @throws InterruptedException     如果有任何线程中断了当前线程。抛出此异常时,将清除当前线程的中断状态*/// 在单独的锁对象而非此线程上同步。public final void join(long millis, int nanos) throws InterruptedException {synchronized (lock) {if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (nanos < 0 || nanos > 999999) {throw new IllegalArgumentException("nanosecond timeout value out of range");}if (nanos >= 500000 || (nanos != 0 && millis == 0)) {millis++;}join(millis);}}public final void join() throws InterruptedException {join(0);}
  1. join()或者join(long)一般使用在一个线程中调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态继续执行代码。
  2. join()方法会响应线程中断抛出InterruptedException异常

setPriority
设置线程的优先级,线程优先级在1~10,不在范围则抛异常。

    /*** 修改线程的优先级,默认是Thread Group的最大优先级,一般为5** @exception  IllegalArgumentException  不在1~10范围内,则抛异常* @exception  SecurityException  if the current thread cannot modify this thread. */public final void setPriority(int newPriority) {ThreadGroup g;checkAccess();if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {// Android-changed: Improve exception message when the new priority is out of bounds.throw new IllegalArgumentException("Priority out of range: " + newPriority);}if((g = getThreadGroup()) != null) {if (newPriority > g.getMaxPriority()) {newPriority = g.getMaxPriority();}// Android-changed: Avoid native call if Thread is not yet started.// setPriority0(priority = newPriority);synchronized(this) {this.priority = newPriority;if (isAlive()) {// BEGIN Android-added: Customize behavior of Thread.setPriority().// http://b/139521784// setPriority0(newPriority);ThreadPrioritySetter threadPrioritySetter =RuntimeHooks.getThreadPrioritySetter();int nativeTid = this.getNativeTid();if (threadPrioritySetter != null && nativeTid != 0) {threadPrioritySetter.setPriority(nativeTid, newPriority);} else {setPriority0(newPriority);}// END Android-added: Customize behavior of Thread.setPriority().}}}}

四、如何使用线程

线程使用方法有三种如下:

  1. 继承Tread类创建线程
    private class TestThread extends Thread{@Overridepublic void run() {super.run();...}}new TestThread().start();
  1. 实现Runnable接口创建线程
  private class TestRunnable implements Runnable{@Overridepublic void run() {...}}new Thread(new TestRunnable()).start();
  1. 继承Tread类创建线程
       FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {...return null;}});new Thread(futureTask).start();

三种方式比较:

  1. Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活。它只是用来启动线程而已,执行代码体一般不放这里。
  2. Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
  3. Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行。
  4. Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

总结

本篇博文主要讲解Thread的创建、使用及常用属性方法,适合新手学习阅读。对于对线程学习有更高要求的同学请继续阅读Thread系列中的其他文章。


面试扩展

1.线程多次调用start()方法后会怎样?
答:会抛出IllegalThreadStateException 异常,不可多次调用start(),除非start()中的createNative()调用失败。才可能再次正常调用start()

相关链接

  1. Thread线程从零认识到深层理解——初识
  2. Thread线程从零认识到深层理解——六大状态
  3. Thread线程从零认识到深层理解——wait()与notify()
  4. Thread线程从零认识到深层理解——线程安全
  5. 线程池从零认识到深层理解——初识
  6. 线程池从零认识到深层理解——进阶

扩展链接:

  1. Android CameraX 使用入门
  2. Android Studio 4.0新特性及升级异常

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

Thread线程从零认识到深层理解——初识相关推荐

  1. Android中使用Thread线程出现的问题

    很多初入Android或Java开发的新手对Thread.Looper.Handler和Message仍然比较迷惑,衍生的有HandlerThread.java.util.concurrent.Tas ...

  2. 在Android中使用Handler和Thread线程执行后台操作

    在 Android中使用Handler和Thread线程执行后台操作 对于线程的控制,我们将介绍一个 Handler类,使用该类可以对运行在不同线程中的多个任务进行排队,并使用Message和Runn ...

  3. Arduino 与 SPI 结合使用 以及SPI 深层理解

    本文主要讲解两部分内容,不做任何转发,仅个人学习记录: 一. Arduino 与 SPI 结合使用  : 二. SPI 深层理解 有价值的几个好的参考: 1. 中文版: https://blog.cs ...

  4. android java thread_Android中断并重启一个Thread线程的简单方法

    这里简单的总结下(大概思路,没调试,可能会有错!): MyThread.java pulbic class MyThread implemets Thread{ @overide public voi ...

  5. [Android]Thread线程入门3--多线程

    经过 [Android]Thread线程入门1 和[Android]Thread线程入门2 的学习,我们对线程有了简单的了解.在实际应用中,一般都会用到多线程.很少像前面的例子这么简单.那么如何实现多 ...

  6. 19.Qt中Thread线程中创建QTcpSocket

    Thread线程中创建QTcpSocket 本文承接上一篇博文,Qt线程创建,本文记录在线程中创建socket 套接字,连接服务器进行编程. /**************************** ...

  7. Thread 线程基础之-线程相关知识

    线程的优先级 设置或者获得当前线程的优先级: using System;using System.Collections.Generic;using System.Text;using System. ...

  8. java thread 线程_Java Thread类简述

    今天我们来看下java.lang.Thread这个类. 在学习Thread类之前,先看下线程相关知识:线程的几种状态.上下文切换,然后介绍Thread类中的方法的具体使用. 1.线程的状态 线程从创建 ...

  9. Java线程池几个参数的理解

    线程池几个参数的理解: 比如去火车站买票, 有10个售票窗口, 但只有5个窗口对外开放. 那么对外开放的5个窗口称为核心线程数, 而最大线程数是10个窗口.如果5个窗口都被占用, 那么后来的人就必须在 ...

最新文章

  1. java if (name!=null name!=),命名不规范,lombok泪两行!
  2. 爬取我主良缘,获取个人图片及其信息
  3. std::bind技术内幕
  4. 索引 - 数据结构 - BTREE
  5. mysql004操作表.增删改
  6. GANs最新综述论文: 生成式对抗网络及其变种如何有用【附pdf下载】
  7. 【MyBatis框架】查询缓存-二级缓存原理
  8. Yam Finance和UMA合作推出Degenerative Finance
  9. 设计模式入门进阶深入书籍汇总
  10. 无法正确检查该计算机的授权,一个问题阻止windows正确检查此机器的许可证,错误代码ox80070002...
  11. 保姆级教程 树莓派4b ubuntu20.04 的 linux 之旅
  12. Win10 Microsoft Edge浏览器播放视频出现绿屏情况解决之一
  13. ORACLE对表批处理操作
  14. 关于saas模式开发
  15. 解决mount.nfs: /home/xxxx/mpi-install is busy or already mounted问题
  16. 怎样做好服务器运维工作
  17. springboot+shiro+jwt实现登录+权限验证
  18. 轻量级容器主机 Photon OS
  19. Tensorflow GPU并行运算
  20. 联想m100系列出现异响 声音大 齿轮响等问题解决方法

热门文章

  1. MySql 里的IFNULL、NULLIF和ISNULL用法区别
  2. 语录分享 ——许逊真君《警世格言》
  3. MyBatis学习总结
  4. [Python3网络爬虫开发实战] --分析Ajax爬取今日头条街拍美图
  5. ICON的设计很重要
  6. zzuoj--10401--物资调度(dfs)
  7. 动手做一个键鼠套装(含linux驱动)
  8. 基金申请-5:如何键入短连字符(连接符、短横线) hyphen/en dash/em dash?
  9. 台式计算机销量排名,2019台式电脑销量排行_笔记本哪些好 2019笔记本销量排行榜...
  10. 免费领!校园招聘全流程知识图谱,HR收藏必看!(牛客独家)