深入理解ReentrantReadWriteLock源码

news2024/11/26 11:59:15

1. ReentrantReadWriteLock简介

之前我们介绍过ReentrantLock,它是基于AQS同步框架实现的,是一种可重入的独占锁。但是这种锁在读多写少的场景下,效率并不高。因为当多个线程在进行读操作的时候,实际上并不会影响数据的正确性。

因此针对读多写少的场景,java提供了ReentrantReadWriteLock(可重入读写锁)。读写锁允许同一时刻被多个读线程访问,但是当写线程在访问时,其他所有的读线程和写线程都会被阻塞

ReentrantReadWriteLock是包含读锁和写锁的,从代码中能够看到:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    public ReentrantReadWriteLock() {
        this(false);   // 默认非公平锁
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
}

读锁和写锁之间是存在一些关系的,具体如下:

  • 读锁和写锁之间是互斥关系;(当有线程持有读锁的时候,想尝试获得写锁的线程不能获得。当有线程持有写锁的时候,想尝试获得读锁的线程不能获得。)
  • 读锁和读锁之间是共享关系
  • 写锁和写锁之间是互斥关系

此外,我们从名字就能够知道,ReentrantReadWriteLock是支持重入的。但是它的重入和ReentrantLock的重入存在些不同。ReentrantReadWriteLock中可重入的含义是:

  • 如果一个线程获取了读锁,那么它可以再次获取读锁,但是不能获取写锁;
  • 如果一个线程获取了写锁,那么它可以获取写锁或读锁;

2. 锁的升降级

上面我们讲到,当一个线程获得了读锁,不能再重入写锁,这其实是涉及到锁的升降级。

ReentrantReadWriteLock不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的。如下测试代码:

public class Test1 {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        reentrantReadWriteLock.readLock().lock();
        System.out.println("get readlock");
        reentrantReadWriteLock.writeLock().lock();
        System.out.println("get writelock");
    }
}

输出结果:

get readlock

可以看到,在获取了读锁之后,尝试获取写锁是不行的。因此ReentrantReadWriteLock是不支持锁升级的。因为可能其他线程同时持有读锁,而读写锁之间是互斥的,锁升级可能会造成冲突。

下面我们再看一下锁降级的测试代码:

public class Test2 {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        reentrantReadWriteLock.writeLock().lock();
        System.out.println("get writelock");
        reentrantReadWriteLock.readLock().lock();
        System.out.println("get readlock");
    }
}

输出结果

get writelock
get readlock

可以看到,在获取了写锁之后,尝试获取读锁是可以的。因此,ReentrantReadWriteLock是支持锁降级的。因为当该线程持有写锁的时候,肯定不会有其他线程同时持有写锁或读锁,锁降级不存在冲突。

3. 公平锁和非公平锁

ReentrantReadWriteLock中有一个Sync的静态内部内,继承自AQS,重写了AQS中需要重写的方法,因为这在公平锁和非公平锁中都是一样的。但是留下了两个抽象的方法等待公平锁和非公平锁的不同实现:

abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract boolean readerShouldBlock();
    abstract boolean writerShouldBlock();

这两个方法的作用是根据公平和非公平,分别判断当前尝试获得读锁和写锁的线程是否有资格去尝试获得锁。返回true就是没资格,返回false就是有资格。归纳如下:
公平模式

  • 无论当前线程请求写锁还是读锁,只要发现此时还有别的线程在同步队列中等待(写锁or读锁),都一律选择让步,没有资格去竞争锁。

非公平模式

  • 请求写锁时,当前线程会选择直接竞争,不会做丝毫的让步
  • 请求读锁时,如果发现同步队列队首线程在等待获取写锁,则会让步。不过这是一种启发式算法,因为写线程可能排在其他读线程后面。这种方式是尽可能避免饥饿。

接下来在加锁代码中会用到。

对于非公平锁,具体实现在NonfairSync内部类中:

static final class NonfairSync extends Sync {
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

// AQS
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&   // 判断头节点的下一个节点是否尝试获得写锁
        s.thread != null;
}

从上面代码中可以看到,在非公平锁中,如果是尝试获取写锁的线程,就肯定有资格,直接返回false。如果是尝试获取读锁的线程,会去判断头节点的下一个节点是否是尝试获得写锁,这样能够避免写锁线程饥饿。

对于公平锁,具体实现在FairSync内部类中:

static final class FairSync extends Sync {
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

// AQS
public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

从上面代码中可以看到,在公平锁中,不论尝试获取写锁还是读锁,都是调用hasQueuedPredecessors判断同步队列中是否有前驱,如果有前驱,就乖乖去排队。

4. 写锁代码分析

4.1 加锁

写锁中的加锁代码是lock()函数,又会进一步调用AQS中的acquire函数:

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

