javalock(八)ReentrantReadWriteLock

news2024/12/21 6:12:22

ReentrantReadWriteLock:
同时实现了共享锁和排它锁。内部有一个sync,同时实现了tryAcquire/tryReleases、tryAcquireShared/tryReleasesShared,一共四个函数,然后ReentrantReadWriteLock内部还实现了一个ReadLock和一个WriteLock,ReadLock和WriteLock都实现了lock和unlock函数,然后ReadLock和WriteLock是对同一个Sync对象的封装,不同之处在于ReadLock的lock函数调用的是Sync.tryAcquireShared,unlock调用的是tryReleasesShared,而WriteLock的lock函数调用的是Sync.tryAcquire/Sync.tryReleases。还有ReadLock重载了newCondition,直接抛异常,因为Condition会调用isHeldExclusive来判断当前线程是否拥有排它锁,而ReadLock是共享锁,这矛盾了,所以ReadLock重载了newCondition函数,然后直接抛异常,而WriteLock则可以正常newCondition,因为WriteLock是排它锁,所以可以支持condition,换句话说:只有排它锁才支持condition。

ReentrantReadWriteLock:
锁资源由两部分组成{state,Holder},state是一个int,用来记录已经获取读锁的线程数和已经获取写锁的线程数,state的32位字节分成了两部分,高16位表示已经获取读锁的线程数,低16位表示写者重入的次数,因为写锁是排它锁,也就是说只会有一个线程获取写锁,所以如果state低16位不为0就表示有人获得了写锁,然后这里就直接用低16位来记录写锁重入的个数,如果读者数不为0,则写者数必定为0,如果写者数为0则读者数必定为0,也就是说读写锁互斥。holder则是记录锁的获取信息,因为是reentrant即可冲入锁,也就是说可能出现这种情况:多个线程同时获取了读锁,然后多个线程又多次readLock.lock,所以就需要一个结构体来记录当前线程是否获得了读锁以及当前线程重入的次数,所以holder是一个threadlocal变量,每个线程都有一份,如果当前线程没有获得该锁,则删除该threadLocal,前面说了写锁重入次数直接用state低16位记录,并且用父类的owner来记录谁获得了写锁,所以写锁不用holder,holder只用于读锁。注意ReentrantLock不需要holder的原因是ReentrantLock是排它锁,最多只有一个线程能获得锁,而父类提供的state和owner变量就足够了,所以不需要holder变量。

一个线程已经获取写锁以后,可以继续获取读锁,但是反过来不行,一个线程获取了读锁后,是不允许再次获取写锁的,原因很简单,如果线程先获取了写锁,那么就能保证其他所有线程都不能获取读锁,所以线程可以安全获取读锁,因为只有他一个人能获取读写权限,而反过来如果线程先获取了读锁,那么就可能还有其他线程此时也获取了读锁,这样就不止一个线程获取了读锁,所以此时是不能获取写锁的。


import java.util.concurrent.TimeUnit;
import java.util.Collection;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

