Java中的锁:实现并发控制与资源共享

JAVA中的锁

    • 锁的概念
      • 锁机制
      • 为什么要使用锁
      • 锁的种类
        • 乐观锁/悲观锁
        • 独享锁/共享锁
        • 互斥锁/读写锁
        • 可重入锁/不可重入锁
        • 公平锁/非公平锁
        • 分段锁/自旋锁
        • CAS/AQS
    • synchronized
      • 概念
      • 应用场景
      • 四种使用场景效果对比
      • synchronized的特点
      • Lock/ReentrantLock对比
    • Volatile
      • 概念
      • Java内存模型
      • 线程可见性
      • 内存可见性解决方案

锁的概念

锁机制

​ 通过锁机制,能够保证在多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

​ 所谓的锁,可以理解为内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功。如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。

为什么要使用锁

​ 在多线程情况下完成操作时,由于并不是原子操作,所以在完成操作的过程中可能会被打断,造成数据的一致性。

锁的种类

乐观锁/悲观锁

​ 乐观锁/悲观锁并不是特指某两种类型的锁,而是一种锁的思想。

1、乐观锁

​ 乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

​ 一般会使用“数据版本机制”或“CAS操作”来实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,因为不加锁会带来大量的性能提升。

  • CAS(下面有说明)

  • 数据版本机制

    ​ 实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳,以版本号为例,

    ​ 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
    

2、悲观锁

​ 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步关键字synchronized关键字的实现就是悲观锁。悲观锁适合写操作非常多的场景。

​ 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

​ 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

独享锁/共享锁

​ 独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

对于Synchronized而言,当然是独享锁。

互斥锁/读写锁

​ 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock;读写锁在Java中的具体实现就是ReadWriteLock。

可重入锁/不可重入锁

1、可重入锁

​ 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。简单来说就是同一个线程可以重复加锁。对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

public class Test{
     Lock lock = new Lock();
     public void methodA(){
         lock.lock();
         ...........;
         methodB();
         ...........;
         lock.unlock();
     }
     public void methodB(){
         lock.lock(); // 重复获取锁,等待
         ...........;
         lock.unlock();
     }
}

2、可重入锁的实现

​ 每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取。

public class RSpitnLock implements Lock {
    private AtomicReference<Thread> currLock = new AtomicReference<>();
    private int count = 0;

    @Override
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == currLock.get()) {
            count++; // 一个线程多次获取锁,count++
            return;
        }
        while (!currLock.compareAndSet(null, current)) { // 获取锁
        }
    }

    @Override
    public void unlock() {
        Thread current = Thread.currentThread();
        if (current == currLock.get()) {
            if (count != 0) {
                count--;
            } else {
                currLock.compareAndSet(current, null); // // 当年线程count=0才能释放锁
            }
        }
    }
}

AtomicReference锁案例

      // 1.创建一个锁对象
        AtomicReference<Thread> currLock = new AtomicReference<>();

        // 2.获取当前线程
        Thread thread = Thread.currentThread();

        // 3.当年线程获取锁
        boolean b = currLock.compareAndSet(null, thread);

        // 4.查看锁别那个线程获取
        Thread thread1 = currLock.get();

        // 5、当前线程和获取锁的线程对比
        System.out.println(Thread.currentThread().getName()+"获取锁:"+b);
//        System.out.println(thread == thread1);

        // 6、启的一个新的线程
        Thread update = new Thread(() -> {
            // 在新的线程中获取锁
            System.out.println(Thread.currentThread().getName()+":获取锁之前,"+currLock.get().getName());
            while (!currLock.compareAndSet(null, Thread.currentThread())) {
            }
            System.out.println(Thread.currentThread().getName()+":获取锁之后,"+currLock.get().getName());
        }, "线程2");
        update.start();

        // 7、休眠3s后main线程释放锁
        Thread.sleep(10000);
        System.out.println(Thread.currentThread().getName()+"修改3s结束开始释放锁。");
        currLock.compareAndSet(thread, null); // 当前线程释放锁

2、不可重入锁

​ 一个线程中多次获取锁,导致死锁。

public static void main(String[] args) {
    Lock lock = new Lock();
    lock.lock(); // 第一次获取锁成功
    // ....
    lock.lock(); // 同一个线程再次获取锁失败
}

class Lock {
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}
公平锁/非公平锁

​ 公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

​ 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。

分段锁/自旋锁

1、分段锁

​ 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

2、自旋锁

​ 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

CAS/AQS

1、AQS

​ AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。

​ AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

​ CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

​ AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
在这里插入图片描述

2、CAS

​ CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。CAS也是一种乐观锁的实现。

