JUC高级二: Java锁(上篇)
创始人
2025-05-30 23:24:50

JUC高级二: Java锁(上篇)

1. 乐观锁和悲观锁

synchronized关键字和Lock的实现类都是悲观锁

  • 悲观锁: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
    • 适合写操作多的场景,先加锁可以保证写操作时数据正确
    • 显式的锁定之后在操作同步资源
  • 乐观锁:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
    • 乐观锁在Java中是通过使用无锁编程来实现最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
    • 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
    • 乐观锁一般有两种实现方式:
      • 采用版本号机制
      • CAS(Compare-and-Swap,即比较并替换)算法实现

2. 8锁案例

2.1 标准访问有ab两个线程,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public  synchronized void sendEmail(){System.out.println("-------sendEmail");}public synchronized void sendSMS(){System.out.println("-------sendSMS");}}/*** 标准访问有ab两个线程,请问先打印邮件还是短信* 结果:* -------sendEmail* -------sendSMS*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{phone.sendSMS();},"b").start();}
}

执行结果:

image-20230318161237581

2.2 sendEmail方法暂停3秒钟,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public  synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public synchronized void sendSMS(){System.out.println("-------sendSMS");}}/*** sendEmail方法暂停3秒钟,请问先打印邮件还是短信* 结果:* -------sendEmail* -------sendSMS*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{phone.sendSMS();},"b").start();}
}

执行结果:

image-20230318162514132

2.3 新增一个普通的hello方法,请问先打印邮件还是hello

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public  synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public synchronized void sendSMS(){System.out.println("-------sendSMS");}public void hello(){System.out.println("-------hello");}}/*** 新增一个普通的hello方法,请问先打印邮件还是hello* 结果:* -------hello* 三秒后* -------sendEmail*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{
//            phone.sendSMS();phone.hello();},"b").start();}
}

执行结果:

image-20230318175029199

2.4 有两部手机,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public  synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public synchronized void sendSMS(){System.out.println("-------sendSMS");}public void hello(){System.out.println("-------hello");}}/*** 有两部手机,请问先打印邮件还是短信* 结果:* -------sendSMS* 三秒后* -------sendEmail*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{
//            phone.sendSMS();
//            phone.hello();phone2.sendSMS();},"b").start();}
}

执行结果:

image-20230318175102479

2.5 两个静态同步方法,同1部手机,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public static synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public static synchronized void sendSMS(){System.out.println("-------sendSMS");}public void hello(){System.out.println("-------hello");}}/*** 两个静态同步方法,同1部手机,请问先打印邮件还是短信* 结果:* 三秒后* -------sendEmail* -------sendSMS*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();
//        Phone phone2 = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{phone.sendSMS();
//            phone.hello();
//            phone2.sendSMS();},"b").start();}
}

执行结果:

image-20230318175319965

2.6 两个静态同步方法, 2部手机,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public static synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public static synchronized void sendSMS(){System.out.println("-------sendSMS");}public void hello(){System.out.println("-------hello");}}/*** 两个静态同步方法, 2部手机,请问先打印邮件还是短信* 结果:* 三秒后* -------sendEmail* -------sendSMS*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{
//            phone.sendSMS();
//            phone.hello();phone2.sendSMS();},"b").start();}
}

执行结果:

image-20230318175445805

2.7 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public static synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public  synchronized void sendSMS(){System.out.println("-------sendSMS");}public void hello(){System.out.println("-------hello");}}/*** 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信* 结果:* -------sendEmail* 2.7秒后* -------sendSMS*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{phone.sendSMS();
//            phone.hello();
//            phone2.sendSMS();},"b").start();}
}

执行结果:

image-20230318180010833

2.8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;class Phone //资源类
{public static synchronized void sendEmail(){//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("-------sendEmail");}public  synchronized void sendSMS(){System.out.println("-------sendSMS");}public void hello(){System.out.println("-------hello");}}/*** 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信* 结果:* -------sendEmail* 2.7秒后* -------sendSMS*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(()->{phone.sendEmail();},"a").start();//暂停毫秒try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()->{
//            phone.sendSMS();
//            phone.hello();phone2.sendSMS();},"b").start();}
}

执行结果:

image-20230318180234669

2.9 8锁执行总结

  • 锁一锁二

    • 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
    • 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
  • 锁三锁四

    • 加个普通方法后发现和同步锁无关
    • 换成两个对象后,不是同一把锁了,情况立刻变化,即两把锁不会锁住对方
  • 锁五锁六

    三种 synchronized 锁的内容有一些差别:

    • 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
    • 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
    • 对于同步方法块,锁的是 synchronized 括号内的对象
  • 锁七锁八

    当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。

    • 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
    • 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class,具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

2.10 个人总结(8锁)

主要的思路就是判断锁对象是否一致,判断我们的锁是对象锁还是类锁,只要锁对象不相同就不会出现加锁阻塞的情况

8锁问题讲的是synchronized,而synchronized 有三种加锁方式

  • 同步方法:锁的是当前实例对象,通常指this

    • 如果两个调用对象是相同的那么就是同一把锁,会出现锁的情况
    • 如果如果两个调用对象不是相同的那么则无影响,就算是相同类的两个对象也是一样.
  • 静态同步方法:静态同步方法,锁的是当前类的Class对象

    • 众所周知一个类在jvm中只有一个对应的Class对象,所以调用同一个类的两个静态同步方法一定是会被锁住的.
  • 同步代码块:时我们手动指定加锁对象比较容易判断,即判断两个线程的锁对象是否一致

3. 从字节码角度分析synchronized实现

3.0 前置知识javap反编译

javap -c ***.class文件反编译

  • -c 对代码进行反汇编

假如你需要更多信息

  • javap -v ***.class文件反编译
    • -v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)

3.1 synchronized同步代码块

package site.zhourui.juc.locks;/*** synchronized同步代码块字节码实现*/
public class LockByteCodeDemo {Object object = new Object();public void m1(){synchronized (object){System.out.println("----------hello sync code");}}public static void main(String[] args) {}
}

