【并发编程】-4.Lock接口与AQS

Lock接口概念

  1. 概念JDK1.5时,在Java.uitl.concurrent并发包中添加了Lock锁接口,其中定义了lock()、unLock()、tryLock()、lockInterruptibly()、newCondition()等一系列接口,它属于是JAVA语言层面的内置锁;
  2. 特性
    1. 显示锁:不同于synchronized隐式锁,锁的获取和释放都是隐式的由JVM来管理,不需要开发人员手动实现;而显示锁的获取和释放都需要开发人员手动编码实现;
    2. 非阻塞式获取锁:定义了tryLock()方法,可以在获取锁时设置超时时长,达到超时时间还未获取到锁会返回一个false
    3. 支持中断获取锁:定义了lockInterruptibly()方法,可以接受中断信号;
    4. 多条件等待唤醒机制ConditionCondition定义了一系列线程之间通信方法,能更细粒度的进行线程间的通信;

Lock接口和synchronized的比较

  1. 锁的类型比较

    1. 公平/非公平synchronized是非公平锁;ReentrantLock可以是公平锁也可以是非公平锁;具体可参考;
    2. 都是可重入锁
    3. 都为悲观锁
  2. 锁的获取与释放比较Lock是显示锁必须手动获取,手动释放;而synchronized是隐式锁,锁的获取与释放由JVM来管理;

    1. Lock可以获取多个锁:ReentrantReadWriteLock中的读锁获取 reentrantReadWriteLock.readLock().lock();
    2. Lock可以通过tryLock()方法非阻塞式获取锁;
  3. 异常情况

    1. synchronized在发生异常的时候,会自动释放线程占有的锁
    2. ReentrantLock在发生异常时,如果没有通过unlock去释放锁,很有可能造成死锁,因此需要在finally块中释放锁;
  4. 线程调度比较:

    1. synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制;
    2. Lock: 使用Condition中的signal(); await();等, 进行线程之间的调度;

Lock接口的使用

Lock接口中的核心方法

public interface Lock {/*** 尝试获取锁,若资源空闲则直接获取锁,否则阻塞等待*/void lock();/*** 与lock()方法作用类似,只是若没有获取到锁阻塞时,可以接受中断命令,中断获取锁*/void lockInterruptibly() throws InterruptedException;/*** 尝试非阻塞获取锁并立刻返回,若资源空闲则直接获取锁并返回true,否则直接返回false不会阻塞等待;*/boolean tryLock();/*** tryLock()方法作用类似,只是设置了获取锁的等待时间,在等待时间内阻塞获取锁,获取到锁返回true,否则返回false;*/boolean tryLock(long time, TimeUnit unit) throws InterruptedException;/*** 释放锁*/void unlock();/*** 可以将一个condition实例绑定到lock实例上,然后可以通过signal(); await();等方法,对锁线程进行调度*/Condition newCondition();
}

Lock接口的实现类:

ReentrantLock:

独占锁,可重入锁,公平/非公平锁,可中断锁;

简单使用案例:
package com.xrl.juc.lock_demo;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @author xrl* @date 2024/7/16 22:47*/
public class ReentrantLockTest implements Runnable{private Lock lock = new ReentrantLock();@Overridepublic void run() {get();}private void get() {lock.lock();try {System.out.println(Thread.currentThread() + "---------get()方法");Thread.sleep(1000);} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public static void main(String[] args) {ReentrantLockTest lockTest = new ReentrantLockTest();new Thread(lockTest).start();new Thread(lockTest).start();new Thread(lockTest).start();new Thread(lockTest).start();new Thread(lockTest).start();}
}

ReentrantReadWriteLock:

  1. 读写锁:一般在读多写少情况下使用。有两把锁一把读锁,一把写锁写锁是独占式的,读锁是共享式的
  2. 实现原理:底层使用AQS实现,同步状态的实现是在一个整形变量上通过“按位切割使用”;将变量切割成两部分,高16位表示读状态,也就是获取到读锁的次数,低16位表示获取到写线程的可重入次数,并通过CAS对其进行操作实现读写分离;
  3. 三个特性
    1. 公平性:支持公平和非公平两种模式。
    2. 重入性:支持重入,读写锁都支持最多65535个。
    3. 锁降级:先获取写锁,再获取读锁,再释放写锁,写锁就能降级为读锁。
简单案例
package com.xrl.juc.lock_demo;import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 有两把锁,一把读锁一把写锁,同一个线程中可以同时拥有两把锁但必须先获取 writeLock 再获取 readLock* 读锁可以有多个线程共有,写锁只能一个线程* 在有读锁的情况下没有写锁,有写锁的小情况下只有拥有写锁的那个线程可以有读锁* 读多写少情况下使用** @author xrl* @date 2024/7/16 23:27*/
public class ReentrantReadWriteLockeTest {/*** 可重入的读写锁*  ReadWriteLock接口需要实现两把锁**/private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();private static  ExecutorService threadPool= Executors.newFixedThreadPool(5);/*** 让线程同步争抢锁*/private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3);private static int i = 100;public static void main(String[] args) {//通过 cyclicBarrier 使三个线程同步运行threadPool.execute(() -> {write(Thread.currentThread());});threadPool.execute(() -> {read(Thread.currentThread());});threadPool.execute(() -> {read(Thread.currentThread());});threadPool.shutdown();}private static void write(Thread thread) {try {//线程阻塞, 让线程同步争抢锁cyclicBarrier.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}//获取写锁reentrantReadWriteLock.writeLock().lock();//获取读锁
//        reentrantReadWriteLock.readLock().lock();try {System.out.println("写线程开始运行" + thread.getName() + " : i=" + i);Thread.sleep(1000);i = 1;System.out.println(thread.getName() + "运行完" + " : i=" + i );} catch (InterruptedException e) {e.printStackTrace();} finally {//释放锁System.out.println("释放锁");reentrantReadWriteLock.writeLock().unlock();}}private static void read(Thread thread) {try {//线程阻塞, 让线程同步争抢锁cyclicBarrier.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}// 线程请求后阻塞至写完成System.out.println("线程请求读锁");reentrantReadWriteLock.readLock().lock();System.out.println("得到读锁");//获取写锁
//        reentrantReadWriteLock.writeLock().lock();try {System.out.println("读线程开始运行" + thread.getName() + " : i=" + i);Thread.sleep(1000);System.out.println(thread.getName() + "运行完");} catch (InterruptedException e) {e.printStackTrace();} finally {//释放锁reentrantReadWriteLock.readLock().unlock();}}
}

结果,说明获得写锁和获得读锁的线程不能同时运行,获得读锁的线程可以同时运行

写线程开始运行pool-1-thread-1 : i=100
pool-1-thread-1运行完 : i=1
读线程开始运行pool-1-thread-2 : i=1
读线程开始运行pool-1-thread-3 : i=1
pool-1-thread-2运行完
pool-1-thread-3运行完

AQS

  1. 概念AQSAbstractQueuedSynchronizer抽象类的简称,它通过模板模式的设计模式构建,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作及解锁操作,从而使AQS只关注内部公共方法实现并不关心外部不同模式的具体逻辑实现;

    模板方法如下;ReentrantLock实现的是tryAcquire(int arg)、tryRelease(int arg)、isHeldExclusively()

    //独占模式下获取锁的方法
    protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
    }
    //独占模式下释放锁的方法
    protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();
    }
    //共享模式下获取锁的方法
    protected int tryAcquireShared(int arg) {throw new UnsupportedOperationException();
    }
    //共享模式下释放锁的方法
    protected boolean tryReleaseShared(int arg) {throw new UnsupportedOperationException();
    }
    //判断是否持有独占锁的方法
    protected boolean isHeldExclusively() {throw new UnsupportedOperationException();
    }
    