    // AQS
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

接着会调用tryAcquire函数,因为这个函数在AQS中是空方法,所以在ReentrantReadWriteLock.Sync中重写了这个方法,尝试获取锁。如果获取锁失败,就会进入同步队列进行排队。我们接下来看ReentrantReadWriteLock.Sync.tryAcquire方法:

    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();  //当前线程
        int c = getState();  // 同步状态值
        int w = exclusiveCount(c);  // 独占锁的重数
        if (c != 0) {  // 如果有线程当前持有锁
            // w!=0意味着当前持有锁的是写锁,如果不是当前线程持有写锁则重入失败
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            // 如果超过了最大重入数
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // 更新重入数
            setState(c + acquires);
            return true;  // 加锁成功
        }
        if (writerShouldBlock() ||    // 是否有资格去尝试获得锁
            !compareAndSetState(c, c + acquires)) // CAS更新state
            return false;
        setExclusiveOwnerThread(current);  // 更新成功则设置当前线程独占
        return true;
    }

在这个方法中,比较重要的方法是exclusiveCount,这个方法是用来判断当前持有写锁的重数。方法如下:

static final int SHARED_SHIFT   = 16;
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
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; }

可以看到,exclusiveCount方法是将state和EXCLUSIVE_MASK进行相与。而EXCLUSIVE_MASK为1左移16为然后减1,即为0X0000FFFF。两者相与之后,取得同步状态的低16位,就是写锁被获取的次数。

而sharedCount方法是将state无符号右移16位,即取同步状态的高16位,表示读锁被获取的次数。具体如下图所示:
在这里插入图片描述

我们再回到tryAcquire方法,写锁的加锁逻辑就是:如果当前读锁被其他线程占有,或写锁被其他线程占有,则加锁失败。否则加锁或重入锁成功。

4.2 解锁

解锁是调用unlock方法,又会进一步调用AQS中的release方法:

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

    //AQS
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

在AQS中的tryRelease方法是空方法,需要自定义的同步器进行实现,具体作用就是尝试解锁。如果解锁成功就会继续唤醒后继线程。接下来我们进入到ReentrantReadWriteLock.Sync.tryRelease方法:

    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())   // 是否当前线程持有写锁
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;   // 减少重数
        boolean free = exclusiveCount(nextc) == 0;  // 判断写锁完全释放
        if (free)
            setExclusiveOwnerThread(null);  
        setState(nextc);    // 更新state
        return free;
    }

这个方法就比较简单,和ReentrantLock释放锁的逻辑差不多。唯一的不同是通过exclusiveCount方法来获取独占锁重入次数的方式不同。

4.3 尝试获得锁

尝试获得锁会调用tryLock方法,这个方法和lock方法的区别在于tryLock是去尝试,拿到就返回true,拿不到就返回false。而lock方法拿不到会一直等待。tryLock代码如下:

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

