(接上文《线程基础:JDK1.5+(8)——线程新特性(上)》)

3、工作在多线程环境下的“计数器”:

从这个小节开始,我们将以一个“赛跑”的例子,讲解JDK1.5环境下一些线程控制工具(包括Semaphore、CountDownLatch和java.util.concurrent.atomic子包),并且复习这个专题讲到的知识点:同步快、锁、线程池、BlockingQueue、Callable等。

3-1、 赛跑比赛的需求

现在您不仅可以通过我们已经介绍的知识点,实现对100米田径比赛的初赛和决赛的模拟,然后公布出比赛的冠亚季军。还可以基于这些知识,模拟机场T1和T2跑道的起降工作。这里我们一起实现前者的需求,首先来看看100米田径比赛的业务需求:

  1. 选手要参加比赛,首先就要报名。为了使功能足够简单,参赛选手的基本只包括:姓名、起跑指数(最低速度)、参赛号三个信息。

  2. 同一个选手的状态不稳定性。也就是说某一个选手,在初赛阶段的速度可能是A,但是决赛阶段由于发挥失常,可能速度就变成了B。而这一切都是随机进行的

  3. 选手们首先进行“初赛”,所有选手的“初赛”成绩将进行汇总。成绩最好的5名选手,将参加“决赛”。“决赛”成绩最好的三名选手,将分别获得冠亚季军,并公布出来。

  4. 比赛场地只有一个,总共有5条跑道可供使用。所以无论是“初赛”还是“决赛”,同一时间参加比赛的选手都不能超过5名。

3-1-1、基本类:Player选手类

本小节后续的内容中,我们将对跑步比赛的实现代码进行多次更改优化,但是无论实现代码如何变化,有几个基本的模型是不会变化的:选手描述和比赛结果描述。

选手除了名字、参赛编号的描述外,还有一个“最低速度”的描述,这是为了保证无论这个选手跑多少次,其状态都不会太过失常。“最低速度”是在创建选手时,系统随机生成的。

以下是Player选手类的定义代码:

package test.thread.track;import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;/*** 这就是一个选手。* 为了简单起见,我们只记录这个选手名字、选手编号、最低速度(创建时随机生成)。* 当然,最为一名选手,最重要的工作就是“跑步”* @author yinwenjie*/
public class Player implements Callable<Result> , Comparable<Player>{/*** 选手编号*/private int number;/*** 选手的名字*/private String name;/*** 最低速度*/private float minSpeed;/*** 本次比赛结果*/private Result result;/*** 跑道*/private Semaphore runway;public Player(String name , int number , Semaphore runway) {this.name = name;this.number = number;this.runway = runway;// 这个最低速度设置是 8米/秒(否则就真是‘龟速’了)this.minSpeed = 8f;}/* (non-Javadoc)* @see java.util.concurrent.Callable#call()*/@Overridepublic Result call() throws Exception {try {// 申请上跑道this.runway.acquire();return this.doRun();} catch(Exception e) {e.printStackTrace(System.out);} finally {// 都要进入初赛结果排序(中途退赛的成绩就为0)this.runway.release();}// 如果执行到这里,说明异常发生了this.result = new Result(Float.MAX_VALUE);return result;}/*** 开始跑步* @return* @throws Exception*/private Result doRun()  throws Exception {/** 为了表现一个选手每一次跑步都有不同的状态(但是都不会低于其最低状态),* 所以每一次跑步,系统都会为这个选手分配一个即时速度。* * 这个即时速度不会低于其最小速度,但是也不会高于 14米/秒(否则就是‘超人’咯)* */// 生成即时速度float presentSpeed = 0f;presentSpeed = this.minSpeed * (1.0f + new Random().nextFloat());if(presentSpeed > 14f) {presentSpeed = 14f;}// 计算跑步结果(BigDecimal的使用可自行查阅资料)BigDecimal calculation =  new BigDecimal(100).divide(new BigDecimal(presentSpeed) , 3, RoundingMode.HALF_UP);float presentTime = calculation.floatValue();// 让线程等待presentSpeed的时间,模拟该选手跑步的过程synchronized (this) {this.wait((long)(presentTime * 1000f));}// 返回跑步结果this.result = new Result(presentTime);return result;}/*** @return the result*/public Result getResult() {return result;}/*** @return the number*/public int getNumber() {return number;}/*** @return the name*/public String getName() {return name;}/* (non-Javadoc)* @see java.lang.Comparable#compareTo(java.lang.Object)*/@Overridepublic int compareTo(Player o) {/** 两个选手间,还可以通过他们的result进行比较* 耗时越小,当然越靠前* */Result myReslut = this.getResult();Result targetReslut = o.getResult();// 如果出现了reslut为null或者targetReslut为null,说明比赛结果出现了问题// 当然如果真的出现这样的问题,最可能的选手中途退赛了if(myReslut == null) {return 1;}if(targetReslut == null) {return -1;}// 耗时越少的选手,当然应该排在“成绩”队列的越前面if(myReslut.getTime() < targetReslut.getTime()) {return -1;} else {return 1;}}
}

为什么Player选手类要实现Comparable接口呢?在实现代码中,我将使用PriorityBlockingQueue队列,将选手依据其比赛成绩进行排序。为了能够保证PriorityBlockingQueue队列能够正常排序,所以需要实现该接口。

当然有的读者会说,实现Comparable接口后,使用普通的List也可以排序。但是List接口的实现类(ArrayList、LinkedList、Vector等等)并不是线程安全的,它们常用的处理场景还是在某一个线程内进行数据线性化处理时使用。

而就目前我们的场景来看,程序员根本就不知道某一个选手什么时候能够跑完100米,并且多个选手跑步的处理结果都将随机的送入队列。所以保证线程安全性是需求实现中重要的一部分

当然,如果您硬是要使用传统的List也行。能可以通过JDK提供的“同步包装器”(Collections.synchronizedList)将它变成线程安全的。但这个问题不是本小节讨论的范围。

另外,做为一个选手来说,最根本的功能就是“跑”这个动作。并且根据需求,很明显我们需要在选手“跑完后”知道“跑”的成绩。所以我们还需要Player类实现Callable接口,以便让选手能够跑起来。

为了模拟跑的过程和选手的状态有关,代码中使用随机数确定本次选手“跑”的速度。但是这个速度不会低于选手的“最低速度”(目前给定的是14秒)。

3-1-2、基本类:Result比赛结果

另外一个不会变动的基本类就是Result成绩:

package test.thread.track;/*** 选手某一次跑步的成绩* @author yinwenjie**/
public class Result {/*** 记录了本次赛跑的用时情况*/private float time;public Result(float time) {this.time = time;}/*** @return the time*/public float getTime() {return time;}/*** @param time the time to set*/public void setTime(float time) {this.time = time;}
}

每一次选手“跑”的成绩都是不一样的。成绩中只包括一个属性,就是跑完100米的用时情况。

3-2、Semaphore:信号量

3-2-1、基本使用

Semaphore信号量,是concurrent包的一个重要工具类,它通过申请和回收“证书”,实现多个线程对同一资源的访问控制。具体的做法是,某个线程在访问某个(可能出现资源抢占的)资源的时候,首先向Semaphore对象申请“证书”,如果没有拿到“证书”就一直阻塞;当拿到“证书”后,线程就解除阻塞状态,然后访问资源;在完成资源操作后,再向Semaphore对象归还“证书”;让我们先来看看Semaphore信号的简单示例:

package test.thread.semaphore;import java.util.concurrent.Semaphore;public class SemaphoreTest {public static void main(String[] args) throws Throwable {new SemaphoreTest().doTest();}public void doTest() {Semaphore semp = new Semaphore(5 , false);// 我们创建10个线程,并通过0-9的index进行编号for(int index = 0 ; index < 10 ; index++) {Thread semaphoreThread = new Thread(new SemaphoreRunnableNonfair(semp , index));semaphoreThread.start();}}/*** 测试Semaphore的非公平模式* @author yinwenjie*/private static class SemaphoreRunnableNonfair implements Runnable {private Semaphore semp;/*** 编号*/private Integer index;public SemaphoreRunnableNonfair(Semaphore semp , Integer index) {this.semp = semp;this.index = index;}@Overridepublic void run() {try {System.out.println("线程" + this.index + "等待信号。。。。。。");this.semp.acquire();// 停止一段时间,模拟业务处理过程synchronized(this) {System.out.println("index 为 " + this.index + " 的线程,获得信号,开始处理业务");this.wait(5000);}} catch (InterruptedException e) {e.printStackTrace(System.out);} finally {// 最后都要释放这个信号/证书this.semp.release();}}}
}

以上代码我们创建了10个线程。分别编号为0-9(这里我们没有使用Thread自带的id,主要还是为了读者能够看得清楚)。Semaphore信号量对象中,我们放置了5个“证书”,也就是说最多同时可以有5个线程进行业务处理,处理完成后向线程向Semaphore信号对象归还“证书”。以上代码的处理结果,可能如下图所示(注意,是“可能”):

线程0等待信号。。。。。。
线程2等待信号。。。。。。
index 为 2 的线程,获得信号,开始处理业务
index 为 0 的线程,获得信号,开始处理业务
线程3等待信号。。。。。。
index 为 3 的线程,获得信号,开始处理业务
线程4等待信号。。。。。。
index 为 4 的线程,获得信号,开始处理业务
线程5等待信号。。。。。。
index 为 5 的线程,获得信号,开始处理业务
线程7等待信号。。。。。。
线程8等待信号。。。。。。
线程6等待信号。。。。。。
线程9等待信号。。。。。。
线程1等待信号。。。。。。
index 为 8 的线程,获得信号,开始处理业务
index 为 7 的线程,获得信号,开始处理业务
index 为 6 的线程,获得信号,开始处理业务
index 为 9 的线程,获得信号,开始处理业务
index 为 1 的线程,获得信号,开始处理业务

3-2-2、Semaphore的基本操作方式

为了方便读者查阅,这里我们列举了Semaphore中常用的操作方式

  • 申请/获取证书:

    void acquire():从此信号量获取一个许可,在Semaphore能够提供一个许可前,当前线程将一直阻塞等待。如果在等待过程中,当前线程收到了interrupt信号,那么将抛出InterruptedException异常。

    void acquire(permits):从此信号量获取permits个许可,在Semaphore能够提供permits个许可前,当前线程将一直阻塞等待。如果在等待过程中,当前线程收到了interrupt信号,那么将抛出InterruptedException异常。

    void acquireUninterruptibly():从此信号量获取一个许可,在Semaphore能够提供一个许可前,当前线程将一直阻塞等待。使用这个方法获取许可时,不会受到线程interrupt信号的影响。

    void acquireUninterruptibly(permits):从此信号量获取permits个许可,在Semaphore能够提供permits个许可前,当前线程将一直阻塞等待。使用这个方法获取许可时,不会受到线程interrupt信号的影响。

    boolean tryAcquire():从此信号量获取一个许可,如果无法获取,线程并不会阻塞在这里。如果获取到了许可,则返回true,其他情况返回false。

    boolean tryAcquire(permits):从此信号量获取permits个许可,如果无法获取,线程并不会阻塞在这里。如果获取到了许可,则返回true,其他情况返回false。

    boolean tryAcquire(int permits, long timeout, TimeUnit unit):从此信号量获取permits个许可,如果无法获取,则当前线程等待设定的时间。如果超过等待时间后,还是没有拿到许可,则解除等待继续执行。如果获取到了许可,则返回true,其他情况返回false。

  • 证书状态:

    int availablePermits():返回此信号量中当前可用的许可数。

    int getQueueLength():返回正在等待获取的线程的估计数目。该值仅是估计的数字,因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。此方法用于监视系统状态,不用于同步控制。

    boolean hasQueuedThreads():查询是否有线程正在等待获取。注意,因为同时可能发生取消,所以返回 true 并不保证有其他线程等待获取许可。此方法主要用于监视系统状态。

    boolean isFair():如果此信号量的公平设置为 true,则返回 true。

  • 释放/返还证书:

    void release():释放一个许可,将其返回给信号量。最好将这个方法的调用,放置在finally程序块中执行。

    void release(permits):释放给定数目的许可,将其返回到信号量。最好将这个方法的调用,放置在finally程序块中执行。

  • fair:公平与非公平

    Semaphore一共有两个构造函数,分别是:Semaphore(int permits)和Semaphore(int permits, boolean fair);permits是指由Semaphore信号量控制的“证书”数量。fair参数是设置这个信号量对象的工作方式。

当fair参数为true时,信号量将以“公平方式”运行。即首先申请证书,并进入阻塞状态的线程,将有权利首先获取到证书;当fair参数为false时,信号量对象将不会保证“先来先得”。默认情况下,Semaphore采用“非公平”模式运行。

3-2-3、实现比赛场景

在介绍了Semaphore的使用方式后,现在我们就要将Semaphore加入“赛跑比赛”的代码实现中。

很显然Semaphore在我们需求中的应用任务是:给选手使用“跑道”的证书/权利,以便让选手“跑步”,并且在选手使用完跑道后,回收跑道的使用证书/权利,给下一位选手。

......
// 这就是跑道,需求上说了只有5条跑道,所以只有5个permits。
Semaphore runway = new Semaphore(5);
......

这个代码片段控制着所有选手的跑步动作:只有在获得跑道的使用权限后,才能执行“跑步”动作。

3-2-4、关键的一个问题

  • 什么情况下视为“初赛”、“决赛”完成?

    那么最直观的描述就是:所有报名的选手都完成了跑步过程(中途退赛也算),才能算“初赛”完成;“初赛”排名最靠前的前5名选手都完成了跑步过程(中途退赛也算)才算是“决赛”完成。

    如果没有完成“初赛”,那么比赛进程就必须停在那里,直到“初赛”过程完成;如果没有完成“决赛”过程,比赛进程就必须停在那里,知道“决赛”完成:

......
//! 只有当PLAYERNAMES.length位选手的成绩都产生了,才能进入决赛,这很重要
synchronized (this.preliminaries) {while(this.preliminaries.size() < OneTrack.PLAYERNAMES.length) {try {this.preliminaries.wait();} catch(InterruptedException e) {e.printStackTrace(System.out);}}
}
......
//! 只有当5位选手的决赛成绩都产生了,才能到下一步:公布成绩
synchronized (this.finals) {while(this.finals.size() < 5) {try {this.finals.wait();} catch(InterruptedException e) {e.printStackTrace(System.out);}}
}
......
  • 怎么监控某一个选手,是否完成了跑步过程?

在我们定义的Player选手类中,已经实现了Callable接口,并且将会在运行完成后,返回Result结果信息。所以看选手是否完成了跑步过程,只需要监控Player的Future就可以了。

但是监控Player的Future可不能在100米比赛的主线程上进行,否则就会出现上一个选手没有跑完就不能启动下一个选手的跑步线程的情况。所以我们需要为每一个选手都创建一个“监控线程”FutureThread:

/*** 这是计分线程,是为了保证产生比赛结果后,在计入PriorityBlockingQueue* 这样才有排列成绩的依据* @author yinwenjie**/
private class FutureThread extends Thread {/*** 选手跑步任务(Player)的执行状态对象*/private Future<Result> future;/*** 跑步成绩出来后,需要操作的队列* (要将对应的选手加入到队列,以便依据成绩进行排序)*/private PriorityBlockingQueue<Player> achievementQueue;/*** 当前进行跑步的选手*/private Player player;public FutureThread(Future<Result> future , Player player , PriorityBlockingQueue<Player> achievementQueue) {this.future = future;this.player = player;this.achievementQueue = achievementQueue;}/* (non-Javadoc)* @see java.lang.Thread#run()*/@Overridepublic void run() {// 如果条件成立,最有可能的就是选手在比赛过程中,// 由于某种原因退赛了!if(this.future == null) {System.out.println("选手退赛,计分为0");} else {try {// 如果选手没有跑完,FutureThread将阻塞在这里// 当然出现跑步过程中退赛,就会抛出异常this.future.get();} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}// 运行到这里,就说明这个选手跑完了(或者退赛了)// 无论什么情况,都计入队列,然后通知主线程this.achievementQueue.put(this.player);synchronized (this.achievementQueue) {this.achievementQueue.notify();}}
}

这样,每个选手在跑步过程中,就会有两个线程:一个用来跑步的线程:Player-Callable;另一个用来监控跑步情况,并操作成绩队列的线程:FutureThread。

3-3、完整的比赛代码

实现代码中主要的问题都解决了,现在我们可以给出完成的实现代码了(注意,之前已经给出的代码,就不在赘述了):

package test.thread.track;import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.Semaphore;/*** 这是第一个比赛程序。* @author yinwenjie**/
public class OneTrack {private static final String[] PLAYERNAMES = new String[]{"白银圣斗士","黄金圣斗士","青铜圣斗士","神斗士","冥斗士","哈迪斯","龟仙人","孙悟空","孙悟饭","贝吉塔","孙悟天"};/*** 报名队列(非线程安全)*/private List<Player> signupPlayers = new LinkedList<Player>();/*** 初赛结果队列(有排序功能,且线程安全)*/private PriorityBlockingQueue<Player> preliminaries = new PriorityBlockingQueue<Player>();/*** 决赛结果队列(有排序功能,且线程安全)*/private PriorityBlockingQueue<Player> finals = new PriorityBlockingQueue<Player>();public void track() {/** 赛跑分为以下几个阶段进行;* * 1、报名* 2、初赛,10名选手,分成两组,每组5名选手。* 分两次进行初赛(因为场地只有5条赛道,只有拿到进场许可的才能使用赛道,进行比赛)* * 3、决赛:初赛结果将被写入到一个队列中进行排序,只有成绩最好的前五名选手,可以参加决赛。* * 4、决赛结果的前三名将分别作为冠亚季军被公布出来* *///1、================报名// 这就是跑道,需求上说了只有5条跑道,所以只有5个permits。Semaphore runway = new Semaphore(5);this.signupPlayers.clear();for(int index = 0 ; index < OneTrack.PLAYERNAMES.length ; ) {Player player = new Player(OneTrack.PLAYERNAMES[index], ++index , runway);this.signupPlayers.add(player);}//2、================进行初赛// 这是裁判ExecutorService refereeService = Executors.newFixedThreadPool(5);for (final Player player : this.signupPlayers) {Future<Result> future = null;future = refereeService.submit(player);new FutureThread(future, player, this.preliminaries).start();}//! 只有当PLAYERNAMES.length位选手的成绩都产生了,才能进入决赛,这很重要synchronized (this.preliminaries) {while(this.preliminaries.size() < OneTrack.PLAYERNAMES.length) {try {this.preliminaries.wait();} catch(InterruptedException e) {e.printStackTrace(System.out);}}}// 3、============决赛(只有初赛结果的前5名可以参见)for(int index = 0 ; index < 5 ; index++) {Player player = this.preliminaries.poll();Future<Result> future = null;future = refereeService.submit(player);new FutureThread(future, player, this.finals).start();}//! 只有当5位选手的决赛成绩都产生了,才能到下一步:公布成绩synchronized (this.finals) {while(this.finals.size() < 5) {try {this.finals.wait();} catch(InterruptedException e) {e.printStackTrace(System.out);}}}// 4、============公布决赛成绩(前三名)for(int index = 0 ; index < 3 ; index++) {Player player = this.finals.poll();switch (index) {case 0:System.out.println("第一名:"  + player.getName() + "[" + player.getNumber() + "],成绩:" + player.getResult().getTime() + "秒");break;case 1:System.out.println("第二名:"  + player.getName() + "[" + player.getNumber() + "],成绩:" + player.getResult().getTime() + "秒");break;case 2:System.out.println("第三名:"  + player.getName() + "[" + player.getNumber() + "],成绩:" + player.getResult().getTime() + "秒");break;default:break;}}}public static void main(String[] args) throws RuntimeException {new OneTrack().track();}//......这里是FutureThread的代码,上面已给出了
}

以下是可能的执行结果。“可能的执行结果”那是因为结果完全是随机的,您的执行结果可能和我给出的不一样:

第一名:龟仙人[7],成绩:7.143秒
第二名:白银圣斗士[1],成绩:7.477秒
第三名:哈迪斯[6],成绩:7.531秒

(接下文:CountDownLatch同步器、java.util.concurrent.atomic子包)

线程基础:JDK1.5+(9)——线程新特性(中)相关推荐

  1. JDK1.8的接口新特性

    JDK1.8的接口新特性 JDK7及其之前1.接口的变量都是public final static 全局静态常量,无变化.2.接口中都是抽象abstract方法,不能有static方法(因为abstr ...

  2. 【C/C++】C++98基础上的C++11新特性

    一.新语法 1.自动类型推导auto auto的自动推导,用于从初始化表达式中推断出变量的数据类型. //C++98 int a = 10; string s = "abc"; f ...

  3. 【线程基础】多个线程,顺序输出

    [线程基础]多个线程,顺序输出 问题描述 解题思路 代码 问题描述 有三个线程: 一个线程只可以输出:0,3,6,9- 一个线程只可以输出:1,4,7,10- 一个线程只可以输出:2,5,8,11- ...

  4. C++11新特性中的匿名函数Lambda表达式的汇编实现分析(二)

    2019独角兽企业重金招聘Python工程师标准>>> C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一) 首先,让我们来看看以&方式进行变量捕获,同样没有参 ...

  5. 7.Java基础之集合框架+JDK8新特性

    1.集合概述 1.1 为什么学集合 思考:数组有什么缺点? 长度一旦定义,不能改变!定义大了,浪费空间:小了,可能不够 ---->动态的数组 对于增删,需要移动位置 ->有人帮我们做这个事 ...

  6. 总结:JDK1.5-JDK1.8各个新特性

    2019独角兽企业重金招聘Python工程师标准>>> JDK各个版本的新特性 以下介绍一下JDK1.5版本到JDK1.7版本的特性及JDK1.8主要部分特性.仅供参考. JDK1. ...

  7. jdk1.7 1.8新特性

    本文是我学习了解了jdk7和jdk8的一些新特性的一些资料,有兴趣的大家可以浏览下下面的内容. 官方文档:http://www.oracle.com/technetwork/java/javase/j ...

  8. JDK1.8 十大新特性详解

    友情提示:本文将用带注释的简单代码来描述新特性,文字少,但是代码较多 接口的默认方法 Java8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示 ...

  9. Java基础笔记-Java8及其他新特性

    第十六章 Java8及其他新特性 16.1 Java8新特性简介 16.2 lambda表达式和函数式接口 16.3 方法引用与构造器引用 16.4 StreamAPI的使用 16.5 Optiona ...

  10. 菜鸟记录之JDK1.8十大新特性

    一.接口的默认方法 Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下: 代码如下: interface Formula {   ...

最新文章

  1. LeetCode简单题之将每个元素替换为右侧最大元素
  2. 基础练习 Huffuman树 (优先队列)
  3. 2019牛客暑期多校训练营(第七场)D Number(思维)
  4. 上海电力学院计算机学院怎么样,上海电力学院计算机科学与技术学院在职研究生_上海电力学院在职研究生_在职研究生招生信息网...
  5. js保存当前html,JavaScript保存当前页面
  6. 基于物品的相似度还是基于用户的相似度
  7. 3d激光雷达开发(平面分割)
  8. Kaggle 数据清洗挑战 Day 3 - 快速解析日期(date)数据
  9. “无语!只因姓True,苹果封了我的iCloud账户”
  10. ping命令的作用。
  11. rocketmq 顺序消费_RocketMQ核心概念扫盲
  12. java小数正负数据类型_Java - day001 - 8种基本数据类型
  13. visualroute 很棒的一款工具
  14. Word转换PDF:pdf虚拟打印机怎么用操作技巧详解
  15. 显示100以内的所有偶数php,vb100-急需vb编程求100以内所有奇数和及所有偶数和vb编程求100以 爱问知识人...
  16. 阿里云虚拟主机备案期间网站调试
  17. 【生活中的逻辑谬误】止于分析和简化主义
  18. 常见的tenor操作
  19. python读取odb_python - 从.odb文件中提取von mises应力值 - 堆栈内存溢出
  20. java中style的用法

热门文章

  1. kali安装httpie
  2. java 绘制角色_如何设计角色人物?角色人物绘制设计教程
  3. STM32标准库函数之 TIM1定时器产生PWM波
  4. 21、487-3279
  5. SPFA的SLF与LLL优化
  6. python线性插值函数_Numpy一维线性插值函数的用法
  7. E0135 class...没有成员....C2039: Class is not a member of Namespace 非活动预处理块
  8. “老赖”罗永浩被群嘲:莫欺少年穷,莫笑中年败,莫嘲梦想狂
  9. 用html做网站古诗春思,《春思》-五言古诗
  10. SAP Marketing Cloud 功能概述(一)