核心属性

  1. 同步状态AQS内部通过一个用volatile修饰的int类型变量state作为同步状态private volatile int state;;当state为0时,代表着当前没有线程占用锁资源,反之不为0时,代表着锁资源已经被线程持有,通过CAS设置值;
  2. Node内部类(同步队列的节点):当锁资源已经被占用时(state != 0),则当前线程则会被封装成一个Node节点加入同步队列进行等待,Node节点主要包括了当前执行的线程本身、线程的状态,是否被阻塞、是否处于等待唤醒、是否中断等;每个Node节点中都关联着前驱节点prev以及后继节点next,从而构成一个双端队列;
  3. 同步队列:由上述未获取到锁资源的节点Node构成的FIFO双端队列,被称之为CLH队列,并在AQS中维护了两个变量private transient volatile Node head; private transient volatile Node tail;分别指向CLH队列的头和尾,方便数据新增和添加;
  4. 阻塞队列AQS使用内部类ConditionObject用来构建等待队列,当Condition调用await()方法后,等待获取锁资源的线程将会加入等待队列中,而当Condition调用signal()方法后,线程将会从等待队列转移到同步队列;

原理分析

  1. AQS主要内部维护着一个由Node节点构成的FIFO同步队列
  2. 当一个线程执行ReetrantLock.lock()加锁方法获取锁失败时,该线程会被封装成Node节点加入同步队列等待锁资源的释放,期间不断执行自旋等待;
  3. 当该线程所在节点的前驱节点为队列头结点时,当前线程就会开始尝试对同步状态标识state进行修改(+1),如果可以修改成功则代表获取锁资源成功,然后将自己所在的节点设置为队头head节点,表示自己已经持有锁资源;
  4. 当一个线程调用ReetrantLock.unlock()释放锁时,最终会调用Sync内部类中的tryRelease(int releases)方法再次对同步状态标识state进行修改(-1),成功之后唤醒当前线程所在节点的后继节点中的线程。