tryLock又会继续调用tryWriteLock方法:

    final boolean tryWriteLock() {
        Thread current = Thread.currentThread();  // 当前线程
        int c = getState();  // 同步状态
        if (c != 0) {  // 如果当前有线程获取到锁
            int w = exclusiveCount(c);  // 独占锁重入次数
            // 如果是有线程持有读锁或者持有写锁的不是当前线程,就返回false
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            // 如果已经到了最大重入数
            if (w == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
        }
        // CAS更新state
        if (!compareAndSetState(c, c + 1))
            return false;
        setExclusiveOwnerThread(current);  // 设置线程独占
        return true;
    }

这个方法的逻辑我们看到是和tryAcquire方法差不多,唯一的区别在于tryAcquire方法中有writerShouldBlock去判断是否有资格。

5. 读锁代码分析

5.1 加锁

加锁调用的是lock函数,又会进一步调用AQS中的acquireShared:

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

    // AQS
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

在acquireShared函数中,会调用tryAcquireShared去尝试获得共享锁。如果获取失败,就会调用doAcquireShared去继续尝试获得锁。在AQS中的tryAcquireShared函数是空方法,所以ReentrantReadWriteLock.Sync进行了重写:

    protected final int tryAcquireShared(int unused) {
        Thread current = Thread.currentThread();  // 当前线程
        int c = getState();  // 获取state
        if (exclusiveCount(c) != 0 &&  // 如果发现有线程持有独占锁
            getExclusiveOwnerThread() != current) // 且不是当前线程持有独占锁
            return -1;  // 获取读锁失败
        int r = sharedCount(c);  // 共享锁的重数
        if (!readerShouldBlock() &&  // 是否有资格去获取锁
            r < MAX_COUNT &&  // 是否超过重数
            compareAndSetState(c, c + SHARED_UNIT)) {  // 重入
            if (r == 0) {  // 如果本身没有线程持有读锁
                firstReader = current;  // 设置第一个读线程为当前线程
                firstReaderHoldCount = 1;  // 第一个读线程持有重数为1
            } else if (firstReader == current) {  // 如果本身就是当前线程持有
                firstReaderHoldCount++;  // 更新重数
            } else {
                HoldCounter rh = cachedHoldCounter;  // 获取缓存的读线程重数
                // 如果没有设置过,或cache不是当前线程
                if (rh == null || rh.tid != getThreadId(current)) 
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                // 完全释放读锁时,会将holdCounter从ThreadLocal移除,这里重新放入
                    readHolds.set(rh);  
                rh.count++;  // 重入次数增加
            }
            return 1;  // 加锁成功
        }
        // 解决读锁重入会因为readerShouldBlock方法重入失败的问题
        return fullTryAcquireShared(current);
    }

在这个方法中,我们可以看到,当有线程持有写锁的时候,读锁肯定是无法进行加锁的。此外在这个函数中,使用到了cachedHoldCounter这个变量,用来保存最近一次加读锁线程的重数。我们来看下相关代码:

    static final class HoldCounter {
        int count = 0;
        // Use id, not reference, to avoid garbage retention
        final long tid = getThreadId(Thread.currentThread());
    }

    static final class ThreadLocalHoldCounter
        extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }

    private transient ThreadLocalHoldCounter readHolds;

    private transient HoldCounter cachedHoldCounter;

HoldCounter包含两个成员变量,分别是count和tid,用来记录读锁的重数和线程id。因为sharedCount只能反映所有读锁线程共同的重数,所以需要一个变量来存储每个线程分别持有读锁的重数。所以这里引入了readHolds这个变量,它是ThreadLocalHoldCounter,是我们之前讲过的ThreadLocal类型的,相当于每个线程都会拥有各自的HoldCounter类型变量,保存了各自的读锁加锁重数,正符合我们的要求。

而cachedHoldCounter相当于是一个缓存,用来记录最近一次加读锁线程的重数。因为每次去readHolds是需要消耗时间的,通过这个缓存可以减少一定量的时间。

我们再回到tryAcquireShared方法,他的逻辑就是:判断是否有线程持有写锁,如果有的话就加锁失败。如果没有,就去尝试获得锁。但是因为readerShouldBlock会导读线程没办法重入,所以就会进入fullTryAcquireShared去解决这个问题

    final int fullTryAcquireShared(Thread current) {
        HoldCounter rh = null;
        for (;;) {
            int c = getState();  // 获取state
            // 如果有线程持有写锁,就加锁失败
            if (exclusiveCount(c) != 0) {
                if (getExclusiveOwnerThread() != current)
                    return -1;
            } else if (readerShouldBlock()) {  // 如果没有资格去获得锁
                // Make sure we're not acquiring read lock reentrantly
                // 如果当前线程是第一个线程,那么在tryAcquiredShared中肯定已经重入了
                if (firstReader == current) {  
                    // assert firstReaderHoldCount > 0;
                } else {  // 如果第一个读线程不是当前线程,在tryAcquireShared无法重入
                    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");
            // 和tryAcquireShared方法实现一样
            if (compareAndSetState(c, c + SHARED_UNIT)) {
                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; // cache for release
                }
                return 1;
            }
        }
    }

能进入到fullTryAcquireShared只有两种可能:CAS失败、非第一个读线程重入失败。可以看到fullTryAcquireShared的实现逻辑和tryAcquiredShared是基本相同的,除了没有readerShouldBlock函数。

5.2 解锁

解锁方法会调用unlock函数,又会进一步调用AQS中的releaseShared方法:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

releaseShared中的tryReleaseShared函数就是尝试释放锁,然后唤醒后继线程。AQS中的tryReleaseShared是一个空函数,所以就会调用ReentrantReadWriteLock.Sync的tryReleaseShared函数:

    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();  // 当前线程
        if (firstReader == current) {   // 如果第一个读线程是当前线程
            // assert firstReaderHoldCount > 0;
            if (firstReaderHoldCount == 1)  // 读锁重入数为1
                firstReader = null;  // 释放
            else
                firstReaderHoldCount--;  // 重入数-1
        } else {   // 如果第一个读线程不是当前线程
            HoldCounter rh = cachedHoldCounter; // 获取缓存
            if (rh == null || rh.tid != getThreadId(current))
                rh = readHolds.get();
            int count = rh.count;  // 当前线程持有读锁的重数
            if (count <= 1) {  // 如果重数为1
                readHolds.remove();  // readHolds中删除
                if (count <= 0)
                    throw unmatchedUnlockException();
            }
            --rh.count;  // 读锁重数-1
        }
        for (;;) {
            int c = getState();   // 获取state
            int nextc = c - SHARED_UNIT;  // state-1
            if (compareAndSetState(c, nextc))  // CAS更新state值
                // Releasing the read lock has no effect on readers,
                // but it may allow waiting writers to proceed if
                // both read and write locks are now free.
                return nextc == 0;  // 如果完全释放,就返回true
        }
    }