//同时实现了读写锁
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;

    //读锁对象
    private final ReentrantReadWriteLock.ReadLock readerLock;
    //写锁对象
    private final ReentrantReadWriteLock.WriteLock writerLock;

    //读锁对象和写锁对象封装了同一个sync对象
    final Sync sync;

  
    public ReentrantReadWriteLock() {
        this(false);
    }

    //默认是非公平锁
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        //readLock和writeLock都是使用的同一个ReentrantReadWriteLock对象
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

  
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

      

        //读者数偏移,读者数=state>>SHARED_SHIFT
        static final int SHARED_SHIFT   = 16;
        //读者数+1就直接加SHARED_UNIT就行
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        //最大读者数
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        //写者重入次数数掩码,写者数=state&&EXCLUSIVE_MASK
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

      
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
      
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

        //定义的holder类,记录了当前线程id,以及加读锁的次数
        //如果线程获得了读锁,就会拥有一个holdCounter对象
        //如果线程没有获取读锁,就会删除holdCounter对象
        //因为HoldCounter对象被用作ThreadLocal对象
        static final class HoldCounter {
            //记录读锁重入的次数
            int count = 0;
            //线程id
            final long tid = getThreadId(Thread.currentThread());
        }


        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
        //这里解释一下readHolds/cachedHoldCounter/firstReader/firstReaderHoldCount
        //这些变量都只有一个目的:如果当前线程持有读锁,则返回本线程重入次数
        //readHolds是一个ThreadLocal变量,其他三个都不是ThreadLocal
        //本来要返回本线程重入次数直接返回ThreadLocal内保存的count就行了
        //但是ThreadLocal.get可能比较慢,所以为了优化,
        //就用了两个变量firstReader/cachedHolderCount
        //firstReader记录的是当没有任何获取读锁时,记录第一个获得读锁的线程的thread对象
        //cachedHolderCount记录的是上次获取读锁的线程的id
        //readHolds记录的是本线程的信息,是threadLocal
        //然后如果有人要获取当前线程读锁的重入次数,那么获取逻辑是这样的:
        //先看能不能从firstReader读,如果firstReader就是当前线程
        //那么就直接返回firstReadHoldCount,就不用去读threadLocal了
        //如果firstReader不是当前线程,则尝试去cachedHolderCount读
        //如果上一次获取读锁的线程就是当前线程,那么ok,可以直接从cachedHolderCount读
        //这样就不用去读ThreadLocal了,如果都失败了,那么就只能去读readHolds了
        //而读threadLocal肯定是比较慢的。。。(花里胡哨的,不过文档说有助于提高并发效率。。。)
        private transient ThreadLocalHoldCounter readHolds;
        private transient HoldCounter cachedHoldCounter;
        private transient Thread firstReader = null;
        private transient int firstReaderHoldCount;

        Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); 
        }

      

      
        abstract boolean readerShouldBlock();

      
        abstract boolean writerShouldBlock();

      
        //释放写锁
        protected final boolean tryRelease(int releases) {
            //首先判断当前线程是否拥有锁:直接判断owner是不是当前线程,如果是则拥有
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //然后扣减写锁计数,写锁是低16位,所以可以直接减
            int nextc = getState() - releases;
            //判断写锁重入计数是否为0
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                //如果是则设置owner为null
                setExclusiveOwnerThread(null);
            //然后设置state
            //笔记:在释放写锁前,读者数必定为0
            setState(nextc);
            return free;
        }

        //获取读锁
        protected final boolean tryAcquire(int acquires) {

            //获取当前线程
            Thread current = Thread.currentThread();
            //获取锁资源状态
            int c = getState();
            //计算写锁重入次数个数
            int w = exclusiveCount(c);

            //如果c!=0,表示有人获得了读锁或者有人获得了写锁
            if (c != 0) {
                //c!=0,但是w=0,表明有人获得了读锁
                //c!=0,并且w!=0,表示有人获得了写锁,所以还需要判断是不是自己获得了写锁
                //所以这里就是如果有人获得了读锁或者获取写锁的线程不是自己,那么本次写锁获取失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //如果是自己获得了写锁,则判断可重入次数是否超过了(2^16-1)次
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //如果没有,则表示成功获取锁资源,返回true
                //注意:此时是线程已经获得了锁,并且是自己,
                // 也就是说只有本线程可以修改state,所以此时无需用cas操作
                setState(c + acquires);
                return true;
            }
            //如果c==0表示此时没有任何人获得锁
            //因为ReentrantReadWrite支持公平或者非公平锁,所以writeShouldBlock是一个抽象方法
            //公平方式:看aqs的sync list是否有节点等待,如果有则返回true,表示当前线程不能获取锁
            //非公平方式:writeShouldBlock直接返回false,表示立即尝试获取锁
            if (writerShouldBlock() ||
                //如果不需要阻塞,则立即通过cas尝试获取锁
                !compareAndSetState(c, c + acquires))
                //如果cas获取失败就返回false表示本次获取锁失败
                return false;
            //获取成功就设置锁拥有者为自己
            setExclusiveOwnerThread(current);
            return true;
        }

        //释放读锁
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //此处到下面for循环前面的都是为了更新当前线程的读锁重入次数

            //如果当前线程就是自己,那么直接更新firstReaderHoldCount
            //就不用去写threadLocal了
            if (firstReader == current) {
                //如果重入次数为1,那么-1之后就表示释放掉了
                //所以直接把firstReader设置为null
                //这样,firstReader是无效的,那么firstReaderHoldCount就肯定失效了
                //所以此处只设置firstReader=null,而没有扣减firstReaderHoldCount
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    //否则读锁重入次数大于1,释放一次后,线程还是拥有读锁
                    //所以firstReader不用变
                    firstReaderHoldCount--;
            } else {
                //如果自己不是firstReader,那么再尝试cachedHoldCounter
                //即尝试自己是不是上一次访问的线程
                HoldCounter rh = cachedHoldCounter;
                //如果缓存的holder为null或者缓存的holder不是当前线程的
                if (rh == null || rh.tid != getThreadId(current))
                    //那么就表示从cachedHoldCounter读取失败
                    //此时就只能去读threadLocal了
                    rh = readHolds.get();
                //此时rh为cachedHoldCounter或者readHolds
                int count = rh.count;
                //如果count<=1,那么释放一次后就为0
                //所以此时就要移除threadLocal
                //免得内存泄漏,比如一个线程获取了读锁,释放后就永不再是用这个rw锁了
                if (count <= 1) {
                    //移除threadLocal
                    readHolds.remove();
                    if (count <= 0)
                        //如果count<=0就表示自己没有持有读锁,却常是释放读锁
                        throw unmatchedUnlockException();
                }
                //当前线程重入次数-1
                --rh.count;
            }
            //更新完读锁重入次数,那么下面就是通过cas更新state也就是更新读锁总数
            for (;;) {
                //获取读锁总次数
                int c = getState();
                //-1
                int nextc = c - SHARED_UNIT;
                //cas设置state
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

        private IllegalMonitorStateException unmatchedUnlockException() {
            return new IllegalMonitorStateException(
                "attempt to unlock read lock, not locked by current thread");
        }

        //尝试获取读锁
        //!!!在持有写锁的状态下允许获取读锁,但是反过来持有读锁时是不允许再次获取写锁
        //也就是说支持锁降级(当然,这里没有降级),但是不支持锁升级
        protected final int tryAcquireShared(int unused) {

            //获取当前线程对象
            Thread current = Thread.currentThread();
            //获取锁资源状态
            int c = getState();
            //判断是否有线程持有写锁,如果有,则判断是不是当前线程
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                //如果不是,则返回-1表剩余读锁资源不足,读锁获取失败
                return -1;
            //走到此处,要么无人获取写锁,要么自己获取了写锁
            //先获取读者计数
            int r = sharedCount(c);
            //判断本次获取读锁是否应该阻塞,分公平和非公平方式
            //非公平方式:如果aqs队列中第一个等待的线程是要获取写锁,那么本次读者应该等待
            //也就是避免写进程长时间获取不到读锁,也就是说如果第一个等待的线程是要获取读锁,
            //那么本线程会立即获取读锁,不管后面的,也就是说对读不公平,但是对写带点公平
            //公平方式:如果aqs队列中有等待的进程,则本次获取read需要阻塞
            // 可能是因为达到总读锁上限了,因为state用于读者锁计数的只有16位
            if (!readerShouldBlock() &&
                //如果没有达到总读者锁重入计数
                r < MAX_COUNT &&
                //那么就尝试获取读锁
                compareAndSetState(c, c + SHARED_UNIT)) {
                //下面就是更新线程对应的读锁重入次数了
                //r==0表示本线程是第一个获得读锁的线程
                if (r == 0) {
                    //那么就设置firstReader为本线程
                    firstReader = current;
                    //并且读锁重入次数为1
                    firstReaderHoldCount = 1;
                //如果当前线程正好是第一个读者
                } else if (firstReader == current) {
                    //那么直接更新firstReaderHoldCount就行
                    firstReaderHoldCount++;
                } else {
                    //反之判断cachedHoldCounter是否为当前线程
                    HoldCounter rh = cachedHoldCounter;
                    //如果cached为null或者cached不是当前线程
                    if (rh == null || rh.tid != getThreadId(current))
                        //则更新cached为当前线程的readHold
                        cachedHoldCounter = rh = readHolds.get();
                    //如果cached是自己,如果count=0表示我们之前已经删除了
                    //那么直接设置readHolds等于我们缓存的,就不用重新创建了
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    //更新本线程读锁重入次数
                    rh.count++;
                }
                //返回获取锁成功
                return 1;
            }
            //如果获取锁失败或者本线程获取读锁需要阻塞,则走重逻辑获取锁
            //重逻辑和上面的逻辑几乎一样
            return fullTryAcquireShared(current);
        }

        //while cas获取读锁
        final int fullTryAcquireShared(Thread current) {
          
            HoldCounter rh = null;
            for (;;) {
                //获取锁状态
                int c = getState();
                //判断是不是有人获取了写锁
                if (exclusiveCount(c) != 0) {
                    //如果有,则再判断是不是自己
                    if (getExclusiveOwnerThread() != current)
                        //如果不是,则返回失败
                        return -1;
                  
                    //如果本次读者需要阻塞:
                    //非公平方式:如果aqs队列中第一个等待的线程是要获取写锁,那么本次读者应该等待
                    //也就是避免写进程长时间获取不到读锁,也就是说如果第一个等待的线程是要获取读锁,
                    //那么本线程会立即获取读锁,不管后面的,也就是说对读不公平,但是对写带点公平
                    //公平方式:如果aqs队列中有等待的进程,则本次获取read需要阻塞
                    // 可能是因为达到总读锁上限了,因为state用于读者锁计数的只有16位
                } else if (readerShouldBlock()) {
                    //这里又是一样的逻辑,用来更新count的,懒得写了
                    if (firstReader == current) {
                      
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //cas操作尝试获取锁
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    //下面又是更新count的逻辑了,和上面一样,略
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; 
                    }
                    //返回1表示读锁资源剩余1
                    //这里是固定返回1,就是说不管获取多少次读锁,剩余读锁资源总是1
                    return 1;
                }
            }
        }

        //和tryAcquire逻辑一模一样,唯一区别就是这里即使线程拥有写锁,也是通过cas更新state
        //略
        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

        //和fullTryAcquireShared逻辑几乎一模一样,略
        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

        //判断当前线程是否拥有写锁
        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

      
        //写锁支持条件变量,writeLock的newCondition中调用这个函数
        //注意:读锁是不支持的,readLock的newCondition函数则是直接跑UnSupport异常
        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        //如果写锁已分配,获取持有写锁的线程对象
        final Thread getOwner() {
            return ((exclusiveCount(getState()) == 0) ?
                    null :
                    getExclusiveOwnerThread());
        }

        //获取读锁总次数
        final int getReadLockCount() {
            return sharedCount(getState());
        }
        //判断是否有现成是否已经获得了写锁
        final boolean isWriteLocked() {
            return exclusiveCount(getState()) != 0;
        }

        //获取写锁重入次数,就是state低15位
        final int getWriteHoldCount() {
            return isHeldExclusively() ? exclusiveCount(getState()) : 0;
        }

        //获取当前线程读锁重入次数
        final int getReadHoldCount() {
            if (getReadLockCount() == 0)
                return 0;

            Thread current = Thread.currentThread();
            //先尝试从firstReader读取,
            if (firstReader == current)
                return firstReaderHoldCount;
            //如果firstReader不是自己,则再尝试从cachedHolderCount读取
            HoldCounter rh = cachedHoldCounter;
            if (rh != null && rh.tid == getThreadId(current))
                return rh.count;

            //如果cachedHolderCount也不是自己,那么最后才去读threadLocal,即读readHolder
            int count = readHolds.get().count;
            if (count == 0) readHolds.remove();
            return count;
        }

      
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            readHolds = new ThreadLocalHoldCounter();
            setState(0); 
        }

        final int getCount() { return getState(); }
    }

    //非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        //本次获取写锁是否该阻塞?答案是:用不阻塞
        final boolean writerShouldBlock() {
            return false; 
        }
        //本次获取读锁是否应该阻塞?
        //答案是如果是如果第一个等待的线程是写锁,则本次获取读锁需要阻塞
        //反之如果是读锁,则不阻塞,对读锁不公平,对写锁稍显公平
        final boolean readerShouldBlock() {
            return  apparentlyFirstQueuedIsExclusives();
        }

    }

    //公平方式
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        //本次获取写锁是否需要阻塞?如果等待队列不为空则需要阻塞
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        //本次获取读锁是否需要阻塞?如果等待队列不为空则需要阻塞
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }


    //下面就是ReadLock和WriteLock了,这两都是对上面的Sync的一个简单封装
    //ReadLock和WriteLock的各种函数都是简单的转调用Sync的相关函数,
    //一眼就能看明白,略


    public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;

      
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

      
        public void lock() {
            sync.acquireShared(1);
        }

      
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }

      
        public boolean tryLock() {
            return sync.tryReadLock();
        }

      
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }

      
        public void unlock() {
            sync.releaseShared(1);
        }

        //!!!读锁不支持条件变量
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }

      
        public String toString() {
            int r = sync.getReadLockCount();
            return super.toString() +
                "[Read locks = " + r + "]";
        }
    }
  
    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;

      
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

      
        public void lock() {
            sync.acquire(1);
        }

      
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }

      
        public boolean tryLock( ) {
            return sync.tryWriteLock();
        }

      
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }

      
        public void unlock() {
            sync.release(1);
        }

        //写锁支持条件变量,就是AQS的条件变量
        public Condition newCondition() {
            return sync.newCondition();
        }

      
        public String toString() {
            Thread o = sync.getOwner();
            return super.toString() + ((o == null) ?
                                       "[Unlocked]" :
                                       "[Locked by thread " + o.getName() + "]");
        }

      
        public boolean isHeldByCurrentThread() {
            return sync.isHeldExclusively();
        }

      
        public int getHoldCount() {
            return sync.getWriteHoldCount();
        }
    }

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

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