ReentrantLock源码分析

  1. ReentrantLockLock接口的实现类,当我们在new ReentrantLock();时,实际是创建了一个内部类Sync(同步器)的子类sync = new NonfairSync();,它拥有两个子类一个为公平锁一个为非公平锁sync = fair ? new FairSync() : new NonfairSync();

    ReentrantLock调用方法时,大部分方法实际都是调用sync 类中的方法,如:public void lock() { sync.lock(); }

    由下面代码可知sync继承AbstractQueuedSynchronizer抽象类简称AQS

    public class ReentrantLock implements Lock, java.io.Serializable {private static final long serialVersionUID = 7373984872572414699L;private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {
    

ReentrantLock方法说明:

    //构造器//默认构造器创建非公平锁public ReentrantLock();//传入一个Boolean类型的参数,true创建一个公平锁,false非公平public ReentrantLock(boolean fair);// 查询当前线程调用lock()的次数int getHoldCount() // 返回目前持有此锁的线程,如果此锁不被任何线程持有,返回null  protected Thread getOwner(); // 返回一个集合,它包含可能正等待获取此锁的线程,其内部维持一个队列(后续分析)protected Collection<Thread> getQueuedThreads(); // 返回正等待获取此锁资源的线程估计数int getQueueLength();// 返回一个集合,它包含可能正在等待与此锁相关的Condition条件的线程(估计值)protected Collection<Thread> getWaitingThreads(Condition condition); // 返回调用当前锁资源Condition对象await方法后未执行signal()方法的线程估计数int getWaitQueueLength(Condition condition);// 查询指定的线程是否正在等待获取当前锁资源boolean hasQueuedThread(Thread thread); // 查询是否有线程正在等待获取当前锁资源boolean hasQueuedThreads();// 查询是否有线程正在等待与此锁相关的Condition条件boolean hasWaiters(Condition condition); // 返回当前锁类型,如果是公平锁返回true,反之则返回flaseboolean isFair() // 查询当前线程是持有当前锁资源boolean isHeldByCurrentThread() // 查询当前锁资源是否被线程持有boolean isLocked()

核心方法解析

构造方法:

  1. public ReentrantLock(boolean fair);构造方法返回的是,实现了AQS抽象类中部分方法的抽象类Sync的子类。

    sync = fair ? new FairSync() : new NonfairSync();

    FairSync NonfairSync两个对象的实现大致相同 ,只是一个是公平性的,一个是非公平性的;

    两者之前的区别在于FairSync使用lock()方法获取锁之前,会先去判断同步队列中是否存在元素(hasQueuedPredecessors()方法),如果存在则将当前任务放入同步队列的队尾;

    NonfairSync只判断当前锁是否有人占用,如果没有就直接获取;

lock()方法

  1. lock()方法,底层是调用的是AQS中的模板方法acquire(int arg);

     public final void acquire(int arg) {// 调用子类中重写的`tryAcquire`判断能否获得资源;if (!tryAcquire(arg) &&// 将当前线程封装为一个`Node`节点,加入同步队列中;// 在同步队列中等待同步资源的释放,当线程争取到同步资源 的使用权后,线程中同步队列中出队;acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// selfInterrupt();}
    
  2. tryAcquire(arg) :调用子类中重写的tryAcquire判断能否获得资源;

    protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();// 得到当前锁的状态int c = getState();// 锁的状态为0时,表示现在锁没有被占用if (c == 0) {//hasQueuedPredecessors()判断当前的同步队列中是否存在元素,当前元素是不是队头元素,是公平锁和非公平锁的区别所在if (!hasQueuedPredecessors() &&// CAS修改state同步状态的值compareAndSetState(0, acquires)) {// 给当前线程设置独占访问权(拿到锁了)setExclusiveOwnerThread(current);return true;}}// 如果当前线程等于持有锁的线程(锁的重入)else if (current == getExclusiveOwnerThread()) {// 计数加一int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 修改同步状态的值setState(nextc);return true;}// 到这里说明没有拿到锁,返回falsereturn false;
    }
    
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg):在同步队列中等待同步资源的释放,当线程争取到同步资源 的使用权后,线程中同步队列中出队;

    final boolean acquireQueued(final Node node, int arg) {// 是否获取到资源标记boolean failed = true;try {// 中断标识boolean interrupted = false;// 自旋 for (;;) {// 获得传入节点的前驱节点final Node p = node.predecessor();// 如果当前节点的前驱节点节点等于头节点,且可以获得资源if (p == head && tryAcquire(arg)) {// 将当前结点设置为头节点setHead(node);// 设置原头结点的后继节点为null,帮助jvm回收垃圾;(能静方法说明头节点线程加锁操作已运行完成)p.next = null; // help GC// 标记已经成功获取同步资源failed = false;return interrupted;}// 当前节点不是头节或者获取资源失败// shouldParkAfterFailedAcquire(p, node)线程获取资源失败后,判断是否阻塞线程if (shouldParkAfterFailedAcquire(p, node) &&// 阻塞线程等待其他线程唤醒,并判断中断状态parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)// 如果最终都没能成功获取同步状态,结束该线程的请求cancelAcquire(node);}
    }
    
    1. shouldParkAfterFailedAcquire(p, node):线程获取资源失败后,判断是否阻塞线程

      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 得到前驱节点的同步状态int ws = pred.waitStatus;// 等于阻塞状态if (ws == Node.SIGNAL)// 返回ture说明要阻塞当前线程,等待前驱节点释放资源,唤醒后继节点 执行parkAndCheckInterrupt()return true;// 前驱状态大于0,说明state是CANCELLED,线程是已取消的if (ws > 0) {// 将同步队列中已经标记已取消的节点全部剔除do {// 从同步队列中去掉pred节点,(将后继节点的前驱节点改为这个节点的前驱节点node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 同步状态不大于0,状态是初始态或propagate态(在共享模式同步状态可以进行传播),则通过CAS将pred的状态改为SIGNAL阻塞状态compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
      }
      
    2. parkAndCheckInterrupt():阻塞线程等待其他线程唤醒,并判断中断状态

      private final boolean parkAndCheckInterrupt() {// 阻塞当前线程,底层是使用了Unsafe类提供的方法来实现线程的park(阻塞)和unpark(唤醒)操作// 方法内部,调用 setBlocker(t, blocker);方法,将当前线程关联到指定的同步对象blocker,标记当前线程由该对象导致阻塞// 之后调用UNSAFE.park(false, 0L)方法阻塞线程,第一个参数false表示不相对时间进行阻塞,第二个参数0L表示无期限地阻塞当前线程,直到有其他线程唤醒// 当有线程唤醒当前线程时,再次调用setBlocker(t, null);解除线程和对象直接的关联LockSupport.park(this);// 判断当前线程是否已被中断,返回中断标识,并清除中断标识return Thread.interrupted();
      }
      
    3. addWaiter(Node.EXCLUSIVE):将当前线程封装为一个Node节点,加入同步队列中;

      private Node addWaiter(Node mode) {// 将当前线程封装为一个节点Node node = new Node(Thread.currentThread(), mode);Node pred = tail;// 尾结点不为空if (pred != null) {// 将当前接节点插入队尾node.prev = pred;// CAS设置尾结点if (compareAndSetTail(pred, node)) {// 构成双端队列,将前一个元素指向下一个元素pred.next = node;// 设置成功,返回当前接节点return node;}}// 尾节点为空也就是同步队列为空 或 CAS设置尾结点失败enq(node);return node;
      }
      
    4. enq(node):将节点插入队列,队列为空时初始化

      private Node enq(final Node node) {// 自旋for (;;) {Node t = tail;// 尾节点为空if (t == null) { // cas 初始化头节点if (compareAndSetHead(new Node()))// 没有节点时,头尾相等tail = head;} else {// 将当前元素插入同步队列中,同addWaiter中的逻辑node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
      }
      