从tryReleaseShared方法中可以看到,分为两部分:更新readHolds、更新state。因为释放锁,不仅仅会减少当前线程的读锁重数(readHolds),也要减少全局读锁重数(state)。

5.3 尝试加锁

tryLock方法和之前说的一样,只会进行一次尝试,成功就返回true,失败就返回false:

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

接下来进一步调用tryReadLock方法:

    final boolean tryReadLock() {
        Thread current = Thread.currentThread();  // 当前线程
        for (;;) {
            int c = getState();  // 获取state
            // 如果存在独占锁,且独占锁不是当前线程,加读锁失败
            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)) {  // CAS更新state
                if (r == 0) {  // 如果之前没有人持有读锁
                    firstReader = current;  // 设置第一个读线程为当前线程
                    firstReaderHoldCount = 1;  // 读锁重数为1
                } else if (firstReader == current) { // 如果当前线程就是第一个持有读锁的线程
                    firstReaderHoldCount++;  // 第一个线程读锁重数+1
                } else {
                    // 更新readHolds
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;  // 读锁持有重数+1
                }
                return true;  // 加锁成功
            }
        }
    }

这个方法的基本逻辑和tryAcquiredShared是差不多的,只是少了readerShouldBlock

参考文章:
全网最详细的ReentrantReadWriteLock源码剖析(万字长文)
深入理解读写锁ReentrantReadWriteLock

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

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

相关文章

分享5款小众软件,大家按需下载

今天推荐一些可以大幅度提升办公效率的小软件&#xff0c;安全无毒&#xff0c;下载简单&#xff0c;最重要的是没有广告&#xff01; 1.进程调试——Process Lasso Process Lasso是一款独特的调试进程级别的系统优化工具 &#xff0c;主要功能是基于其特别的算法动态调整各个…

Linux——文件系统inode与软硬链接

目录 一.inode &#xff08;一&#xff09;.背景知识 &#xff08;二&#xff09;.inode 二.软硬链接 &#xff08;一&#xff09;.软链接 &#xff08;二&#xff09;.硬链接 一.inode &#xff08;一&#xff09;.背景知识 我们知道&#xff0c;磁盘是按磁道与扇区划分…

广告行业中那些趣事系列58:当我们面对文本分类任务的时,可以使用哪些优化策略...

导读&#xff1a;本文是“数据拾光者”专栏的第五十七篇文章&#xff0c;这个系列将介绍在广告行业中自然语言处理和推荐系统实践。本篇主要总结了一下我在实际项目中对于文本分类任务的优化策略&#xff0c;对于想要提升线上文本分类任务效果的小伙伴可能有所帮助。欢迎转载&a…

专访 | 罗成:开源并非“只可远观”

OpenMLDB: 请先来一段自我介绍吧。 罗成&#xff1a; 我是罗成&#xff0c;来自华中科技大学&#xff0c;目前研二在读&#xff0c;研究方向是云原生数据库的架构研究。 OpenMLDB: 请问是什么样的契机让你接触到了 OpenMLDB 呢&#xff1f; 罗成&#xff1a; 当时课余时间比…

skywalking链路追踪整合spring-cloud

skywalking安装资料 一、安装skywalking 将apache-skywalking-apm-bin.zip上传到/opt目录下2. 解压apache-skywalking-apm-bin.zip unzip apache-skywalking-apm-bin.zip&#xff0c;解压后即可使用&#xff0c;里面的配置文件都提前配置好了 3. 启动skywalking 进入apache-…

HTML做一个传统节日端午节 带设计报告4500字

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

征文连载丨在不修改数据库源码的情况下,如何提高 MogDB 对 Oracle 的兼容性?...