相关文章

线性代数基础与应用:基底 (Basis) 与现金流及单期贷款模型(中英双语)

具体请参考&#xff1a;https://web.stanford.edu/~boyd/vmls/ 下面的例子来源于这本书。 线性代数基础与应用&#xff1a;基底 (Basis) 与现金流及单期贷款模型 在线性代数中&#xff0c;基底&#xff08;Basis&#xff09;是一个重要的概念&#xff0c;广泛应用于信号处理、…

微信小程序的消息头增加的字段不能有下滑线,字段大写字母自动转换消息字母

微信小程序的消息头增加的字段不能有下滑线&#xff0c;字段大写字母自动转换消息字母。这个是微信小程序的坑。 正式环境&#xff1a; 微信小程序的消息头增加了一个字段device_id,结果node.js打印出来的字段没有该字段。 [2024-12-20T09:45:54.476] [DEBUG] app - ctx.head…

【Java项目】基于SpringBoot的【旅游管理系统 】

【Java项目】基于SpringBoot的【旅游管理系统 】 技术简介&#xff1a;本系统使用JAVA语言开发&#xff0c;采用B/S架构、Spring Boot框架、MYSQL数据库进行开发设计。 系统简介&#xff1a;&#xff08;1&#xff09;管理员功能&#xff1a;可以管理个人中心、用户管理、景区分…