unlock()方法

  1. 底层调用的是AQSrelease(1);方法;

    public final boolean release(int arg) {// 尝试释放锁  ReentrantLock实现的模板方法if (tryRelease(arg)) {Node h = head;// 头节点不为空 且 节点状态不为初始状态if (h != null && h.waitStatus != 0)// 唤醒同步队列的后继节点unparkSuccessor(h);return true;}return false;
    }
    
    1. tryRelease(arg):尝试释放锁 ReentrantLock实现的模板方法

      protected final boolean tryRelease(int releases) {// 计算同步状态int c = getState() - releases;// 如果当前释放锁的线程不为持有锁的线程则抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 判断状态是否为0,如果是则说明已释放同步状态if (c == 0) {free = true;// 设置Owner为nullsetExclusiveOwnerThread(null);}// 修改同步状态setState(c);return free;
      }
      
    2. unparkSuccessor(h);: 唤醒同步队列的后继节点

      private void unparkSuccessor(Node node) {int ws = node.waitStatus;// 节点阻塞状态小于0,说明节点没有被取消if (ws < 0)// 将节点阻塞状态设置为0compareAndSetWaitStatus(node, ws, 0);Node s = node.next;// 后继节点为null或者为取消状态if (s == null || s.waitStatus > 0) {s = null;//  寻找到下一个非取消态的结点 (从同步队列队尾结点开始向前遍历,非空且不等于当前node ,则校验此结点t的状态)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 唤醒结点t对应的线程if (s != null)LockSupport.unpark(s.thread);
      }
      