​ 举个例子:原本表中的数据name的值为toString, 线程将name的读出来,修改为name=Java,在写入的时候先判断name的值是否和修改之前的值(toString)一致,如果一致就提交,否则就重新读取内容再修改,所以CAS操作长时间不成功的话,会一直自旋,相当于死循环了,CPU的压力会很大。

CAS存在一个ABA的问题

​ 线程1读取到内容A,线程也读取到内容A后把A修改为B写入进去,此时线程3读取到最新内容是B,然后把B改为A写入进去,最后线程A在写入的时候发现是A,所以依然可以写入成功。虽然写入成功但是线程A不知道这个值已经被其他线程修改过了,所以这就是典型的ABA的问题。
在这里插入图片描述

携带版本号可以有效的防止CAS中ABA的问题

synchronized

概念

​ synchronized是Java中的关键字,是一种同步锁。用来保证被它修饰的方法或者代码块在任意时刻只能有一个线程调用。

应用场景

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    public class Demo01 {
    
        private static Object object = new Object();
    
        public static void main(String[] args) {
            // 注意,这里是锁的是对象不是代码块,面试题
            synchronized (object) { // // 锁对象
                // ....
            }
            synchronized (Object.class) { // 锁类
            }
        }
    
        public synchronized static void test1() {
            //synchronized(Demo01.class)
        }
    
        public synchronized void test2() {
            // synchronized(this)
        }
    }
    

四种使用场景效果对比

public class ThreadDemo {
    public static void main(String[] args) {

        // 创建两个对象
        User user1 = new User("张三");
        User user2 = new User("李四");

        // 创建两个任务对象
        MyThread myThread1 = new MyThread(user1);
        MyThread myThread2 = new MyThread(user2);

        // 启动20个线程
        for (int i = 0; i < 10; i++) {
            new Thread(myThread1).start();
            new Thread(myThread2).start();
        }
    }
}

class User {
    public String name;
    public User(String name) {
        this.name = name;
    }

//    public synchronized void add() throws InterruptedException {
//    public static synchronized void add() throws InterruptedException {
    public void add() throws InterruptedException {
        synchronized (User.class) { // this和User.class的区别?
            Thread.sleep(1000);
            System.out.println("name:" + name + ",threadNmae:" + Thread.currentThread().getName() + "---> add");
            Thread.sleep(1000);
        }
    }
}

class MyThread implements Runnable {

    private User user;
    public MyThread(User user) {
        this.user = user;
    }

    @Override
    public void run() {
        try {
            user.add();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

内存图
在这里插入图片描述

synchronized的特点

1、原子性:原子性的操作执行到一半,并不会因为CPU线程调度而被打断。

2、可见性:释放锁所有的数据都会写回内存,获取锁都会从内存中读取最新数据。

3、可重入锁:它是一个可重入锁

4、重量级锁:

​ 他是一个重量级锁,开销很大,开发过程中尽量少用。
​ 底层是通过一个监视器对象(monitor)完成,wait () , notify ()等方法也依赖于monitor,对象监视器锁(monitor)的本质依赖于底层操作系统的互斥锁(MutexLock)实现,而操作系统实现线程切换需从用户态转换到内核态,上述切换过程较长,所以synchronized效率低&重量级。

5、自动加锁和自动释放锁

Lock/ReentrantLock对比

ReentrantLock加锁演示

       // 1.获取锁对象
        Lock lock = new ReentrantLock();

        // 2.加锁
        lock.lock(); // 获取不到锁会一直阻塞
        try {
            // 3.处理业务 .....
        } finally {
            // 5.释放锁
            lock.unlock();
        }

1、使用上的区别:
1)Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;

​ 2)synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象。Lock发生异常,若没有主动释放,极有可能造成死锁,故需要在finally中调用unLock方法释放锁;

​ 3)Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程一直等待下去,不能响应中断

​ 4)通过Lock可以知道有没有成功获取到锁,synchronized就只能等待。

​ 5)Lock可以提高多个线程进行读操作的效率

2、在锁概念上的区别:

​ 1)可中断锁:响应中断的锁,Lock是可中断锁(体现在lockInterruptibly()方法),synchronized不是。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。

​ 2)公平锁和非公平锁:synchronized是非公平锁,ReentrantLock默认是非平锁,可以设置为公平锁。

​ 3)读写锁:读写锁将对一个资源(如文件)的访问分为2个锁,一个读锁,一个写锁;读写锁使得多个线程的读操作可以并发进行,不需同步。而写操作就得需要同步,提高了效率
ReadWriteLock就是读写锁,是一个接口,ReentrantReadWriteLock实现了这个接口。可通过readLock()获取读锁,writeLock()获取写锁

3、性能比较:
synchronized是一个重量级锁,性能要低于ReentrantLock。

​ 但是synchronized存在也有它的道理,它是因多线程应运而生,它的存在也大幅度简化了Java多线程的开发。它的优势就是使用简单,你不需要显示去加减锁,相比之下ReentrantLock的使用就繁琐的多了,你加完锁之后还得考虑到各种情况下的锁释放,而synchronized就不用关心这些。