游戏引擎学习第55天

仓库: https://gitee.com/mrxiao_com/2d_game 介绍 今天的主题是让世界存储真正实现稀疏化&#xff0c;即便当前效率可能并不高。我们计划花一些时间处理这个问题&#xff0c;并探讨相关的成本。稀疏化世界存储是接下来的重要步骤&#xff0c;为此需要逐步实施。 修复 SetCa…

每日十题八股-2024年12月19日

1.Bean注入和xml注入最终得到了相同的效果&#xff0c;它们在底层是怎样做的&#xff1f; 2.Spring给我们提供了很多扩展点&#xff0c;这些有了解吗&#xff1f; 3.MVC分层介绍一下&#xff1f; 4.了解SpringMVC的处理流程吗&#xff1f; 5.Handlermapping 和 handleradapter有…

Linux限制root 用户的远程登录(安全要求)

前言&#xff1a;现在基本用户主机都不允许使用root来操作&#xff0c;所以本文通过创建新用户&#xff0c;并限制root用户的ssh来解决这个问题 1. 创建新账户 aingo 首先&#xff0c;使用 root 账户登录系统。 sudo useradd aingo设置 aingo 账户密码&#xff1a; sudo pa…

mysql的事务和存储引擎+备份

mysql的事务和存储引擎备份 一. mysql的事务1.1 mysgl支持事务四种隔离级别1.2 事务控制语句1.3 行锁和死锁1.3.1 行锁1.3.2 死锁1.3.3 如何避免死锁的发生 二. msyql的备份和还原以及日志管理2.1 数据库备份的分类2.2 备份策略2.2.1 物理冷备份&#xff08;全量&#xff09;2.…