实现总结

  1. 基础属性

    1. 同步状态标识:标识锁资源的占有状态;
    2. 同步队列:存放获取锁失败的线程;
    3. 等待队列:用于实现多条件唤醒;
    4. Node节点:队列的每个节点,线程封装体;
  2. 基础操作

    1. cas修改同步状态标识;
    2. 获取锁失败加入同步队列阻塞;
    3. 释放锁时唤醒同步队列第一个节点线程;
  3. 加锁操作

    1. 调用tryAcquire()修改标识state,失败加入队列等待;
    2. 加入队列后判断节点是否为signal状态,是则调用LockSupport.park(this);阻塞挂起当前线程;
    3. 判断是否为cancel状态,是则往前遍历删除队列中所有cancel状态节点;
    4. 如果节点为0或者propagate状态则将其修改为signal状态;
    5. 阻塞被唤醒后如果为head则获取锁,成功返回true,失败则继续阻塞;
  4. 解锁操作

    1. 调用tryRelease()释放锁修改标识state
    2. 释放锁成功后唤醒同步队列后继阻塞的线程节点;
    3. 被唤醒的节点会自动替换当前节点成为head节点;

Condition接口

使用Condition可以实现类似于Objectwait()、notify()等线程间通信操作;具体实现可参考;原理参考;

简单案例