&#xff1a; 2022年8月4日至9月9日&#xff0c;墨天轮社区联合云和恩墨发起了首届「MogDB 主题征文活动」&#xff0c;邀请各位技术从业者学习、使用 MogDB 数据库&#xff0c;分享使用心得与实战案例&#xff0c;一起探索这款融合了众多创新特性的商业版企业级数据库。活动期…

[附源码]Python计算机毕业设计Django校园租赁系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Docker安装Jenkins

docker安装&#xff1a;可参考这里 拉取Jenkins镜像 docker pull jenkins/jenkins创建挂载目录并赋予777权限 mkdir /data/jenkins chmod 777 /data/jenkins运行Jenkins容器并设置名字、端口、目录、时间 50000:50000:前面端口是宿机的端口&#xff0c;&#xff1a;后面端口…

客服如何维护客户?

有网店工作经验的客服&#xff0c;都知道维护老客户的重要性&#xff0c;因为老客户关乎着商品的销量、店铺的升级、客户转化率的多少&#xff0c;因此客服需要通过好的售后服务留住大量的老客户。 前言 有网店工作经验的客服&#xff0c;都知道维护老客户的重要性&#xff0c…

LIBTIFF读取tiff文件时,打印buf出错

如图所示&#xff0c;按照官网提供的例程读取tiff文件&#xff0c;并打印读取的值时&#xff0c;提示&#xff1a; Subscript of pointer to incomplete type void 代码如下&#xff1a; //---打开tiff文件的测试TIFF* tif TIFFOpen("a.tif", "r");if(tif…

使用docker安装RocketMQ

1.创建namesrv服务拉取镜像docker pull rocketmqinc/rocketmq创建namesrv数据存储路径mkdir -p /docker/rocketmq/data/namesrv/logs /docker/rocketmq/data/namesrv/store构建namesrv容器docker run -d \ --restartalways \ --name rmqnamesrv \ -p 9876:9876 \ -v /docker…

如何用vue+免费的webdb 实现一个世界杯足球竞猜系统

一、前言 最近世界杯在如火如荼的进行。我们都知道&#xff0c;中国也派出了我们的一支强大的队伍&#xff1a;中国建筑队&#xff0c;全程参与了世界杯的所有比赛。 哈哈开个玩笑&#xff0c;不过说到世界杯&#xff0c;还真有不少朋友&#xff0c;不仅仅是看球&#xff0c;…

[附源码]Python计算机毕业设计Django疫情防控管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

手把手教你成为荣耀开发者:数据报表使用指引

荣耀开发者服务平台是荣耀面向开发者的统一生态入口&#xff0c;通过聚合周边内外部系统&#xff0c;分全球多站点部署&#xff0c;为全球开发者提供业务全生命周期的商业支撑服务&#xff0c;拥有应用分发、智慧服务、开放能力、HONOR Connect等众多业务等您来合作。 “数据报…

Partial differential equation

In mathematics, a partial differential equation (PDE) is an equation which imposes relations between the various partial derivatives of a multivariable function. The function is often thought of as an “unknown” to be solved for, similarly to how x is th…

【三维目标检测】SASSD(一)

SASSD是用于点云三维目标检测模型算法&#xff0c;发表在CVPR 2020《Structure Aware Single-stage 3D Object Detection from Point Cloud》&#xff0c;论文地址为“https://www4.comp.polyu.edu.hk/~cslzhang/paper/SA-SSD.pdf”。SASSD与基于Anchor的目标检测模型的结构基本…

Kafka(四)- Kafka 生产者

文章目录一、生产者消息发送流程1. 发送流程2. 生产者重要参数列表二、异步发送 API1. 普通异步发送2. 带回调函数的异步发送3. 同步发送三、生产者分区1. 分区好处2. 生产者发送消息的分区策略&#xff08;1&#xff09;默认的分区器 DefaultPartitioner&#xff08;2&#xf…

行业沙龙第四期丨企业供应链协同的数字化解痛之道

当前&#xff0c;数字经济正在蓬勃发展&#xff0c;我们正在迈向一个协同一体的全球化时代。所谓协同&#xff0c;协者&#xff0c;众和之同也&#xff0c;多方能够协作才能达到一个共同的目标。其中&#xff0c;数据是实现协同的基本要素&#xff0c;技术是实现协同重要途径&a…

网络安全 核心基础篇总结

目录 前言 网络安全三大要素 CIA含义 1. Confidentiality&#xff08;机密性&#xff09; 1.1 机密性主要三个解决方法 1.1.1 加密 1.1.2 权限管理 1.1.3 敏感信息暴露 2. Integrity&#xff08;完整性&#xff09; 3. Availabitity&#xff08;可用性&#xff09; 威胁…