当前位置:网站首页>5 LockSupport与线程中断
5 LockSupport与线程中断
2022-06-13 11:48:00 【乘风会落雨】
5 LockSupport与线程中断
目录
线程中断机制
阿里蚂蚁金服面试题
void | interrupt() | 中断此线程 |
---|---|---|
static boolean | interrupted() | 测试当前线程是否已被中断 |
boolean | isInterrupted() | 测试此线程是否已经被中断 |
三个方法了解过吗?用在哪?
如何停止一个运行中的线程?
如何中断一个运行中的线程??
什么是中断机制?
- 首先
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
- 其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制——中断。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
- eg.顾客在无烟餐厅中吸烟,服务员希望他别吸烟了,不是强行停止他吸烟,而是给他的标志位打为true,具体的停止吸烟还是要顾客自己停止。(体现了协商机制)
中断的相关API方法之三大方法说明
public void interrupt() | 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程 |
---|---|
public static boolean interrupted() | 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态这个方法做了两件事:1 返回当前线程的中断状态2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。) |
public boolean isInterrupted() | 实例方法,判断当前线程是否被中断(通过检查中断标志位) |
大厂面试题:如何使用中断标识停止线程?
1如何停止中断运行中的线程?
① 通过一个volatile变量实现
- volatile保证了可见性,t2修改了标志位后能马上被t1看到
public class interruptDemo {
static volatile boolean isStop = false;
public static void main(String[] args) {
new Thread(()->{
while(true){
if(isStop){
//如果这个标志位被其他线程改为true了
System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序终止");
break;
}
System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
}
},"t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {
e.printStackTrace();}
new Thread(()->{
isStop = true;
},"t2").start();
}
}
//--
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 isStop被修改为true,程序终止
② 通过AtomicBoolean(原子布尔型)
public class interruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
m1_volatile();
}
public static void m1_volatile() {
new Thread(()->{
while(true){
if(atomicBoolean.get()){
//如果这个标志位被其他线程改为true了
System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序终止");
break;
}
System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
}
},"t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {
e.printStackTrace();}
new Thread(()->{
atomicBoolean.set(true);
},"t2").start();
}
}
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 isStop被修改为true,程序终止
③ 通过Thread类自带的中断api方法实现
//默认的中断标志位是false,然后被改为了true
public static void main(String[] args) {
m1_volatile();
}
public static void m1_volatile() {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
//一旦发现中断标志位被修改
System.out.println(Thread.currentThread().getName() + "\t isInterrupted()被修改为true,程序终止");
break;
}
System.out.println("t1 ------hello interrupt ");//----------------------如果没停止,那就一直打印
}
}, "t1");
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {
e.printStackTrace();}
new Thread(()->{
t1.interrupt();//把t1中断
},"t2").start();
}
}
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 ------hello interrupt
//t1 isInterrupted()被修改为true,程序终止
—API源码分析
实例方法interrupt(),没有返回值
//Thread.java
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag----调用了interrupt0()方法
b.interrupt(this);
return;
}
}
interrupt0();
}
//Thread.java
/* Some private helper methods */
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0(); //---------------------------调用了c底层
private native void setNativeName(String name);
实例方法isInterrupted,返回布尔值
//Thread.java
public boolean isInterrupted() {
return isInterrupted(false);
}
//Thread.java
private native boolean isInterrupted(boolean ClearInterrupted);//也调用了c底层
说明
具体来说,当对一个线程,调用 interrupt() 时:
如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的
interrupt
方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。(中断不活动的线程不会产生任何影响,看下面案例)
2 当前线程的中断标识为true,是不是线程就立刻停止?
否
- 仅仅设置了一个中断状态
看看中断是否会立即停止这个300的线程
- 否,虽然中断标志位变了。但是i一直在循环
public class InterruptDemo02 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i = 0;i < 300;i ++){
System.out.println("---------" + i);
}
System.out.println("after t1.interrupt()---第2次----"+Thread.currentThread().isInterrupted());
},"t1");
t1.start();
System.out.println("before t1.interrupt()----"+t1.isInterrupted());
t1.interrupt();
try {
TimeUnit.MILLISECONDS.sleep(3);} catch (InterruptedException e) {
e.printStackTrace();}
System.out.println("after t1.interrupt()---第1次---"+t1.isInterrupted());
try {
TimeUnit.MILLISECONDS.sleep(3000);} catch (InterruptedException e) {
e.printStackTrace();}
System.out.println("after t1.interrupt()---第3次---"+t1.isInterrupted());
}
}
//before t1.interrupt()----false
//---------0
//---------1
//---------2
//---------3
//....
//---------136
//after t1.interrupt()---第1次---true ------此处中断标志位设置为了true,但是t1仍然在运行
//---------137
//---------298
//---------299
//after t1.interrupt()---第2次----true
//after t1.interrupt()---第3次---false//中断不活动的线程不会产生任何影响,线程结束后应该是自动变为了false
后手案例-深入
在我们基本中断程序的骨架上 + 一个sleep阻塞
中断异常 且 会导致程序无限循环.
public class InterruptDemo03 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"\t"+
"中断标志位:"+Thread.currentThread().isInterrupted()+"程序终止");
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
// Thread.currentThread().interrupt(); 假如加了这个,程序可以终止,只会爆异常
}
System.out.println("-----hello InterruptDemo03");
}
},"t1");
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(1);} catch (InterruptedException e) {
e.printStackTrace();}
new Thread(() -> t1.interrupt()).start();
}
}
//爆异常了,并且程序一直在跑
//java.lang.InterruptedException: sleep interrupted
// at java.lang.Thread.sleep(Native Method)
//-----hello InterruptDemo03
//-----hello InterruptDemo03
//-----hello InterruptDemo03
//......
//----------------------------
//---------加了Thread.currentThread().interrupt();
//java.lang.InterruptedException: sleep interrupted
// at java.lang.Thread.sleep(Native Method)
// at com.zhang.admin.controller.InterruptDemo03.lambda$main$0(InterruptDemo03.java:15)
// at java.lang.Thread.run(Thread.java:748)
//-----hello InterruptDemo03
//t1 中断标志位:true程序终止
前文
- ② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的
interrupt
方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。
- ② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的
/** * 1 中断标志位 默认是false * 2 t2 ----->t1发出了中断协商,t2调用t1.interrupt(),中断标志位true * 3 中断标志位true,正常情况下,程序停止,^-^ * 4 中断标志位true,异常情况下,InterruptedException,将会把中断状态清除,并且将收到InterruptedException。中断标志位false导致无限循环。 * * 5 在catch块中,需要再次给中断标志位设置为true,2次调用停止 */
sleep方法抛出InterruptedException后,中断标识也被清空置为false,我们在catch没有通过th.interrupt()方法再次将中断标志设置为true,这就导致无限循环了
小总结
- 中断只是一种协同机制,修改中断标识位仅此而已,而不是立刻stop打断
3 静态方法Thread.interrupted(),谈谈你的理解
api里的第二个
- public static boolean interrupted()
- 静态方法,
Thread.interrupted();
判断线程是否被中断,并清除当前中断状态这个方法做了两件事:1 返回当前线程的中断状态2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。) |
public class InterruptDemo04 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println("-----1");
Thread.currentThread().interrupt();//中断标志位设置为true
System.out.println("-----2");
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
}
}
//main false
//main false
//-----1
//-----2
//main true
//main false
看下源码,
interrupted()
对比isInterrupted()
public static boolean interrupted() { return currentThread().isInterrupted(true); } private native boolean isInterrupted(boolean ClearInterrupted);
public boolean isInterrupted() { return isInterrupted(false); } private native boolean isInterrupted(boolean ClearInterrupted);
他们在底层都调用了native方法isInterrupted。
只不过传入参数ClearInterrupted一个传参传了
true
,一个传了false
。静态方法
interrupted()
中true
表示清空当前中断状态。实例方法
isInterrupted
则不会。
LockSupport是什么
官方解释:用于创建锁和其他同步类的基本线程阻塞原语。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyfrOAqO-1654754693266)(image/image_SOr3KJQSy_.png)]
核心就是
park()
和unpark()
方法park()
方法是阻塞线程unpark()
方法是解除阻塞线程
线程等待唤醒机制
3种让线程等待和唤醒的方法
使用Object中的
wait()
方法让线程等待,使用Object中的notify()
方法唤醒线程使用JUC包中
Condition
的await()
方法让线程等待,使用signal()
方法唤醒线程LockSupport
类可以阻塞当前线程以及唤醒指定被阻塞的线程
①Object类中的wait和notify方法实现线程等待和唤醒
正常
public class LockSupportDemo { public static void main(String[] args) { Object objectLock = new Object(); new Thread(() -> { synchronized (objectLock) { System.out.println(Thread.currentThread().getName()+"\t ---- come in"); try { objectLock.wait();//----------------------这里先让他等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了"); },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { synchronized (objectLock) { objectLock.notify();//-------------------------再唤醒它 System.out.println(Thread.currentThread().getName()+"\t ---发出通知"); } },"t2").start(); } } //t1 ---- come in //t2 ---发出通知 //t1 ---被唤醒了
异常1—去掉synchronized
- 说明要使用
wait
和notify
必须加synchronized
public class LockSupportDemo { public static void main(String[] args) { Object objectLock = new Object(); new Thread(() -> { // synchronized (objectLock) { System.out.println(Thread.currentThread().getName()+"\t ---- come in"); try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } // } System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了"); },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { // synchronized (objectLock) { objectLock.notify(); System.out.println(Thread.currentThread().getName()+"\t ---发出通知"); // } },"t2").start(); } } //t1 ---- come in //Exception in thread "t1" java.lang.IllegalMonitorStateException // at java.lang.Object.wait(Native Method) // at java.lang.Object.wait(Object.java:502) // at com.zhang.admin.controller.LockSupportDemo.lambda$main$0(LockSupportDemo.java:15) // at java.lang.Thread.run(Thread.java:748)
- 说明要使用
异常2—把notify和wait的执行顺序对换
- 说明顺序不能对换
public class LockSupportDemo { public static void main(String[] args) { Object objectLock = new Object(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectLock) { System.out.println(Thread.currentThread().getName()+"\t ---- come in"); try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了"); },"t1").start(); new Thread(() -> { synchronized (objectLock) { objectLock.notify();//这个先执行了 System.out.println(Thread.currentThread().getName()+"\t ---发出通知"); } },"t2").start(); } } //一直处于循环中
小总结
wait和notify方法必须要在同步块或者方法里面,且成对出现使用
先wait后notify才OK,顺序
②Condition接口中的await后signal方法实现线程的等待和唤醒
正常
public class LockSupportDemo { public static void main(String[] args) { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t-----come in"); condition.await(); System.out.println(Thread.currentThread().getName()+"\t -----被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { lock.lock(); try { condition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } System.out.println(Thread.currentThread().getName()+"\t"+"我要进行唤醒"); },"t2").start(); } } //t1 -----come in //t2 我要进行唤醒 //t1 -----被唤醒
异常1原理同上
- 仍然返回
IllegalMonitorStateException
- 仍然返回
异常2 原理同上
- 仍然在不停的循环
小总结
await
和notify
类似于上面wait
和notify
Condition中的线程等待和唤醒方法,需要先获取锁
一定要先await后signal,不能反了
Object和Condition使用的限制条件
总结
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒
③LockSupport类中的park等待和unpark唤醒
是什么
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
官网解释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-za4dJOuv-1654754693269)(image/image_E5vM4GxxSB.png)]
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
permit(许可)只有两个值1和0,默认是0。0 是阻塞,1是唤醒
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
许可(Permit)
主要方法
API
阻塞
park()/park(Object blocker)
调用
LockSupport.park()
时,发现它调用了unsafe类
,并且默认传了一个0public static void park() { UNSAFE.park(false, 0L); }
- permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,
然后会将permit再次设置为零并返回。
唤醒
调用
LockSupport.unpark();
时,也调用了unsafe类
public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread); }
- 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
代码
正常+无锁块要求
public class LockSupportDemo { public static void main(String[] args) { Thread t1 = new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t----------come in"); LockSupport.park(); System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"); },"t1"); t1.start(); new Thread(()->{ LockSupport.unpark(t1); System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1"); },"t2").start(); } } //t1 ----------come in //t2 -----发出通知,去唤醒t1 //t1 ----------被唤醒了
之前错误的先唤醒后等待,LockSupport照样支持
public class LockSupportDemo { public static void main(String[] args) { Thread t1 = new Thread(()->{ try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"\t----------come in"+"\t"+System.currentTimeMillis()); LockSupport.park(); System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"+"\t"+System.currentTimeMillis()); },"t1"); t1.start(); new Thread(()->{ LockSupport.unpark(t1); System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1"); },"t2").start(); } } //t2 -----发出通知,去唤醒t1 //t1 ----------come in 1654750785663 //t1 ----------被唤醒了 1654750785663
sleep方法3秒后醒来,执行park无效,没有阻塞效果,解释如下。先执行了unpark(t1)导致上面的park方法形同虚设无效,时间是一样的
- 类似于高速公路的ETC,提前买好了通行证unpark,到闸机处直接抬起栏杆放行了,没有park拦截了。成双成对要牢记
最后
许可证是只要一个的
public class LockSupportDemo { public static void main(String[] args) { Thread t1 = new Thread(()->{ try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"\t----------come in"+"\t"+System.currentTimeMillis()); LockSupport.park(); LockSupport.park(); System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"+"\t"+System.currentTimeMillis()); },"t1"); t1.start(); new Thread(()->{ LockSupport.unpark(t1); LockSupport.unpark(t1); System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1"); },"t2").start(); } } //t2 -----发出通知,去唤醒t1 //t1 ----------come in 1654750970677--------------------卡在这里了
小总结
Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。
Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。归根结
底, Lock Support调用的Unsafe中的native代码。Lock Support提供
park()
和unpark()
方法实现阻塞线程和解除线程阻塞的过程
Lock Support和每个使用它的线程都有一个许可(permit) 关联。
每个线程都有一个相关的permit, permit最多只有一个, 重复调用un park也不会积累凭证。形象的理解
线程阻塞需要消耗凭证(permit) , 这个凭证最多只有1个。
当调用方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
而****则相反, 它会增加一个凭证, 但凭证最多只能有1个, 累加无效。
面试题
为什么可以突破wait/notify的原有调用顺序?
因为un park获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。
先发放了凭证后续可以畅通无阻。为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1, 连续调用两次un park和调用一次un park效果一样, 只会增加一个凭证;
而调用两次park却需要消费两个凭证, 证不够, 不能放行。
边栏推荐
- Miidock file distribution
- Projet de développement web, développement d'une page Web
- Based on vue+nest Js+mysql cross platform open source community operation management system
- break algorithm---dynamic planning(dp-arr)
- break algorithm---multi-interface
- Performance monster on arm64: installation and performance test of API gateway Apache APIs IX on AWS graviton3
- web开发视频教程,web开发教学
- Performance monster on arm64: installation and performance test of API gateway Apache APIs IX on AWS graviton3
- 『忘了再學』Shell基礎 — 30、sed命令的使用
- 2022.2:EyeshotPro EyeshotUltimate EyeshotFem
猜你喜欢
数据库连接数设置多少合适?
书籍+视频+学习笔记+技能提升资源库,面试必问
【TcaplusDB知识库】TcaplusDB-tcapsvrmgr工具介绍(三)
【TcaplusDB知识库】TcaplusDB表数据缓写介绍
轻量级实时语义分割:ENet & ERFNet
如何使用 DATAX 以 UPSERT 语义更新下游 ORACLE 数据库中的数据?
【真相】大厂招人不怕花钱的原因竟然是。。。
基于STM32F103——矩阵按键+串口打印
The most complete network, including interview questions + Answers
Seccloud and trend technology jointly release the overall solution of container cloud platform and GPU resource pooling
随机推荐
自己炒股怎么开户?安全可靠吗?
【TcaplusDB知识库】TcaplusDB-tcapsvrmgr工具介绍(三)
数据库连接数设置多少合适?
Seccloud and trend technology jointly release the overall solution of container cloud platform and GPU resource pooling
(Yousheng small message-04) how to use mobile WPS for electronic signature on PDF
[tcapulusdb knowledge base] tcapulusdb operation and maintenance doc introduction
Docker dockerfile installation mysql5.7
智能客服系统框架rasa
树莓派开发笔记(十六):树莓派4B+安装mariadb数据库(mysql开源分支)并测试基本操作
基于STM32F103——SIM900A发送短信+串口打印
[SQL statement basics] - select (supplement to single table query sequence)
The leader said he would go online tomorrow, but he didn't know the development process at all
TS advanced keyof
web开发视频教程,web开发教学
Kubernetes deploying ActiveMQ
Auto. JS floating window centered
Committed to R & D and manufacturing of ultra surface photonic chip products, Shanhe optoelectronics completed a round of pre-A financing of tens of millions of yuan
[tcapulusdb knowledge base] Introduction to tcapulusdb tcapsvrmgr tool (II)
TS进阶之keyof
[tcapulusdb knowledge base] Introduction to tmonitor background one click installation (II)