Volatile

概念

​ volatile是Java中的关键字,提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

Java内存模型

​ Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。

​ JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。
在这里插入图片描述

线程可见性

多个线程共享一个数据,其中一个线程修改了数据,另一个线程维持的还是旧数据。

public class Demo02 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        while (true) {
            if (myThread.getFlag()) {
                System.out.println(Thread.currentThread().getId() + ":flag为true:" + System.currentTimeMillis());
            }
        }
    }
}
class MyThread extends Thread {

    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("线程1修改为true");
    }

    public boolean getFlag() {
        return this.flag;
    }
}

在这里插入图片描述

对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。

​ 解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。

内存可见性解决方案

1、加锁

  while (true) {
            synchronized (myThread) {
                if (myThread.getFlag()) {
                    System.out.println(Thread.currentThread().getId() + ":flag为true:" + System.currentTimeMillis());
                }
            }
        }

实现原理
在这里插入图片描述

2、volatile

class MyThread extends Thread {
    private volatile boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("线程1修改为true");
    }

    public boolean getFlag() {
        return this.flag;
    }
}

实现原理
在这里插入图片描述

​ 当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
写操作会导致其他线程中的缓存无效。
​ 这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性。

后记
👉👉💕💕美好的一天,到此结束,下次继续努力!欲知后续,请看下回分解,写作不易,感谢大家的支持!! 🌹🌹🌹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/486225.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多工作空间并存时ROS的环境变量异常问题

今天突然发现当多工作空间并存时&#xff0c;我的ROS的环境变量发生了比较诡异的异常&#xff0c;按照我之前的理解&#xff0c;在.bashrc文件中按顺序设定了ROS的环境变量后&#xff0c;ROS就会按照.bashrc中编写的环境变量来搜寻功能包&#xff0c;例如在.bashrc文件使用sour…

Nuclei Poc开发

1、Poc开发工具介绍 Nuclei&#xff1a;https://nuclei.projectdiscovery.io/ Cloud Platfrom云平台&#xff1a;https://cloud.projectdiscovery.io/ 2、目标站点简介 目标演示站点&#xff1a;http://glkb-jqe1.aqlab.cn/nacos/#/login 指纹&#xff1a;Nacos 已知常用漏洞…

【微服务】Eureka(服务注册,服务发现)

文章目录 1.基本介绍1.学前说明2.当前架构分析1.示意图2.问题分析 3.引出Eureka1.项目架构分析2.上图解读 2.创建单机版的Eureka1.创建 e-commerce-eureka-server-9001 子模块2.检查父子pom.xml1.子 pom.xml2.父 pom.xml 3.pom.xml 引入依赖4.application.yml 配置eureka服务5.…

【Web APIs】事件高级

目录 1.事件对象 1.1获取事件对象 1.2事件对象常用属性 2.事件流 1.1事件流的两个阶段&#xff1a;冒泡和捕获 1.2阻止事件流动 1.3阻止默认行为 1.4两种注册事件的区别 3.事件委托 1.事件对象 1.1获取事件对象 事件对象&#xff1a;也是一个对象&#xff0c;这个对象里…

rapidssl证书通配符证书800元

RapidSSL旗下的DV基础型通配符SSL证书可以同时保护多个域名站点&#xff0c;保护主域名以及主域名下的所有子域名。这款通配符SSL证书可以为网站提供数据加密服务&#xff0c;营造安全的上网环境&#xff0c;确保用户在网站上的数据安全传输。今天就随SSL盾小编了解RapidSSL旗下…

2024年HCIE考试题二

27、以下关于在网络中选择认证点位置的描述中&#xff0c;错误的是哪一项&#xff1f; A.在网络的接入层部署认证&#xff0c;有利于实现权限的细颗粒度管理和网络的高安全性 B.用户认证点从接入层上移到汇聚层之后&#xff0c;可能会导致用户的MAC认证失败 C.当用户认证点从…

DC电源模块的设计与调试技巧

BOSHIDA DC电源模块的设计与调试技巧 DC电源模块的设计与调试是电子工程师在实际项目中常常需要面对的任务。一个稳定可靠的DC电源模块对于电路的正常运行起到至关重要的作用。以下是一些设计与调试的技巧&#xff0c;帮助工程师们更好地完成任务。 第一&#xff0c;正确选择…

vue3.0 + ts + eslint报错:error Parsing error: ‘>‘ expected

eslint报错 这里加上对应的 eslint配置即可&#xff1a; parser: vue-eslint-parser, parserOptions: {parser: "typescript-eslint/parser",ecmaVersion: 2020,sourceType: module, }具体如下&#xff1a; module.exports {parser: vue-eslint-parser,parserOpti…

