《安琪拉与面试官二三事》系列文章
一个HashMap能跟面试官扯上半个小时
一个synchronized跟面试官扯了半个小时

《安琪拉教鲁班学算法》系列文章

安琪拉教鲁班学算法之动态规划

安琪拉教鲁班学算法之BFS和DFS

安琪拉教鲁班学算法之堆排序
《安琪拉教妲己学分布式》系列文章
安琪拉教妲己分布式限流
《安琪拉教百里守约学并发编程》系列文章
安琪拉教百里守约学并发编程之多线程基础

本文是来自读者群里@凯的建议,决定开一个多线程的专栏,尊重读者安琪拉是认真的,为什么是教百里守约,也是因为读者群里@百里守约是安琪拉的忠实读者,每期必读,经常草丛里定点蹲安琪拉,然后抢板凳(放大招)。这期是《安琪拉教百里守约学并发线程》系列文章第一集多线程基础。

前言

并发编程应该是Java 后端工程必备的技能,在日常开发中用的好能提升系统吞吐量,提升业务逻辑执行效率,提高系统的响应性,简化程序结构,当然这把青龙偃月刀也不是随随便便就能耍的好,需要些内力。先放一张Java 并发工具包JUC的知识脑图,后面 Wx公众号【安琪拉的博客】《安琪拉教百里守约学并发线程》会按以下思维脑图详细介绍 JUC 的各部分组件实际使用场景以及组件特性:

开场

百里守约:安琪拉,你熟悉线程(Thread)吗?和进程(Process)有什么区别?

安琪拉:熟悉啊!一个应用就是一个进程,一个进程可以包含多个线程,从操作系统层面看,同一个进程中的线程共享该进程的资源,例如内存空间和文件句柄。Linux 操作系统中线程是轻量级进程。

百里守约:在Java 中怎么创建一个线程呢?

安琪拉:线程的创建有2 种方式,如下,很多网上的文章还写了通过线程池的方式创建,其本质也是这二种中的一种:

  1. 继承 Thread 类;
  2. 实现 Runnable 接口;

百里守约:能不能用实际的代码举例说一下?

安琪拉:可以,如下所示:

public static void main(String[] args) {new Seller("笔").start();new Thread(new Seller02("书")).start();
}//第一种方式 继承 Thread
public static class Seller extends Thread{String product;public Seller(String product){this.product = product;}@Overridepublic void run() {System.out.println("继承 Thread类 卖 " + product);}
}//第二种方式 实现 Runnable
public static class Seller02 implements Runnable{String product;public Seller02(String product){this.product = product;}@Overridepublic void run() {System.out.println("实现 Runnable接口 卖 " + product);}
}

百里守约:如果我直接使用 new Seller("笔").run() 执行和start() 有什么区别?

安琪拉start() 方法是native 方法,JVM 会另起一个线程执行,而直接执行run() 方法是本地线程执行,我们可以使用示例程序对比一下,如下:

public static void main(String[] args) {new Seller("笔").run(); //没有另起一个线程new Seller("笔").start(); //在新线程中执行 run 函数
}//第一种方式 继承 Thread
public static class Seller extends Thread{String product;public Seller(String product){this.product = product;}@Overridepublic void run() {System.out.println(String.format("当前线程: %s 卖%s", Thread.currentThread().getName(), product));}
}

看下控制台输出如下:

当前线程: main 卖笔
当前线程: Thread-1 卖笔

因为调用start() 方法后,JVM 会新建一个线程来执行run() 方法内容。

百里守约:我理解了,Thread 对象是Java 中普通的对象,和其他对象一样,只是在调用 start() 这个native 方法时变得不一样了,JVM 会根据Thread 对象来创建线程。

安琪拉:你说的非常对,new Thread() 创建Thread 对象时,JVM 还没有实际创造线程,调用start() 方法后JVM 才会通过 pthread_create 方法(Linux系统)创建线程。因此一定要将Thread 对象和真实的线程区分开。

百里守约:那JVM 又是如何创建线程的呢?

安琪拉:你这个问的有点深了,我可以大致讲讲,因为今天是基础篇,因此不展开聊,想深入了解的可以关注【安琪拉的博客】公众号,有源代码层的详细的讲解,先丢个源代码地址:Hotspot1.8 jvm.cpp,推荐先将本篇文章整体看完,然后回过头来再看实现原理。担心很多同学没学过c++,或者源码太多无从下嘴,后面会出一期JVM 创建线程源代码解析。

