当时我刚开始学习并发编程其实是挺茫然的,讲的好的视频资料很少,只能靠自己看书看文档。但是大部分书是写的是比较教科书似的,让人看一眼就想关上。所以自己在入门之后,就想做一个简单的并发编程的教程,方便想要入门学习的人有一个低门槛。

在本场 Chat 中,会讲到如下内容:

  1. Thread 的基本使用
  2. 基本的等待通知模型

适合人群: 准备学习并发编程的人群。

构建简单的 GUI 线程和数据线程工作模式

GUI 初始化和数据初始化

为了为我们所有的 test 构建一个上下文的 context,首先我们来写几个类,来简单模拟下我们安卓中,GUI 线程和数据线程是如何来显示按钮和数据的:

  • ModelAndView我们简单的编写一个可见的按钮,并给出了几个主要的属性,其中包含外观的数据和按钮本身要绑定的数据:
/**     * 一个 GUI 控件,缩略版本     */    @Data    class ModelAndView {        /**         * 长度         */        private Integer length;        /**         * 宽度         */        private Integer width;        /**         * X 位置         */        private Integer xPos;        /**         * Y 位置         */        private Integer yPos;        /**         * 控件上绑定数据         */        private Map<String, Object> data;    }
  • 初始化 GUI 线程在这个线程中,我们主要模拟按钮的外观初始化过程:
/**    * 初始化 GUI 线程    */   class GuiInitThread implements Runnable {       ModelAndView modelAndView;       public GuiInitThread(ModelAndView modelAndView) {           this.modelAndView = modelAndView;       }       @Override       public void run() {           modelAndView.setLength(1);           modelAndView.setWidth(1);           modelAndView.setXPos(0);           modelAndView.setYPos(0);       }   }
  • 数据初始化线程在这个线程中,我们主要是进行绑定数据的初始化。因为一般来说,在我们实际使用中,这个数据的初始化时间是比较长的,为了跟展示的初始化相互影响,一般绑定数据的初始化都会放在额外的线程来做。在类里面,我们主要是对绑定数据进行赋值。一般来说,这个线程所承担的工作大部分是对远端接口进行请求,获取数据,然后处理数据,绑定回控件,整个过程受网络影响,数据大小影响等等。
   /**    * 数据初始化线程    */   class DataThread implements Runnable {       ModelAndView modelAndView;       public DataThread(ModelAndView modelAndView) {           this.modelAndView = modelAndView;       }       @Override       public void run() {           Map<String, Object> data = new HashMap<>();           try {               Thread.sleep(1000); //徒增耗时           } catch (InterruptedException e) {               //先这样               e.printStackTrace();           }           for (int i = 0; i < 100; i++) {               data.put(String.valueOf(i), i);           }           modelAndView.setData(data);       }   }

主要的类我们编写完了,接下来来简单写个 test case:

    /**     * init model and view test     *     * @throws Exception     */    @Test    public void initModelAndView() throws Exception {        ModelAndView modelAndView = new ModelAndView();        Thread guiInitThread = new Thread(new GuiInitThread(modelAndView));        Thread dataThread = new Thread(new DataThread(modelAndView));        guiInitThread.start();        dataThread.start();        guiInitThread.join();        dataThread.join();        System.out.println(JSONObject.toJSONString(modelAndView)); //完成后打印下啦,看看初始完成之后的情况    }

以上就是第一个 demo,做这个 demo 的目的主要是自己当时初学多线程时候,由于当时还没接触过客户端的开发,对多线程的学习完全是从方法学起的,而不是在这样一个环境下,这就造成了不知道什么时候该多线程,这样的不在 context 环境下的对多线程的学习,其实是无用的。所以之后,我们尽量都会先构建一个环境,然后再环境下我们面对什么样子的问题,对于这个问题我们去学习。

PS:少理论,多硬核代码,主要还是对照例子体会。

涉及方法解析

  • start
/**    * Causes this thread to begin execution; the Java Virtual Machine    * calls the <code>run</code> method of this thread.    * <p>    * The result is that two threads are running concurrently: the    * current thread (which returns from the call to the    * <code>start</code> method) and the other thread (which executes its    * <code>run</code> method).    * <p>    * 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()    */

调用 start 马上会执行我们在 run 里面写的代码。

  • 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    *         the length of time to sleep in milliseconds    *    * @throws  IllegalArgumentException    *          if the value of {@code millis} is negative    *    * @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 native void sleep(long millis) throws InterruptedException;

使得当前线程睡眠一个毫秒数,但是当前线程不会放弃它的监视器,即不会释放锁(monitor 的事情后面再说);

  • join
/**     * Waits for this thread to die.     *     * <p> An invocation of this method behaves in exactly the same     * way as the invocation     *     * <blockquote>     * {@linkplain #join(long) join}{@code (0)}     * </blockquote>     *     * @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 final void join() throws InterruptedException {        join(0);    }

等待当前线程死掉,就是 run 方法跑完了。这里实际调用的是 join(0),再让我们看看 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.     *     * @param  millis     *         the time to wait in milliseconds     *     * @throws  IllegalArgumentException     *          if the value of {@code millis} is negative     *     * @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 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) {            while (isAlive()) {                wait(0);            }        } else {            while (isAlive()) {                long delay = millis - now;                if (delay <= 0) {                    break;                }                wait(delay);                now = System.currentTimeMillis() - base;            }        }    }

当传入参数是 0 的时候,不计较时间,就一直等着就完事儿了。但是当大于 0 的时候,会有个循环,循环里面调用的是我们的 Object 的 wait 方法,所以实际上,调用 join 方法是会释放锁的。

GUI 不断刷新,当重新绑定数据时候,停止刷新过程

构造移动过程

我们通过调整 xPos 和 yPos 来改变这个小按钮的位置,来改变按钮的位置,从视觉上产生一个按钮在移动的感觉。之后我们重新绑定数据,同时希望重新绑定数据开始时候,小按钮位置不再改变。

下面我们来添加一个对象移动的方法:

/**    * 对象移动    */   class MoveThread extends Thread {       ModelAndView modelAndView;       public Boolean isStop; //停止标记位置       public MoveThread(ModelAndView modelAndView) {           this.modelAndView = modelAndView;           this.isStop = false;       }       @Override       public void run() {               //注意:此处为正确停止线程方式           while (!this.isInterrupted() && !isStop) {                modelAndView.setXPos(modelAndView.getXPos() + 1);                modelAndView.setYPos(modelAndView.getYPos() + 1);          }       }   }

通过这个新线程,就能让我们的小按钮一直沿直线移动。

接着写我们的 test case:

/**     * 一直移动,但当重新绑定数据时候,停止移动     */    @Test    public void moveInterruptedByBindingData() throws Exception {      ModelAndView modelAndView = new ModelAndView();       //初始化 gui       Thread guiInitThread = new Thread(new GuiInitThread(modelAndView));       guiInitThread.start();       //开始移动       MoveThread moveThread = new MoveThread(modelAndView);       moveThread.start();       //开始数据绑定       Thread dataThread = new Thread(new DataThread(modelAndView));       dataThread.setDaemon(true);       dataThread.start();       //把移动过程停止       moveThread.interrupt(); //moveThread.isStop = true;       Thread.sleep(100);       System.out.println(JSONObject.toJSONString(modelAndView)); //完成后打印下啦,看看完成之后的情况    }

观察打印结果,会发现由于数据绑定开始,所需要的时间较长,所以将移动线程中断之后,又过了一段时间,绑定数据的线程还没开始准备数据。

涉及方法解析

  • 正确的停止线程让我们再次重新看类MoveThread,它内部定义了 isStop 方法:

public Boolean isStop; //停止标记位置

通过对 run 方法执行条件的观察,可以发现,当遇到外部中断或者手动标记 stop 都会使 run 方法停止,这种方法不会抛出异常,或者像之前的 stop 方法一样,出现不会立即停止的情况。

  • isInterrupted
/**    * Tests whether this thread has been interrupted.  The <i>interrupted    * status</i> of the thread is unaffected by this method.    *    * <p>A thread interruption ignored because a thread was not alive    * at the time of the interrupt will be reflected by this method    * returning false.    *    * @return  <code>true</code> if this thread has been interrupted;    *          <code>false</code> otherwise.    * @see     #interrupted()    * @revised 6.0    */   public boolean isInterrupted() {       return isInterrupted(false);   }   /**    * Tests if some Thread has been interrupted.  The interrupted state    * is reset or not based on the value of ClearInterrupted that is    * passed.    */   private native boolean isInterrupted(boolean ClearInterrupted);

该方法只会测试下线程是被中断,而不会影响中断标记位置,可以用作判断使用。

  • interrupt
/**    * Interrupts this thread.    *    * <p> Unless the current thread is interrupting itself, which is    * always permitted, the {@link #checkAccess() checkAccess} method    * of this thread is invoked, which may cause a {@link    * SecurityException} to be thrown.    *    * <p> If this thread is blocked in an invocation of the {@link    * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link    * Object#wait(long, int) wait(long, int)} methods of the {@link Object}    * class, or of the {@link #join()}, {@link #join(long)}, {@link    * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},    * methods of this class, then its interrupt status will be cleared and it    * will receive an {@link InterruptedException}.    *    * <p> If this thread is blocked in an I/O operation upon an {@link    * java.nio.channels.InterruptibleChannel InterruptibleChannel}    * then the channel will be closed, the thread's interrupt    * status will be set, and the thread will receive a {@link    * java.nio.channels.ClosedByInterruptException}.    *    * <p> If this thread is blocked in a {@link java.nio.channels.Selector}    * then the thread's interrupt status will be set and it will return    * immediately from the selection operation, possibly with a non-zero    * value, just as if the selector's {@link    * java.nio.channels.Selector#wakeup wakeup} method were invoked.    *    * <p> If none of the previous conditions hold then this thread's interrupt    * status will be set. </p>    *    * <p> Interrupting a thread that is not alive need not have any effect.    *    * @throws  SecurityException    *          if the current thread cannot modify this thread    *    * @revised 6.0    * @spec JSR-51    */   public void interrupt() {       if (this != Thread.currentThread())           checkAccess();       synchronized (blockerLock) {           Interruptible b = blocker;           if (b != null) {               interrupt0();           // Just to set the interrupt flag               b.interrupt(this);               return;           }       }       interrupt0();   }

中断线程,清除标记为,简单粗暴,没了;

  • setDaemon
/**     * Marks this thread as either a {@linkplain #isDaemon daemon} thread     * or a user thread. The Java Virtual Machine exits when the only     * threads running are all daemon threads.     *     * <p> This method must be invoked before the thread is started.     *     * @param  on     *         if {@code true}, marks this thread as a daemon thread     *     * @throws  IllegalThreadStateException     *          if this thread is {@linkplain #isAlive alive}     *     * @throws  SecurityException     *          if {@link #checkAccess} determines that the current     *          thread cannot modify this thread     */    public final void setDaemon(boolean on) {        checkAccess();        if (isAlive()) {            throw new IllegalThreadStateException();        }        daemon = on;    }

标记线程为后台线程,注意 start 前面设置,之后再设置就没用了。

二人对话

交替对话

>

  • hihello
  • how are ui'm fine,thank u
  • and u?i'm ok!

下面我们来 imagine 一个初中背诵并默写全文的一个英语场景,这也能是你学习这么多年英语别的都忘了,就记得这段对话的一个场景。

PS:我的建议是先自己写一个交替对话这样的两个线程,完成之后再往下看

先上代码,然后我们来分析下这个:

   String[] dialogs = {"hi", "hello", "how are u", "i'm fine,thank u", "and u?", "i'm ok!"};   Boolean isChineseSpeak = true;   final Object monitor = new Object();   Integer index = 0;   class ChinesePersonThread implements Runnable {       @Override       public void run() {           while (index < 5) {               synchronized (monitor) {                   while (!isChineseSpeak) {                       try {                           //当条件不满足时候,在这里等待条件对方完成的通知                           monitor.wait();                       } catch (InterruptedException e) {                           e.printStackTrace();                       }                   }                   isChineseSpeak = false;                   System.out.println("thread id : " + Thread.currentThread().getId() + ", say : " + dialogs[index]);                   index++;               }           }       }   }   class ForeignPersonThread implements Runnable {       @Override       public void run() {           while (index < 5) {               synchronized (monitor) {                   if (!isChineseSpeak) {                       System.out.println("thread id : " + Thread.currentThread().getId() + ", say : " + dialogs[index]);                       index++;                       isChineseSpeak = true;                       //执行完成之后通知等待线程                       monitor.notifyAll();                   }               }           }       }   }   @Test   public void test() throws Exception {       Thread chineseThread = new Thread(new ChinesePersonThread());       Thread foreignThread = new Thread(new ForeignPersonThread());       chineseThread.start();       foreignThread.start();       Thread.sleep(1000);   }

涉及方法解析

  • wait
/**     * Causes the current thread to wait until another thread invokes the     * {@link java.lang.Object#notify()} method or the     * {@link java.lang.Object#notifyAll()} method for this object.     * In other words, this method behaves exactly as if it simply     * performs the call {@code wait(0)}.     * <p>     * The current thread must own this object's monitor. The thread     * releases ownership of this monitor and waits until another thread     * notifies threads waiting on this object's monitor to wake up     * either through a call to the {@code notify} method or the     * {@code notifyAll} method. The thread then waits until it can     * re-obtain ownership of the monitor and resumes execution.     * <p>     * As in the one argument version, interrupts and spurious wakeups are     * possible, and this method should always be used in a loop:     * <pre>     *     synchronized (obj) {     *         while (&lt;condition does not hold&gt;)     *             obj.wait();     *         ... // Perform action appropriate to condition     *     }     * </pre>     * This method should only be called by a thread that is the owner     * of this object's monitor. See the {@code notify} method for a     * description of the ways in which a thread can become the owner of     * a monitor.     *     * @throws  IllegalMonitorStateException  if the current thread is not     *               the owner of the object's monitor.     * @throws  InterruptedException if any thread interrupted the     *             current thread before or while the current thread     *             was waiting for a notification.  The <i>interrupted     *             status</i> of the current thread is cleared when     *             this exception is thrown.     * @see        java.lang.Object#notify()     * @see        java.lang.Object#notifyAll()     */    public final void wait() throws InterruptedException {        wait(0);    }

调用此方法时候,必须获得对象的锁,然后直到其他线程通过 notify/notifyAll 或中断,它才能继续执行。ps,wait 方法会释放锁(emm,大家都这么写,其实翻译过来是监视器,一个意思)。同样,notify 这种通知方法,使用前也需要获取对象锁,然后通知一个在该对象上等待的线程。

  • 等待通知模式
  1. 对象 1 在获得锁的基础上,当条件不达到,就循环等待;
  2. 对象 2 在获得锁的基础上,执行完成之后,通知等待对象。

这个例子主要是为了写线程交互中的等待通知模式,其实你可以看完之后,自己再写写其他实现方式。

测试锁的释放情况

测试 wait / notify 释放锁情况

final Object lock = new Object();    boolean waiting = true;    class WaitThread extends Thread {        @Override        public void run() {            synchronized (lock) {                System.out.println("current time : " + System.currentTimeMillis() + " ; wait thread hold lock : " + Thread.holdsLock(lock));                while (waiting) {                    try {                        System.out.println("begin wait ...." + "current time : " + System.currentTimeMillis());                        lock.wait();                        System.out.println("current time : " + System.currentTimeMillis() + " ; wait thread hold lock : " + Thread.holdsLock(lock));                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }    }    class NotifyThread implements Runnable {        @Override        public void run() {            try {                Thread.sleep(10);            } catch (InterruptedException e) {                e.printStackTrace();            }            synchronized (lock) {                System.out.println("current time : " + System.currentTimeMillis() + " ;  notify thread hold lock : " + Thread.holdsLock(lock));                if (waiting) {                    waiting = false;                    lock.notify();                }                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println("current time : " + System.currentTimeMillis() + " ;  notify thread hold lock : " + Thread.holdsLock(lock));            }        }    }    @Test    public void testWaitNotifyLock() throws Exception {        new WaitThread().start();        new Thread(new NotifyThread()).start();        Thread.sleep(3000);    }

输出:

current time : 1574852090614 ; wait thread hold lock : truebegin wait ....current time : 1574852090614current time : 1574852090627 ;  notify thread hold lock : truecurrent time : 1574852090729 ;  notify thread hold lock : truecurrent time : 1574852090729 ; wait thread hold lock : true

从时间上来看, wait 线程先获得锁,之后进入等待过程,调用 wait 方法;此时 wait 线程还没执行完,这时 notify 线程获取了锁,并执行完成,说明在 wait 之后,notify 线程获取到了 lock ,说明 wait 方法调用之后,锁被释放掉了, notify 线程才能获取到锁。当 notify 线程执行完成之后, wait 线程又重新获得了锁,继续执行。

测试 sleep 方法获取释放锁情况

@Test    public void testSleepLock() throws Exception {        Runnable r1 = () -> {            synchronized (sleepLock) {                System.out.println("r1 begin current time : " + System.currentTimeMillis());                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println("r1 end  current time : " + System.currentTimeMillis());            }        };        Runnable r2 = () -> {            //让r1先获取到锁            try {                Thread.sleep(20);            } catch (InterruptedException e) {                e.printStackTrace();            }            synchronized (sleepLock) {                System.out.println("r2 current time : " + System.currentTimeMillis());            }        };        new Thread(r1).start();        new Thread(r2).start();        Thread.sleep(3000);    }

输出:

r1 begin current time : 1574855304815r1 end  current time : 1574855305819r2 current time : 1574855305819

我们让存在 sleep 的线程 r1 先获取到锁,然后r1进入一个长时间的 sleep ,可以看到在这个时间内,r2 并没有获取到锁,而是 r1 执行完之后,r2 才获取到锁。

测试 yield 方法获取释放锁情况

在上面的基础上,我们已经证明了 sleep 不会释放线程拥有的锁,然后我们改改上面例子,测试下 yield 方法会不会释放锁:

@Test   public void testYieldLock() throws Exception {       Runnable r1 = () -> {           synchronized (sleepLock) {               System.out.println("r1 begin current time : " + System.currentTimeMillis());               Thread.yield();               try {                   Thread.sleep(800);               } catch (InterruptedException e) {                   e.printStackTrace();               }               System.out.println("r1 end  current time : " + System.currentTimeMillis());           }       };       Runnable r2 = () -> {           //让r1先获取到锁           try {               Thread.sleep(20);           } catch (InterruptedException e) {               e.printStackTrace();           }           synchronized (sleepLock) {               System.out.println("r2 current time : " + System.currentTimeMillis());           }       };       new Thread(r1).start();       new Thread(r2).start();       Thread.sleep(2000);   }

输出:

r1 begin current time : 1574855591635r1 end  current time : 1574855592437r2 current time : 1574855592437

可以看到 r1 获取锁之后,就一直占用,直到同步块结束。


本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

阅读全文: http://gitbook.cn/gitchat/activity/5ddde8a981c08a49d99654bf

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App , GitChat 专享技术内容哦。

小例子带你入门多线程相关推荐

  1. 一个有趣的小例子,带你入门协程模块-asyncio

    上篇文章写了关于yield from的用法,简单的了解异步模式,[上次的内容链接]这次让我们通过一个有趣例子带大家了解asyncio基本使用. 目标效果图 在控制台中显示一个由ASCII字符" ...

  2. 用几个最简单的例子带你入门 Python 爬虫

    作者 | ZackSock 来源 | 新建文件夹X(ID:ZackSock) 头图 | CSDN下载自视觉中国 前言 爬虫一直是Python的一大应用场景,差不多每门语言都可以写爬虫,但是程序员们却独 ...

  3. 图解爬虫,用几个最简单的例子带你入门Python爬虫

    一.前言 爬虫一直是Python的一大应用场景,差不多每门语言都可以写爬虫,但是程序员们却独爱Python.之所以偏爱Python就是因为她简洁的语法,我们使用Python可以很简单的写出一个爬虫程序 ...

  4. 新手如何学习c语言? 小马带你入门

    大家好 本人是虽仍是一名代码小白 但也有一年c语言学习的经验 所以在这里我将把我的所有学习心得与大家共同分享 一同进步 文章全部由本人原创如有错误 请大家及时告诉小马 小马定会及时更正,和大家一同进步 ...

  5. python中self_一个例子带你入门Python装饰器

    ============ 欢迎关注我的公众号:早起python ============ 前言 在还未正式发布的python3.9中,有一个新功能值得关注,那就是任意表达式可以作为装饰器,如果你还不知 ...

  6. 【Python爬虫系列教程 28-100】小姐姐带你入门爬虫框架Scrapy、 使用Scrapy框架爬取糗事百科段子

    文章目录 Scrapy快速入门 安装和文档: 快速入门: 创建项目: 目录结构介绍: Scrapy框架架构 Scrapy框架介绍: Scrapy框架模块功能: Scrapy Shell 打开Scrap ...

  7. 用Python自动化管理邮件简直太方便了,三个实用小例子带你体会!

    ‍‍ 大家好,我是早起. 在之前的文章中我们已经了解如何对自己的邮箱做一些代码操作前的基础配置,以及通过 Python 代码收发.读取邮件.本文将简单复习之前的部分重点内容,并通过 3 个小案例进一步 ...

  8. 小例子带你深撅直播流技术

    欢迎和号主[前端点线面]进群盘算法,此外本号干货满满:14个门类(100+篇原创)内容(又干又硬).<前端百题斩>pdf(助力薪资double).20+篇思维导图(知识系统化.记忆简单化) ...

  9. python open函数_精选2个小例子,带你快速入门Python文件处理

    阅读本文大概需要7分钟讲完了函数和模块,我们来讲一讲文件的使用,python对数据的处理分两种一种是本地文件的处理,另外一种是通过网络数据处理(也就是爬虫相关的).而本地的数据处理,主要是通过文件的读 ...

最新文章

  1. Turing渲染着色器网格技术分析
  2. 算法----删除链表中的节点(Java)
  3. oracle db-link 分布式数据库网络配置协议错误,Oracle学习(18)【DBA向】:分布式数据库...
  4. 粒度过粗_这些书帮助我度过了第一次成为技术主管的经历
  5. 大数据自学1-CentOS 下安装CDH及Cloudera Manager
  6. mysql 高并发 卡死,高并发中的卡死状态 -HashMap
  7. 大组合数:Lucas定理
  8. 在龙芯机器上编译OpenJDK8
  9. FFmpeg —— MP4文件提取h264文件
  10. 认知之经济学:经济是如何运行的
  11. 【5G系列】MAC (Medium Access Control)协议详解
  12. 锂电池容量下降怎么办?锂电池容量下降修复方法
  13. 批量更改PPT文件中的内容(修改大量PPT)
  14. 2022-2027年中国石油装备制造市场竞争态势及行业投资前景预测报告
  15. 对接京东平台的第一篇
  16. PSRAM/SRAM与XMC硬件连接的推荐方法
  17. 游戏公司盯上了区块链:是机会,还只是一场游戏?
  18. 最新无限制188旅游系统V7.0升级补丁 全社通V2.0补丁 Tourex 5.0源码下载
  19. Redis笔记(Linux部署redis过程)
  20. JavaScript日期时间详解

热门文章

  1. 动力电池编码_工信部就动力电池规格尺寸及编码国家标准征求意见
  2. 【练习册】 2015-08-09 Dlist Queue Stack by python
  3. 【华为OD机试】1049 - 挑7
  4. 氨基修饰花粉状二氧化硅纳米球 NH2-Pollen silica nanospheres(羧基 COOH/PEG/蛋白 Protein/抗体 antibody修饰的花粉状二氧化硅纳米球)
  5. 史上最新最全的M1 Air(2020 macbook air)配置homebrew步骤
  6. 顺序表(SeqList)
  7. 将夜神模拟器中的文件拷贝至电脑
  8. 微信获取openid方法
  9. c0000021a 错误和asms的处理
  10. 背单词打卡c语言程序,坚持背单词打卡107天的感悟