代码随想录阅读笔记-栈与队列【删除字符串中的所有相邻重复项】

题目 给出由小写字母组成的字符串 S&#xff0c;重复项删除操作会选择两个相邻且相同的字母&#xff0c;并删除它们。 在 S 上反复执行重复项删除操作&#xff0c;直到无法继续删除。 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。 示例&#xff1a; 输入&am…

如何成功将自己开发的APP上架到应用商店

如何成功将自己开发的APP上架到应用商店 随着移动应用市场的蓬勃发展&#xff0c;开发一款优秀的APP已成为许多企业和个人的首要选择。然而&#xff0c;成功上架并有效推广APP至关重要。本文将逐步介绍完整的上架流程&#xff0c;包括准备所需材料、注册开发者账户、进行APP备…

OpenLayers基础教程——使用WebGLPoints加载海量点数据

1、前言 最近遇到一个问题&#xff1a;如何在OpenLayers中高效加载海量的场强点&#xff1f;由于项目中的一些要求&#xff0c;不能使用聚合的方法加载。一番搜索之后发现&#xff1a;OpenLayers中有一个WebGLPoints类&#xff0c;使用该类可以轻松应对几十万的数据量&#xf…

多目标追踪实现_3.9

目标 利用sort算法完成多目标追踪 在这里主要实现了一个多目标跟踪器&#xff0c;管理多个卡尔曼滤波器对象&#xff0c;主要包括以下内容&#xff1a; 初始化&#xff1a;最大检测数&#xff0c;目标未被检测的最大帧数 目标跟踪结果的更新&#xff0c;即跟踪成功和失败的目…

Android第一行代码——快速入门 Kotlin 编程(4.7 编写界面的最佳实践)

目录 4.7 编写界面的最佳实践 4.7.1 制作 9—Patch 图片 4.7.2 编写精美的聊天界面 4.7 编写界面的最佳实践 既然已经学习了那么多 UI 开发的知识,是时候实战一下了。这次我们要综合运用前面所学的大量内容来编写出一个较为复杂且相当美观的聊天…

【前端寻宝之路】JavaScript初学之旅

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-azUa9yH16cRXQUxE {font-family:"trebuchet ms",verdana,arial,sans-serif;f…

代码随想录算法训练营第十五天| 二叉树的层序遍历、226.翻转二叉树、101.对称二叉树

系列文章目录 目录 系列文章目录二叉树层序遍历&#xff08;10题&#xff09;102. 二叉树的层序遍历①DFS--递归方式②BFS--迭代方式--借助队列 107.二叉树的层次遍历 II199.二叉树的右视图637.二叉树的层平均值429.N叉树的层序遍历遍历每个节点的子节点 515.在每个树行中找最大…

Ftrans安全数据摆渡系统 构建便捷的内外网数据交换通道

安全数据摆渡系统是一种设计用于解决内外网环境下&#xff0c;数据传输、管理、共享问题的安全系统&#xff0c;通过加密、访问控制等策略&#xff0c;提供安全可靠的数据传输和共享服务&#xff0c;尤其适用于对网络安全建设要求高的行业&#xff0c;比如研发型企业、党政机构…

构建vue3项目以及bem架构

构建vue3vite项目 &#xff08;1&#xff09;使用vite初始化一个项目 npm init vitelatest &#xff08;2&#xff09;构建cli项目 vue create <project> bem架构 src下新建文件bem.scss $namespace: "xc" !default; $block-sel: "-" !defaul…

【LVGL-开关部件】

LVGL-开关部件 ■ LVGL-开关部件■ 开关部件&#xff1a;指示器打开的颜色■ 开关部件&#xff1a;不可修改■ 开关部件&#xff1a;获取开关状态■ 开关部件&#xff1a;示例一&#xff1a;制冷,制暖,开关 ■ LVGL-开关部件 ■ 开关部件&#xff1a;指示器打开的颜色 ■ 开关部…

Leetcode算法题笔记(2)

目录 图论51. 岛屿数量解法一 52. 腐烂的橘子解法一 53. 课程表解法一 54. 实现 Trie (前缀树)解法一 回溯55. 全排列解法一 56. 子集解法一解法二 57. 电话号码的字母组合解法一 58. 组合总和解法一解法二 59. 括号生成解法一解法二 60. 单词搜索解法一 图论 51. 岛屿数量 给…

PostgreSQL FDW(外部表) 简介

1、FDW: 外部表 背景 提供外部数据源的透明访问机制。PostgreSQL fdw(Foreign Data Wrapper)是一种外部访问接口,可以在PG数据库中创建外部表,用户访问的时候与访问本地表的方法一样,支持增删改查。 而数据则是存储在外部,外部可以是一个远程的pg数据库或者其他数据库(…