今天先丢个大致原理:Java 种新建的Thread 对象只是操作系统线程运行的载体,Thread类的作用主要有二点:

  • Thread 对象内的属性提供了创建新线程时所需要的线程描述信息,例如线程名、线程id、线程组、是否为守护线程;
  • Thread 对象内的方法提供了Java 程序可以跟操作系统线程打交道的手段,例如wait、sleep、join、interrupt等。

前面说到JVM new Thread对象时其实还没有真实创建线程,调用start() 方法时才开始正式创建。

百里守约:那线程是怎么从创建到执行,最后销毁的啊?

安琪拉:那你就要看Java 中线程的生命周期了,如下图所示:

在 Thread 类中有个State 枚举类型标识线程状态,如下。

public static enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

同时可以使用Thread.currentThread().getState()获取当前线程的状态。

解释一下每种状态:

  • New: 刚创建而未启动的线程就是这个状态。由于一个线程只能被启动一次,因此一个线程只可能有一次在这个状态。
  • Runnable:如上图,这个状态实际是个复合状态,包含二个子状态:Ready 和 Running。Ready是就绪状态,可以被JVM 线程调度器(Scheduler) 进行调度,如果是单核CPU,同一时刻只有一个线程处于Running 状态,可能有多个线程处于 Ready 状态,Running 表示当前线程正在被CPU 执行,在Java 中就是Thread 对象只 run() 方法正在被执行。当 yield() 方法被调用,或者线程时间片被用完,线程就会从 Running 状态转为 Ready 状态。另外有个小姿势点,CPU 的一个时间片时间是多久呢? 这个展开来讲又可以单独写篇文章,这里只说一个结论:CPU时间片和主机时钟频率有关系,一般是10 ~ 20 ms。
  • Blocked:一个线程发生一个阻塞式I/0 (文件读写I/O, 网络读写I/O)时,或者试图获取其他线程持有的锁时,线程会进入此状态,例如:获取别的线程已经持有的 synchronized 修饰的对象锁。如果大家对synchronized 关键字感兴趣,可以看我这篇文章 一个synchronized跟面试官扯了半个小时,建议看完这篇再回过头看,顺便还可以点个赞。在Blocked 状态的线程不会占用CPU 资源,但是程序如果出现大量处于这个状态的线程,需要警惕了,可以考虑优化一下程序性能。
  • Waiting: 一个线程执行了Object.wait( )、 Thread.join( ) 、LockSupport.park( ) 后会进入这个状态,这个状态是处于无限等待状态,没有指定等待时间,可以和Timed_Waiting 对比,Timed_Waiting是有等待时间的。这个状态的线程如果要恢复到Runnable 状态需要通过别的线程调用Object.notify( )、Object.notifyAll( )、LockSupport.unpark( thread )。
  • Timed_Waiting: 带时间限制的Waiting。
  • Terminated: 已经执行结束的线程处于此状态。Thread 的 run( ) 方法执行结束,或者由于异常而提前终止都会让线程处于这个状态。

百里守约:你刚才上面讲了wait( )、sleep( )、join( )、yield( ) 、notify()、notifyAll( ) 都是做什么的?什么区别?