执行一下后用在控制台找到字节码文件进行反编译

image-20230319103117193

小总结:

synchronized同步代码块实现使用的是monitorenter和monitorexit指令

3.1.2 一定是一个enter两个exit吗?

m1方法里面自己添加一个异常试试

package site.zhourui.juc.locks;/*** synchronized同步代码块字节码实现*/
public class LockByteCodeDemo {Object object = new Object();public void m1(){synchronized (object){System.out.println("----------hello sync code");throw new RuntimeException("------exp");//添加异常}}public static void main(String[] args) {}
}

执行一下后用在控制台找到字节码文件进行反编译

image-20230319103528908

回答不是:在程序出异常的情况下只有一个一个enter一个exit

3.2 synchronized同步代码方法

package site.zhourui.juc.locks;/**
* synchronized同步代码方法字节码实现
*/
public class LockByteCodeDemo {public  synchronized void m2(){System.out.println("----------hello static synchronized m2");}public static void main(String[] args) {}
}

我们使用-v查看更多信息

javap -v .\LockByteCodeDemo.class

image-20230319105030796

同步方法字节码的实现是通过加标志位ACC_SYNCHRONIZED实现的

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

3.3 synchronized静态同步方法

package site.zhourui.juc.locks;/*** synchronized静态同步代码方法字节码实现*/
public class LockByteCodeDemo {public static synchronized void m3(){System.out.println("----------hello static synchronized m3");}public static void main(String[] args) {}
}

我们使用-v查看更多信息

javap -v .\LockByteCodeDemo.class

image-20230319105030796

image-20230319105141092

静态同步方法字节码的实现是通过加标志位ACC_STATICACC_SYNCHRONIZED实现的

4. 反编译synchronized锁的是什么(为什么任何一个对象都可以成为一个锁?)

回答:因为在HotSpot虚拟机中,monitor采用ObjectMonitor实现,而我们每个java类的父类都是Object类,所以每个对象天生都带着一个对象监视器,所以为什么任何一个对象都可以成为一个锁

4.1 C++源码解读

  1. c++ 源码解读 : ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

  2. objectMonitor.hpp中定义了几项重要的属性

    image-20230319112522293