raid 状态查看 storcli64

场景 当磁盘报错的时候使用该命令排查 fdisk -l /dev/sdb fdisk: cannot open /dev/sdb: Input/output error进一步使用 smartctl 排查 smartctl -a /dev/sdb 输出 smartctl 7.1 2019-12-30 r5022 [x86_64-linux-5.4.0-144-generic] (local build) Copyright (C) 2002-19, B…

[搜广推]王树森推荐系统——双塔模型

内容概览 矩阵补充模型 矩阵补充模型输入用户ID 和 物品ID&#xff0c;利用embedding把 用户ID 和 物品ID 映射成向量&#xff0c;输出向量的内积&#xff08;一个实数&#xff09;&#xff0c;内积越大说明用户对这个物品越感兴趣 详见[搜广推]王树森推荐系统——矩阵补充&a…

96 vSystem

vSystem系统 1 技术背景 网络虚拟化旨在构建出一套与网络底层物理拓扑相互独立的逻辑网络环境&#xff0c;提供给不同需求的用户使用。基于这种思想&#xff0c;诞生出了 VLAN 技术和 VPN 技术。近年来&#xff0c; 随着以 VMM&#xff08;Virtual Machine Monitor&#xff0c…

低代码软件搭建自学的第一天——熟悉PyQt

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 学习计划第 1 步&#xff1a;环境搭建1.1 安装 Python1.2 安装 PyQt安装命令&#xff1a;验证安装&#xff1a; 第 2 步&#xff1a;PyQt 基础知识2.1 创建第一个窗…