安琪拉:这些方法都是线程控制方法,JAVA 通过这些方法跟它创建的操作系统线程进行交互,具体如下:

  • wait:线程等待,调用该方法会让线程进入 Waiting 状态,同时很重要的一点,线程会释放对象锁,所以wait 方法一般用在同步方法或同步代码块中;

  • sleep: 线程休眠,调用该方法会让线程进入Time_Waiting 状态,调sleep 方法需要传入一个参数标识线程需要休眠的时间;

  • yield:线程让步,yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片,一般来说,优先级高的线程有更大的可能性成功竞争到CPU 时间片,但不是绝对的,有的系统对优先级不敏感。

  • join:在当前线程中调用另一个线程的join 方法,则当前线程转为阻塞状态,等到另一线程执行结束,当前线程才会从阻塞状态变为就绪状态,等待CPU 的调度。写个代码一看就明白:

    public static void main(String[] args) {System.out.println(String.format("主线程%s 开始运行...", Thread.currentThread().getName()));Thread threadA = new Thread(new ThreadA());threadA.start();try {// 主线程 wait(0) 释放 thread 对象锁,主线程进入 waiting 状态threadA.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(String.format("主线程%s 运行结束...", Thread.currentThread().getName()));
    }private static class ThreadA implements Runnable{@Overridepublic void run() {System.out.println(String.format("子线程%s 开始运行...", Thread.currentThread().getName()));try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(String.format("子线程%s 准备结束运行...", Thread.currentThread().getName()));}
    }
    

    控制台输出如下:

    主线程main 开始运行...
    子线程Thread-0 开始运行...
    子线程Thread-0 准备结束运行...
    主线程main 运行结束...
    

    主线程调用threadA.join() 导致主线程等Thread-0 线程执行结束才开始继续执行。

    join() 函数的内部实现如下:

    public final void join() throws InterruptedException {join(0);
    }
    /*** Waits at most {@code millis} milliseconds for this thread to* die. A timeout of {@code 0} means to wait forever.** <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.*/
    public final synchronized void join(long millis)throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {//如果当前Thread对象关联的线程还是存活的,当前正在执行的线程进入 Waitting状态,如果当前Thread对象关联的线程执行结束,会调用notifyAll() 唤醒进入 Waitting状态的线程。while (isAlive()) {wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}wait(delay);now = System.currentTimeMillis() - base;}}
    }//wait 属于 Object 对象方法
    public class Object{//线程进入 Time_Watting 或 Waiting 状态public final native void wait(long timeout) throws InterruptedException;
    }
    

    为了便于大家理解,我画了图(一言不合就上图),大家对照着代码和图看,上面代码主要有二个线程,主线程和 ThreadA 线程,主线程创建ThreadA并启动ThreadA线程,然后调用threadA.join() 会导致主线程阻塞,直到ThreadA 线程执行结束 isActive 变为 false,主线程恢复继续执行。

  • interrupt:线程中断,调用interrupt 方法中断一个线程,是希望给这个线程一个通知信号,会改变线程内部的一个中断标识位,线程本身并不会因为中断而改变状态(如阻塞、终止等)。调用interrupt 方法有二种情况:

    1. 如果当前线程正处于 Running 状态,interrupt( ) 只会改变中断标识位,不会真的中断正在运行的线程;
    2. 如果线程当前处于 Timed_Waiting 状态,interrupt( ) 会让线程抛出 InterruptedException。

    所以我们在编写多线程程序时,优雅关闭线程需要同时处理这二种情况,常规写法是:

    public static class ThreadInterrupt implements Runnable{@Overridepublic void run() {//1. 非阻塞状态,通过检查中断标识位退出while(!Thread.currentThread().isInterrupted()){try{//doSomething()Thread.sleep(1000);} catch (InterruptedException e) {//2. 阻塞状态,捕获中断异常,break 退出e.printStackTrace();break;}}}
    }
    
  • notify:notify方法和wait方法一样,也是Object 类中的方法,notify方法用于唤醒在此对象监视器上等待的单个线程,如果有多个线程在此对象监视器上等待,选择其中一个进行唤醒。另外要注意一点的是,当前线程唤醒等待线程后不会立即释放锁,而是当前线程执行结束才会释放锁,因此被唤醒的线程不是说唤醒之后立即就可以开始执行,而是要等到唤醒的线程执行结束,获得对象锁之后开始执行。上代码吧。

    public static void main(String[] args) {new Thread(new ThreadA()).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(new ThreadB()).start();
    }private static final Object lock = new Object();private static class ThreadA implements Runnable{@Overridepublic void run() {synchronized (lock){System.out.println("Thread-A 进入状态 running...");try {System.out.println("Thread-A 进入状态 waiting...");lock.wait();System.out.println("Thread-A 进入状态 running...");} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread-A 执行完毕, 进入状态 terminated...");}
    }private static class ThreadB implements Runnable{@Overridepublic void run() {synchronized (lock){System.out.println("Thread-B 进入状态 running...");try {System.out.println("Thread-B 进入状态 time_waiting...");Thread.sleep(3000);System.out.println("Thread-B 进入状态 running...");lock.notify();System.out.println("Thread-B 进入状态 time_waiting...");Thread.sleep(5000);System.out.println("Thread-B 进入状态 running...");} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread-B 执行完毕, 进入状态 terminated...");}
    }
    

    控制台输出:

    Thread-A 进入状态 running...
    Thread-A 进入状态 waiting...
    Thread-B 进入状态 running...
    Thread-B 进入状态 time_waiting...
    Thread-B 进入状态 running...
    Thread-B 进入状态 time_waiting...
    Thread-B 进入状态 running...
    Thread-B 执行完毕, 进入状态 terminated...
    Thread-A 进入状态 running...
    Thread-A 执行完毕, 进入状态 terminated...
    

    可以看到B 线程调用 lock.notify() 之后A 线程没有立即开始执行,而是等到B 线程执行结束后才开始执行,所以lock.notify() 唤醒 A 线程只是让 A 线程进入预备执行的状态,而不是直接进 Running 状态,B 线程调 notify 没有立即释放对象锁。

    鉴于篇幅原因,此篇也是基础篇,知识部分就到此为止,接下来是一些常规的线程面试题。

第一题:关闭线程的方式有哪几种?哪种方式最可取?(美团一面面试题)
  1. 使用退出标识位;

    public class ThreadSafe extends Thread { public volatile boolean exit = false;public void run() { while (!exit){//do something }}
    }
    
  2. 调用 interrupt 方法,这种是最可取的,但是要考虑到处理二种情况;

  3. stop 方法,这种属于强行终止,非常危险。就像直接给线程断电,调用thread.stop() 方法时,会释放子线程持有的所有锁,这种突然的释放可能会导致数据不一致,因此不推荐使用这种方式终止线程。

第二题:很多面试会问wait 和sleep 的区别?(比心一面面试题)

主要有以下3点:

  1. sleep 方法让线程进入 Timed_Waiting 状态,sleep 方法必须传入时间参数,会让当前线程挂起一段时间,过了这个时间会恢复到runnable 状态(取决于系统计时器和调度程序的精度和准确性)。而wait 方法会让当前线程进入Waiting 状态,会一直阻塞,直到别的线程调用 notify 或者 notifyAll 方法唤醒。
  2. wait 是Object 类中的方法,sleep 是Thread 类中的方法,理解这点很重要,wait方法跟对象绑定的,调用wait方法会释放wait 关联的对象锁;
  3. 如果在同步代码块,当前线程持有锁,执行到wait 方法会释放对象锁,sleep 只是单纯休眠,不会释放锁;

我们看个代码巩固一下:

public static void main(String[] args) {new Thread(new ThreadA()).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(new ThreadB()).start();
}private static final Object lock = new Object();private static class ThreadA implements Runnable{@Overridepublic void run() {synchronized (lock){System.out.println("Thread-A 进入状态 running...");try {System.out.println("Thread-A 进入状态 waiting...");lock.wait();System.out.println("Thread-A 进入状态 running...");} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread-A 执行完毕, 进入状态 terminated...");}
}private static class ThreadB implements Runnable{@Overridepublic void run() {synchronized (lock){System.out.println("Thread-B 进入状态 running...");try {System.out.println("Thread-B 进入状态 time_waiting...");Thread.sleep(3000);System.out.println("Thread-B 进入状态 running...");lock.notify();System.out.println("Thread-B 进入状态 time_waiting...");Thread.sleep(5000);System.out.println("Thread-B 进入状态 running...");} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread-B 执行完毕, 进入状态 terminated...");}
}

这里我建议大家先不急着看控制台输出,根据自己经验猜测一下输出应该是怎样的,然后对比输出,这样对比能看是否有偏差。另外我建议大家有条件,把本篇文章的示例程序拷贝到本地,实际看下运行。

控制台输出如下:

Thread-A 进入状态 running...
Thread-A 进入状态 waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 执行完毕, 进入状态 terminated...
Thread-A 进入状态 running...
Thread-A 执行完毕, 进入状态 terminated...
第三题:手写一个死锁的例子?(美团二面面试题)
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();public static class DeadLockSample implements Runnable{Object[] locks;public DeadLockSample(Object lock1, Object lock2){locks = new Object[2];locks[0] = lock1;locks[1] = lock2;}@Overridepublic void run() {synchronized (lock1) {try {Thread.sleep(3000);synchronized (lock2) {System.out.println(String.format("%s come in...", Thread.currentThread().getName()));}} catch (InterruptedException e) {e.printStackTrace();}}}
}public static void main(String[] args) {Thread a = new Thread(new DeadLockSample(lock1, lock2));Thread b = new Thread(new DeadLockSample(lock2, lock1));a.start();b.start();
}
第四题:写一个通过线程wait / notify通信的生产者消费者代码?(声网四面面试题)
static class MangoIce{int counter;public MangoIce(int counter) {this.counter = counter;}}static class Producer implements Runnable{private final List<MangoIce> barCounter;private final int           MAX_CAPACITY;public Producer(List<MangoIce> sharedQueue, int size){this.barCounter = sharedQueue;this.MAX_CAPACITY = size;}@Overridepublic void run(){int counter = 1;while (!Thread.currentThread().isInterrupted()){try{produce(counter++);}catch (InterruptedException ex){ex.printStackTrace();break;}}}private void produce(int i) throws InterruptedException{synchronized (barCounter){while (barCounter.size() == MAX_CAPACITY){System.out.println("吧台满了,冰沙放不下 " + Thread.currentThread().getName() + " 线程等待,当前吧台冰沙数: " + barCounter.size());barCounter.wait();}Thread.sleep(1000);barCounter.add(new MangoIce(i));System.out.println("生产第: " + i + "杯冰沙...");barCounter.notifyAll();}}}static class Consumer implements Runnable{private final List<MangoIce> barCounter;public Consumer(List<MangoIce> sharedQueue){this.barCounter = sharedQueue;}@Overridepublic void run(){while (!Thread.currentThread().isInterrupted()){try{consume();} catch (InterruptedException ex){ex.printStackTrace();break;}}}private void consume() throws InterruptedException{synchronized (barCounter){while (barCounter.isEmpty()){System.out.println("吧台空的,没有冰沙 " + Thread.currentThread().getName() + " 消费者线程等待,当前吧台冰沙数: " + barCounter.size());barCounter.wait();}Thread.sleep(1000);MangoIce i = barCounter.remove(0);System.out.println("消费第: " + i.counter + "杯冰沙...");barCounter.notifyAll();}}}public static void main(String[] args){List<MangoIce> taskQueue = new ArrayList<>();int MAX_CAPACITY = 5;Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "生产者");Thread tConsumer = new Thread(new Consumer(taskQueue), "消费者");tProducer.start();tConsumer.start();}

控制台输出

生产第: 1杯冰沙...
生产第: 2杯冰沙...
生产第: 3杯冰沙...
生产第: 4杯冰沙...
生产第: 5杯冰沙...
吧台满了,冰沙放不下 生产者 线程等待,当前吧台冰沙数: 5
消费第: 1杯冰沙...
消费第: 2杯冰沙...
消费第: 3杯冰沙...
消费第: 4杯冰沙...
消费第: 5杯冰沙...
吧台空的,没有冰沙 消费者 消费者线程等待,当前吧台冰沙数: 0
生产第: 6杯冰沙...
生产第: 7杯冰沙...
生产第: 8杯冰沙...
生产第: 9杯冰沙...
生产第: 10杯冰沙...
吧台满了,冰沙放不下 生产者 线程等待,当前吧台冰沙数: 5
消费第: 6杯冰沙...
消费第: 7杯冰沙...

后面几期会分别讲以下内容,顺序还没定,大概会按照读者群的反馈来。

  • 线程上下文切换、JAVA锁
  • 线程池实战、Fork / Join
  • 并发工具 CyclicBarrier、CountDownLatch、Semaphore的实际使用场景
  • synchronized、volatile、Atomic*** 涉及的原子性、内存可见性和指令重排序原理
  • AQS、ReentrantLock原理、以及和synchronized区别、CAS原理
  • 阻塞队列、线程调度
    欢迎关注Wx 公众号【安琪拉的博客】查看后续内容更新

参考:How to work with wait(), notify() and notifyAll() in Java?

安琪拉教百里守约学并发编程之多线程基础相关推荐

  1. 安琪拉教鲁班学堆排序

    <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 <安琪拉教鲁班学算法>系列文章 安琪拉教鲁班学算法 ...

  2. 安琪拉教鲁班学算法之动态规划

    <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 <安琪拉教鲁班学算法>系列文章 安琪拉教鲁班学算法 ...

  3. 安琪拉教鲁班学算法之BFS和DFS

    安琪拉教鲁班学算法之BFS和DFS <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 <安琪拉教鲁班学算法 ...

  4. 安琪拉教鲁班玩Java反射-业务场景篇

    安琪拉教鲁班玩Java反射-业务场景篇 Java 语言中有很多特性,其中有一项很重要的特性就是反射,我们在很多框架类的代码中可能会看到反射的身影,那实际在业务逻辑层是否可以用反射特性做些事情呢? 安琪 ...

  5. 安琪拉教妲己分布式限流

    安琪拉教妲己分布式限流 在系统设计中,限流是保障系统高可用的一种常规手段,同样的手段还有熔断.服务降级等等,此篇文章作为一个开端,是<安琪拉教妲己分布式系统设计>的第一篇 妲己:听说最近你 ...

  6. 并发编程(一)多线程基础和原理

    多线程基础 最近,准备回顾下多线程相关的知识体系,顺便在这里做个记录. 并发的发展历史 最早的计算机只能解决简单的数学运算问题,比如正弦. 余弦等.运行方式:程序员首先把程序写到纸上,然后穿 孔成卡片 ...

  7. 并发编程之多线程基础-守护线程与非守护线程(四)

    守护线程概念: 只要当前JVM实例中尚存在任何一个非守护线程没有结束, 守护线程就全部工作; 只有当最后一个非守护线程结 束时, 守护线程随着 JVM 一同结束工作. 守护线程最典型的应用就是 GC ...

  8. 并发编程之多线程基础-Thread和Runnable的区别及联系(二)

    上篇文章讲述了创建线程的常用方式 本篇主要分析一下Thread和Runnable两种方式创建线程的区别及联系 联系: ▶Thread类实现了Runable接口. ▶都需要重写里面Run方法. 区别: ...

  9. 遇到网络问题你是怎么解决的?安琪拉有二招

    本文是以安琪拉在项目中解决的一个网络异常(Broken Pipe)为出发点写的,我想玩家们应该多少遇到过网络问题,这篇文章也提供了一种解决网络问题的思路,遇到类似网络问题时可以借鉴这个思路. 为了尽量 ...

最新文章

  1. productFlavors 差异打包问题
  2. pythonsqlite3教程_使用 Python 在线操作 sqlite3
  3. 每天一道LeetCode-----找到序列中第一个没有出现的正整数,要求时间复杂度是O(n),空间复杂度是O(1)
  4. linux 卸载 openldap,Linux下安装openldap
  5. 此计算机到未能识别网络连接,本地连接显示“未识别的网络”
  6. C#几个经常用到的字符串的截取
  7. IndexedDB:浏览器里内置的数据库
  8. Matlab随笔之三维图形绘制
  9. [转载] python之flask框架
  10. 计算机视觉方面书籍推荐
  11. 20155302《网络对抗》Exp7 网络欺诈防范
  12. Quartus II中通过调用IP核实现RS编解码
  13. Protel网络教程
  14. java拆分pdf_itextpdf拆分pdf问题
  15. C# 将Big5繁体转换简体GB2312的代码
  16. 控制系统伯德图、带宽及动态响应的关联性
  17. HTML href指向function
  18. Android攻城狮fragment
  19. gammatone 滤波器详解及其MATLAB代码实现
  20. 智慧之战——农民工大战博士后

热门文章

  1. react骨架屏自动生成_前端骨架屏方案小结
  2. 基于docker安装部署Zabbix及使用,自动发现、日志监控报警及参照官方文档遇到的问题(图文)
  3. 你知道二维码和NFC的优缺点吗?你认为谁才是移动支付的未来?
  4. 【IoT】交互式智能多功能插排设计
  5. Java知识学习与总结
  6. CSS 7 CSS 层叠规则 BFC 和 IFC 机制
  7. IDL 合并一维二维三维数组
  8. 关于天地图覆盖物-聚合点数据问题
  9. QTreeWidget删除节点及子节点
  10. PYTHON_设置索引