    image-20230319112550020

5. 公平锁和非公平锁

image-20230319114646627

5.1 公平锁

是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的

ReentrantLock reentrantLock = new ReentrantLock(true);

5.2 非公平锁

是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)

ReentrantLock reentrantLock = new ReentrantLock(false);//默认false
ReentrantLock reentrantLock = new ReentrantLock();//默认非公平锁

5.3面试题

5.3.1 为什么会有公平锁/非公平锁的设计为什么默认非公平?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

5.2.2 使⽤公平锁会有什么问题

公平锁保证了排队的公平性,不会造成锁饥饿,但是会增加线程的开销

非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”

5.2.3 什么时候用公平?什么时候用非公平?

  • 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
  • 否则那就用公平锁,大家公平使用。

5.4 从ReentrantLock卖票编码演示公平和非公平现象

package site.zhourui.juc.locks;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class Ticket
{private int number = 50;private Lock lock = new ReentrantLock(); //默认用的是非公平锁,分配的平均一点,=--》公平一点public void sale(){lock.lock();try{if(number > 0){System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);}}finally {lock.unlock();}}
}public class SaleTicketDemo {public static void main(String[] args){Ticket ticket = new Ticket();new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"a").start();new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"b").start();new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"c").start();new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"d").start();new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"e").start();}
}

默认非公平锁时执行结果:

线程a直接将票卖完了

image-20230319114400895

公平锁执行结果:

所有线程都参与进来了,没有出现线程饥饿的问题

image-20230319114458696

5.5 预埋伏AQS(抽象队列同步器)

image-20230319114905068

image-20230319114920039

6. 可重入锁又名递归锁

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

6.1 为什么会出现可重入锁

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

6.2 可重入锁种类

6.2.1 隐式锁(即synchronized关键字使用的锁)默认是可重入锁

package site.zhourui.juc.locks;public class ReEntryLockDemo {public static void main(String[] args){final Object objectLockA = new Object();new Thread(() -> {synchronized (objectLockA){System.out.println("-----外层调用");synchronized (objectLockA){System.out.println("-----中层调用");synchronized (objectLockA){System.out.println("-----内层调用");}}}},"a").start();}
}

验证结果:该锁可以递归调用

image-20230319115659128

6.2.2 显式锁(即Lock)也有ReentrantLock这样的可重入锁。

package site.zhourui.juc.locks;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ReEntryLockDemo {public static void main(String[] args){Lock lock = new ReentrantLock();new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t"+"-----外层");lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t"+"-----内层");}finally {lock.unlock();}}finally {lock.unlock();}},"t1").start();new Thread(() -> {lock.lock();try{System.out.println("------22222");}finally {lock.unlock();}},"t2").start();}
}

执行结果: 该锁可以递归调用

image-20230319120120358

6.3 Synchronized的重入的实现机理

image-20230319112550020

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

  1. 当执行monitorenter时,如果目标锁对象的计数器_count为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程_owner设置为当前线程,并且将其计数器_count加1。
  2. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器_count加1并且锁的重入次数_recursions加一,否则需要等待,直至持有线程释放该锁。
  3. 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。如果_recursions不为0_recursions同时减一,计数器为零代表锁已被释放。

7. 死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

7.1 产生死锁的主要原因:

  1. 系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当

7.2 一个死锁代码示例