package com.xrl.juc.conditiontest;import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*** 利用lock来替换synchronized实现 生产者,消费者模式** @author shkstart* @create 2024-07-24 17:56*/
public class producerConsumer {public static void main(String[] args) {AppleBox appleBox = new AppleBox();Producer producer = new Producer(appleBox);Consumer consumer = new Consumer(appleBox);Consumer consumer2 = new Consumer(appleBox);Thread p = new Thread(producer, "生产者");Thread c1 = new Thread(consumer, "消费者1");Thread c2 = new Thread(consumer2, "消费者2");p.start();c1.start();c2.start();}
}class Apple {int id;public Apple(int id) {this.id = id;}
}class AppleBox {AtomicInteger index = new AtomicInteger(0);AtomicReferenceArray<Apple> apples = new AtomicReferenceArray<>(new Apple[5]);
//    AtomicReference<Apple[]> apples = new AtomicReference<>();//创建锁对象 ,替换synchronizedprivate final ReentrantLock lock = new ReentrantLock();//生产条件private final Condition produceCondition = lock.newCondition();//消费条件private final Condition consumeCondition = lock.newCondition();// public synchronized boolean deposite(Apple apple) {public boolean deposite(Apple apple) {lock.lock();try {while (index.get() == apples.length()) {//生产线程进入等待produceCondition.await();return false;}apples.set(index.getAndIncrement(), apple);//this.apples[index.incrementAndGet()] = apple;System.out.println(Thread.currentThread().getName() + "生产了: " + index.get());//唤醒消费线程consumeCondition.signal();return true;} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}return false;}public Apple withdraw() throws Exception {lock.lock();try {while (index.get() == 0) {System.out.println(Thread.currentThread().getName() + "消费缺货" );consumeCondition.await();}Apple a = apples.get(index.decrementAndGet());System.out.println(Thread.currentThread().getName() + "消费了: " + a.id);return a;} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}return null;}
}class Producer implements Runnable {AppleBox appleBox;public Producer(AppleBox appleBox) {this.appleBox = appleBox;}@Overridepublic void run() {while (true) {appleBox.deposite(new Apple(appleBox.index.get() + 1));try {Thread.sleep((int) (Math.random() * 1000));} catch (InterruptedException e) {e.printStackTrace();}}}
}class Consumer implements Runnable {AppleBox appleBox;public Consumer(AppleBox appleBox) {this.appleBox = appleBox;}@Overridepublic void run() {while (true) {try {appleBox.withdraw();} catch (Exception e) {e.printStackTrace();}try {Thread.sleep((int) (Math.random() * 3000));} catch (InterruptedException e) {e.printStackTrace();}}}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

47、PHP实现机器人的运动范围

题目&#xff1a; PHP 实现机器人的运动范围 描述&#xff1a; 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动&#xff0c;每一次只能向左&#xff0c;右&#xff0c;上&#xff0c;下四个方向移动一格&#xff0c;但是不能进入行坐标和列坐标的数位之和大于k…

探索云计算的未来:边缘计算如何重塑IT格局

目录 一、引言 二、边缘计算的基本概念与特性 1. 基本概念 2. 技术特性 三、边缘计算如何重塑IT基础设施 1. 分布式计算架构的兴起 2. 边缘节点的部署与管理 3. 数据流与业务逻辑的重新设计 4. 安全性与隐私保护的加强 四、边缘计算在关键行业的应用实践 1. 智能制造…

关联查询(xml)

多对多&#xff1a;数据库中需要有中间表&#xff0c;在两个实体类中都加入对方的List集合&#xff0c;在写查询语句时写三张表

MySQL第一阶段:多表查询、事务

继续我的MySQL之旅&#xff0c;继续上篇的DDL、DML、DQL、以及一些约束&#xff0c;该到了多表查询和事务的学习总结&#xff0c;以及相关的案例实现&#xff0c;为未来的复习以及深入的理解做好知识储备。 目录 多表查询 连接查询 内连接 外连接 子查询 事务 事务简介…

RTK高精度定位终端的功能跟用途

RTK高精度定位终端是一种集成了高精度定位技术的手持或便携式设备&#xff0c;其功能和用途广泛且重要。以下是RTK高精度定位终端的主要功能和用途&#xff1a; 一、功能 高精度定位&#xff1a; RTK技术通过接收卫星信号和地面基站的差分修正数据&#xff0c;实现厘米级甚至…

开源安全信息和事件管理(SIEM)平台OSSIM

简介 OSSIM&#xff0c;开源安全信息和事件管理&#xff08;SIEM&#xff09;产品&#xff0c;提供了经过验证的核心SIEM功能&#xff0c;包括事件收集、标准化和关联。 OSSIM作为一个开源平台&#xff0c;具有灵活性和可定制性高的优点&#xff0c;允许用户根据自己的特定需…

Java Linux操作系统

1、操作系统是协助用户调度硬件工作&#xff0c;充当用户和计算机硬件之间的桥梁 2、Linux内核 提供了linux系统的主要功能 3、发行版Centos&#xff1a;内核应用程序 4、快照&#xff1a;保存虚拟机的状态&#xff0c;当虚拟机出现问题的时候&#xff0c;可以恢复原始的状态…

Excel下载模板文件和导入文件的步骤

一、配置api // 题目导入模板下载接口 export function exportTemplate() {return request({url: /edu/question/exportTemplate,method: get,//必加header: {headers: {Content-Type: application/x-download},},responseType: "blob",//必加}) } // 题目模板数据导…

华为强制恢复出厂设置后如何恢复数据?数据重生的2个方法介绍

华为作为全球知名的手机品牌&#xff0c;其产品在市场上广受欢迎。然而&#xff0c;有时由于各种原因&#xff0c;我们可能需要强制恢复出厂设置&#xff0c;这往往意味着数据的丢失。那么&#xff0c;如何在华为强制恢复出厂设置后&#xff0c;让数据“重生”呢&#xff1f;本…

Postman测试工具详细解读

目录 一、Postman的基本概念二、Postman的主要功能1. 请求构建2. 响应查看3. 断言与自动化测试4. 环境与变量5. 集合与文档化6. 与团队实时协作 三、Postman在API测试中的重要性1. 提高测试效率2. 保障API的稳定性3. 促进团队协作4. 生成文档与交流工具 四、Postman的使用技巧1…

Ubuntu24.04 deb文件 安装 MySQL8.4

Ubuntu24.04 deb文件 安装 MySQL8.4 ubuntu24.04 deb文件安装 MySQL8 升级系统 sudo apt update sudo apt -y dist-upgrade 安装常用工具 sudo apt -y install vim net-tools wget gcc make cmake lrzsz安装依赖 sudo apt -y install libmecab2 libjson-perl libaio1t64下载…

JavaEE - Spring Boot 简介

1.Maven 1.1 什么是Maven 翻译过来就是: Maven是⼀个项⽬管理⼯具。基于POM(Project Object Model,项⽬对象模型)的概念&#xff0c;Maven可以通 过⼀⼩段描述信息来管理项⽬的构建&#xff0c;报告和⽂档的项⽬管理⼯具软件。 可以理解为&#xff1a;Maven是一个项目管理工具…

关键词查找【Knuth-Morris-Pratt (KMP) 算法】

一个视频让你彻底学懂KMP算法_哔哩哔哩_bilibili KMP算法的核心是利用匹配失败后的信息&#xff0c;尽量减少模式串与主串的匹配次数以达到快速匹配的目的。 public class KMP {// 计算部分匹配表 (LPS)private static int[] computeLPSArray(String pattern) {int[] lps new…

Arduino学习笔记1——IDE安装与起步

一、IDE安装 去浏览器直接搜索Arduino官网&#xff0c;点击Software栏进入下载界面&#xff0c;选择Windows操作系统&#xff1a; 新版IDE下载不需要提前勾选所下载的拓展包&#xff0c;下载好后直接点击安装即可。 安装好后打开Arduino IDE&#xff0c;会自动开始下载所需的…

npm publish出错,‘proxy‘ config is set properly. See: ‘npm help config‘

问题&#xff1a;使用 npm publish发布项目依赖失败&#xff0c;报错 proxy config is set properly. See: npm help config 1、先查找一下自己的代理 npm config get proxy npm config get https-proxy npm config get registry2、然后将代理和缓存置空 方式一&#xff1a; …

基于TensorFlow.js和COCO-SsD模型的实时目标检测网络应用程序

基于TensorFlow.js和COCO-SsD模型的实时目标检测网络应用程序 实现流程 访问用户的桌面录屏并且显示视频源&#xff08;位置居中&#xff09;。对视频源进行实时目标检测。在检测到的目标周围绘制边界框&#xff0c;并用它们的类别和检测置信度进行标记。在视频源下方显示一个…

openEuler安装docker,加速镜像拉取

文章目录 文章来源1.配置镜像源2.编辑配置文件3.安装想要的版本4. ~ 原神&#xff01;5.由于很多镜像无法拉取配置镜像源 文章来源 http://t.csdnimg.cn/zYDYy 原文连接 由于之前的仓库不让用且 1.配置镜像源 由于 国外的镜像仓库好多不让用 所以配置阿里的镜像源 yum-confi…

Memcached开发(七):使用Java进行操作

目录 1. 安装与配置Memcached 1.1 安装Memcached 1.2 配置Memcached 2. 使用Java与Memcached进行交互 2.1 安装Java客户端 2.2 连接Memcached 2.3 基本操作 2.3.1 设置键值对 2.3.2 获取键值对 2.3.3 删除键值对 2.3.4 检查键是否存在 2.3.5 增加和减少数值 2.4 高…

使用 com.alibaba:easyexcel 导出excel文件时遇到的问题

1.字符长度超出限制 java.lang.IllegalArgumentException: The maximum length of cell contents (text) is 32,767 characters 解决方案&#xff1a; 1. 把字符串截取前32767个字符 /*** sqlText 字段导出 excel 时的转化器*/ public class SqlTextConverter implements C…

javascript 如何将 json 格式数组转为 excel 表格| sheetJS

案例 // https://unpkg.com/xlsx0.18.5/dist/xlsx.full.min.js function exportXlsx(jsonData, fileName , mine null) {const workbook XLSX.utils.book_new();// 将JSON数组转换成工作表const worksheet XLSX.utils.json_to_sheet(jsonData);// 向工作簿添加工作表XLSX.…