【mac 终端美化】oh my zsh

快捷的mac终端美化方法。 以下是效果图&#xff0c;比较朴素&#xff0c;但是相比默认方案会好看一些&#xff0c;也能明显查看输入的命令&#xff0c;不会和输出混在一起。 下载主题 访问网址&#xff1a;https://iterm2colorschemes.com/&#xff0c;下载网页的压缩包并解…

移动魔百盒中的 OpenWrt作为旁路由 安装Tailscale并配置子网路由实现在外面通过家里的局域网ip访问内网设备

移动魔百盒中的 OpenWrt作为旁路由 安装Tailscale并配置子网路由实现在外面通过家里的局域网ip访问内网设备 一、前提条件 确保路由器硬件支持&#xff1a; OpenWrt 路由器需要足够的存储空间和 CPU 性能来运行 Tailscale。确保设备架构支持 Tailscale 二进制文件&#xff0c;例…

【笔记】深度学习模型评估指标

推荐链接&#xff1a; &#xff08;0&#xff09;多分类器的评价指标 &#xff08;1&#xff09;泛化误差的评价方法&#xff1a;【机器学习】模型评估与选择&#xff08;留出法、交叉验证法、查全率、查准率、偏差、方差&#xff09; &#xff08;2&#xff09;机器学习&…

Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表

文章目录 Linux性能监控命令_nmon 安装与使用安装解压创建nmono目录解压到nmono目录当中切换到sources目录下解压 配置环境变量创建软链接到 /usr/bin/ 目录下打开 配置文件 配置环境变量在底部增加如下注册 使用使用说明监控监控CPU监控内存监控磁盘监控网络监控文件系统 后台…

串口通信控制LED灯

做这个东西的目的是锻炼一下自己的编程能力以及系统思维能力 首先&#xff0c;清楚自己要干什么&#xff0c;正点原子大家应该都看过&#xff0c;系统框图是一个比较重要的东西&#xff0c;引导我们去设计和思考。 下面先给出系统框图&#xff1a; 模块划分好后&#xff0c;结构…

WPF+MVVM案例实战与特效(四十五)- 打造优雅交互:ListBox 的高级定制与行为触发(侧边菜单交互面板)

文章目录 1、引言2、案例效果3、案例实现1、依赖安装2、文件创建3、代码实现1、依赖引用与上下文2、个性化视觉效果:自定义 ItemContainerStyle3、页面样式与布局完整代码4、ViewModel 逻辑实现5、子界面代码:3、实现效果4、源代码获取5、总结1、引言 在WPF应用程序开发中,…

分享两个爬虫练习网站+一个python游戏网站

目录 第一个网站第二个Python游戏网站 第一个网站 网站一 第二个 网站二 Python游戏网站 网站三

空天地遥感数据识别与计算--数据分析如何助力农林牧渔、城市发展、地质灾害监测等行业革新

在科技飞速发展的时代&#xff0c;遥感数据的精准分析已经成为推动各行业智能决策的关键工具。从无人机监测农田到卫星数据支持气候研究&#xff0c;空天地遥感数据正以前所未有的方式为科研和商业带来深刻变革。然而&#xff0c;对于许多专业人士而言&#xff0c;如何高效地处…

C++中的进程虚拟内存布局

进程虚拟内存布局 进程虚拟内存布局&#xff1a; .text&#xff08;代码段&#xff09;&#xff1a;存放的是程序源代码编译后的机器指令&#xff0c;是只读的。 .rodata&#xff08;只读数据段&#xff09;&#xff1a;存放的是程序中的只读数据&#xff0c;一般是程序里面的…