package site.zhourui.juc.locks;import java.util.concurrent.TimeUnit;public class DeadLockDemo {static Object lockA = new Object();static Object lockB = new Object();public static void main(String[] args){Thread a = new Thread(() -> {synchronized (lockA) {System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");}}}, "a");a.start();new Thread(() -> {synchronized (lockB){System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁");try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }synchronized (lockA){System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功");}}},"b").start();}
}

执行结果:

image-20230319121727590

7.3 如何排查死锁

7.3.1 纯命令

  1. jps -l 查询我们程序执行的进程id

    image-20230319122350759

  2. 我们推测DeadLockDemo出现了死锁,jstack 进程编号,如果出现死效果如下图:

    image-20230319122729382

7.3.2 图形化

  1. 控制台输入jconsole,选中猜测死锁的线程点击连接

    image-20230319123202758

  2. 选择线程选项点击检测死锁

    image-20230319123358143

  3. 查看是否有死锁信息,有死锁的话会自动显示出来

    image-20230319123444932

8. 总结

指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

image-20230319130234087

相关内容

热门资讯

Mysql常用数据类型总结 整形 枚举类型ENUE整形       TINYINT,SMALLINT,MEDIUMINT,IN...
【flink sql】创建表 flink sql创建表语法 CREATE TABLE [IF NOT EXISTS] [catal...
python opencv 保... 👨‍💻个人简介: 深度学习图像领域工作者 dz...
Pytorch深度学习实战3-... 目录1 数据集Dataset2 数据加载DataLoader3 常用预处理方法4 模型处理5 实例&...
自定义类型的超详细讲解ᵎᵎ了解...   目录 1.结构体的声明 1.1基础知识 1.2结构体的声明 1.3结构体的特殊声明  1.4结构...
Docker等容器技术如何与移... 移动应用程序的开发面临着很多挑战,包括开发环境的设置、测试的困难、部署的复杂性等。由于...
【微服务】—— Nacos安装... 文章目录1. Windows安装1.1 下载安装包1.2 解压1.3 端口配置1.4 启动1.5 访...
【OpenGL】 为了理解这个函数我们需要先学习一些OpenGL的内容 OpenGL可视化 https://g...
hjr-详细说一下Redis集... Redis作用 缓存 一般我们用Redis做缓存,热点数据 击穿:访问到...
【蓝桥杯】 C++ 数字三角形... 文章目录题目描述输入描述输出描述实现代码解题思路注意点知识点 题目描述 上图给出了一个数字三角形。从...
VR全景展会丨探索未来,重塑现... 随着科技的不断发展,虚拟现实(VR)技术逐渐成为一个重要的...
C++数据类型 目录 C++基础数据类型 指针 指针类型 指针赋值 引用 参考:《深...
超实用!!! 三分钟将你的项目... 文章目录前言一、在项目中新增配置二、配置github page setting?三、如...
数据结构---队列 专栏:数据结构 个人主页:HaiFan. 专栏简介:这里是...
数字操作方法 系列文章目录 前端系列文章——传送门 JavaScript系列文章——传送门 文章目录系列文章目录...
Cartesi 2023 年 ... 查看 Cartesi Machine、Cartesi Rollups 和 Noether 的更新正在...
JavaWeb——jsp概述入... JSP定义:  在如下一个jsp文件里面有如下的代码  <%@ page content...
一切喜怒哀乐都来自于你的认知 01 有个学子,准备出国,父母请来清华的教授宁向东。请问教授࿱...
JAVA并发编程——synch... 引言         Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,...
git学习----3.21 未... 文章目录前言Git :一个分布式版本控制工具目标一、概述1.1 开发中的实际场景1.2...
Qt优秀开源项目之十七:QtP... QtPromise是Promises/A+规范的Qt/C++实现。该规范的译...
【前端八股文】JavaScri... 文章目录Set概念与arr的比较属性和方法并集、交集、差集Map概念属性和方法String用索引值和...
海康硬盘录像机接入RTSP/o... EasyNVR安防视频云服务平台可支持设备通过RTSP/Onvif协议接入平台,能提供...
在混合劳动力时代如何避免网络安... 在混合劳动力时代如何避免安全网络风险 三年多来,混合工作一直是工作生活中不可或缺的一...
2023还不懂Jmeter接口... 这里介绍的Jmeter接口测试的的实战,如果文章内容没遇看懂的话,我这边...
基于4G/5G弱网聚合的多链路... 基于4G/5G多卡聚合(弱网聚合)的智能融合通信设备技术亮点 增强带宽提供可靠连接 通过将多个有线和...
如何使用Synplify综合v... 文章目录使用Synplify综合的好处synplify的教程方法1(无效)...
2023年全国最新高校辅导员精... 百分百题库提供高校辅导员考试试题、辅导员考试预测题、高校辅导员考试真题、辅导员证考试题库等ÿ...
2022年18个值得期待的Le... 有数百个独特的LearnDash附加组件,您可能很难选择您的LearnDash LMS...
【java基础】Stream流... 文章目录基本介绍流的创建流的各种常见操作forEach方法filter方法map方法peek方法fl...