线程相关学习记录(2)

news2025/1/11 22:57:36

锁分类

按照对共享资源是否上锁,可以分为:

  • 乐观锁
  • 悲观锁

乐观锁的定义:

乐观锁是一种乐观思想,认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在提交修改的时候去判断一下,在此之前有没有其他的线程也修改了这个数据:
如果其他的线程还没有提交修改,那么当前线程就将自己修改的数据成功写入;如果其他的线程已经提交了修改,则当前线程会根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁在java中是采用CAS 算法实现的,具体表现就是juc包下的原子类。使用CAS算法的代码也被称为无锁编程(Lock-free)

悲观锁的定义:

悲观锁是一种悲观思想,认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

悲观锁在java中的实现就是synchronized和Lock接口的实现类。

上锁失败之后,是继续重试还是阻塞线程,可以分为:

  • 自旋锁
  • 适应性自旋锁

因为java中的线程其实是对应的操作系统中的线程,阻塞和唤醒一个线程是需要切换到内核态进行的,频繁的进行内核态和用户态的切换会带来性能消耗。因为CPU处理器的速度很快,可能发生线程刚进入阻塞,cpu就已经执行完了上一条指令,又需要再次切换内核态来唤醒线程。为了避免频繁的切换上下文,当后面的线程请求锁失败的时候,不会直接就阻塞,而是进行自旋操作,也就是做do-while操作,让cpu执行无意义的操作,等待共享资源的释放。

在这里插入图片描述

可以看到这里就有个while操作,但是不会一直就等待下去,超过了一定次数之后就会进入阻塞,默认是 10 次,可以使用参数 -XX:PreBlockSpin来更改。

而适应性自旋锁则是对自旋锁做出的更新,适应性自旋锁并不会自旋固定次数,而是会根据同一个锁同过自旋获取到锁的状态来判断是否进行自旋:

  • 如果在同一个锁上,自旋的线程获得了锁,并且正在运行,那么就会认为当前线程也会有很大概率获得锁,会进行自旋。
  • 如果在同一个锁上,很少有线程通过自旋获取到锁,那就不会进行自旋,直接阻塞。

根据共享资源的竞争激烈情况,对synchronized上锁分为四个状态:

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

无锁就是使用CAS算法来进行对共享资源的获取。

偏向锁就是会偏向被第一个获取锁的线程,如果在之后的执行过程中,第一个获取锁的线程一直持有这个锁,没有其它锁来进行竞争,那么第一个线程就不需要进行CAS操作来竞争锁。

当有其他线程来竞争这个锁,但是线程之间并不会发生冲突,而是交替持有锁,那么锁就会升级为轻量级锁,轻量级锁是不会尝试进行自旋操作的。

在多个线程尝试同时获取锁的时候,获取失败就会进行自旋操作,仍然获取失败就会升级为重量级锁,称为重量级锁是因为,当因为竞争锁失败而进入阻塞和唤醒阻塞的线程都是需要切换到内核态来操作的,比较消耗性能,所以称之为重量级锁。

是否根据排队顺序来选择获取锁:

  • 公平锁
  • 非公平锁

根据申请锁的顺序来进行排队,公平锁的整体效率会比较低,因为等待队列中除了队头的第一个线程以外,其他所有线程都会被阻塞住,而阻塞线程的唤醒需要操作系统陷入内核态。

非公平锁效率比公平锁高在于线程有可能不需要阻塞直接就能获取到锁,但是低优先级的线程可能就会饿死,一直没有执行机会。

根据锁是否可以重复使用:

  • 可重入锁
  • 不可重入锁

可重入锁在尝试获取同一个锁的时候,会直接成功,并且锁的重入次数会加一,当线程退出临界区的时候,锁的重入次数需要减一操作,只有锁的重入次数计量变为0,其他线程才可以重新获取这个锁。

根据锁是否可以被多个线程持有:

  • 排他锁
  • 共享锁

排他锁:也称互斥锁、独享锁,该锁一次只能被一个线程所持有

共享锁:该锁可被多个线程所持有

排他锁是一种悲观锁,每次访问资源都先加上排他锁,但是读操作其实并不会影响数据的一致性,而排他锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取,这限制了并发性。

共享锁是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。ReentrantReadWriteLock中的读锁ReadLock就是一种共享锁。

认识synchronized关键字

synchronized的三个特点:

  • 当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的该synchronized方法或者synchronized代码块的访问将被阻塞。
  • 当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程仍然可以访问该对象的非同步代码块。
  • 当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的其他的synchronized方法或者synchronized代码块的访问将被阻塞。
  /**
     * 相当于类锁
     */
    public static synchronized void test1() {

    }

    /**
     * 对象锁,锁的是当前实例对象,其他线程尝试获取这个对象的其余synchronized修饰的方法都会被阻塞
     */
    public synchronized void test2() {

    }


    /**
     * 锁的是tpf这个对象
     *
     * @param tpf
     */
    public void test3(TestPrintYield tpf) {
        synchronized (tpf) {

        }
    }

synchronized修饰的方法或代码块在编译之后生成的class文件,会多出两个指令:monitorente和monitorexit,这个是jvm提供的指令用来实现同步,monitorenter编译后是在同步代码块的开始的地方插入生成,monitorexit是在方法结束处或者抛出异常处插入生成。

实现同步的原理

在Hotspot虚拟机中,每个对象都是由对象头(Header),实例数据(Instance data)和对齐填充(padding)。

在这里插入图片描述

其中对象头中的Mark Word的描述如下:

HotSpot 虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 个比特和 64 个比特,官方称它为 “Mark Word”。

对象头中的Mark Word就存储了和每个对象关联的监视器,而synchronized编译生成的指令就是这样和监视器有关的指令,所以在synchronized修饰代码块中,可以将任意对象当作一个锁。

任何一个对象都与一个监视器(monitor)相关联。
当一个监视器有拥有者(owner)的时候,这个监视器就会被锁定(locked),所谓拥有者(owner)就是说执行 monitorenter的线程会尝试获得监视器的所有权,或者说尝试获得对象的锁。

另外,每个监视器都维护着一个自己被持有次数(或者说被锁住 locked)的计数器(count),具体如下:

  • 如果与对象关联的监视器的计数器为零,则线程进入监视器成为该监视器的拥有者,并将计数器设置为 1。
  • 当同一个线程再次进入该对象的监视器的时候,计数器会再次自增。
  • 当其他线程想获得该对象关联的监视器的时候,就会被阻塞住,直到该监视器的计数器为 0 才会再次尝试获得其所有权。

锁的内存语义

java官方文档中定义: 所有 Lock 实现都必须强制执行与内置监视器锁 synchronization 提供的相同的内存同步语义。

锁获取的内存语义

当线程想去获取共享资源的时候,JMM会将本地工作内存中的共享资源的副本置为无效,这样就必须要去主内存中获取变量的值。

锁释放的内存语义:

当释放锁的时候,会将工作内存中的数据刷新到主内存中。

final关键字

写 final 域的重排序规则

  • JMM 禁止编译器把对 final 域的写指令重排序到构造函数之外。
  • 编译器会在对 final 域的写指令之后,构造函数 return 之前,插入一个 StoreStore 屏障(这个屏障的作用就是禁止处理器把对 final 域的写指令重排序到构造函数之外)

读 final 域的重排序规则

  • 处理器:在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。
  • 编译器:编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。

有了读final域的重排序规则和写final域的重排序规则,就可以保证final域如果是正确构造的(被构造对象的引用在构造函数中没有“逸出”)),那么不需要使用同步(lock、volatile)就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。也就是对外部的可见状态永远都不会改变。

逃逸分析

所谓逃逸,包括方法逃逸和线程逃逸,线程逃逸的逃逸程度高于方法逃逸:

  • 当一个对象在方法里面被定义后,它如果被外部方法所引用(例如作为调用参数传递到其他方法中),这种称为方法逃逸;
  • 可能被外部其他线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
public class FinalReferenceEscapeTest {    
	final int i;    
	static FinalReferenceEscapeTest obj;    
	public FinalReferenceEscapeTest () {        
		i = 1;                   // 1. 写 final 域
		obj = this;              // 2. this 引用在此 "逸出"
	} 
    
    // 线程 A
	public static void writer() { 
		new FinalReferenceEscapeExample(); 
	} 
    
    // 线程  B
	public static void reader() { 
		if (obj != null) {      // 3 
			int temp = obj.i;   // 4 
		} 
	}
}

CAS的三大问题

  • ABA问题
  • 只能保证一个共享变量的原子操作
  • 循环开销大

ABA问题

CAS思想中主要的三个步骤就是读取,比较,更新;如果读取的值和比较的值一样,那就表示没有其他线程在修改,可以直接更新;否则就报错或者自旋重试。

但是可能比较的值,虽然是和读取的值是一样的,但是可能是经过两次更新的,也就是主内存中的共享变量值原来是A,但是被其他线程先改成B,又改成A, 但是这个在比较的过程中是没法知道的。

java中提供了一个类 AtomicStampedReference 来解决 ABA 问题,原子更新带有版本号的引用类型。这该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

只能保证一个共享变量的原子操作

从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,这样我们就可以把多个变量封装在一个对象里来进行 CAS 操作。

循环时间开销大

其解决办法就是使 JVM 支持底层指令 pause,这个指令的功能就是当自旋失败时让 CPU 睡眠一小段时间再继续自旋,其有两个作用:
1)降低读操作的频率;
2)避免在退出循环的时候因 内存顺序冲突(Memory OrderViolation) 而引起 CPU 流水线被清空(CPU PipelineFlush)。

Lock接口

java中的锁,除了synchronized关键字之外,就是指实现了Lock接口的类。Lock接口的实现基本上都和AQS(AbstractQueuedSynchronizer)有关。

队列同步器AQS中有一个volatile修饰的int变量,就是用来表示获取的锁状态,为0表示没有被获取,大于等于1表示重入了多次。还有一个内部类Node节点类,用来实现FIFO的双向队列。

AQS中区分两种模式,就是在Node节点中做区分,独占式和共享式。

AQS的子类中,只会是独占式或者式共享式其中一个,不会两者皆是。

在这里插入图片描述

AQS中的public final void acquire(int arg)方法式final修饰的,子类无法重写,这个方式就是获取锁的方法,但是其中调用的tryAcquire方法不是final 修饰的,并且在AQS只是抛出了异常,是用来让子类进行重写的。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

如果调用tryAcquire方法失败了,就会走到acquireQueued方法的参数addWaiter方法中:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在addWaiter方法中,有个if判断(if (pred != null)),如果当前的尾节点不为空,那就表示不用设置首节点,直接挂在当前的尾节点后面就行,如果为空,则进入enq方法,可以看到enq方法中有个for(;;)死循环方法,必须将传入的node节点挂到当前tail尾节点成功才可以退出。

具体来说,如果某个线程请求锁(共享资源)失败,则该线程就会被加入到 CLH 队列的末端。当持有锁的线程释放锁之后,会唤醒其后继节点,这个后继节点就可以开始尝试获取锁。

独占式

独占式获取锁

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

其中tryAcquire方法是开放给子类重写的获取锁的方法,在AQS中并没有具体实现这个方法,只是一个抛出异常的方法;如果获取失败,就会通过addWaiter方法,在CLH队尾追加一个构造的独占式节点,也就是上面说的有一个写死的for循环,除非追加节点成功。

    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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这里有个判断就是: p == head && tryAcquire(arg); 其中的head节点式CLH队列的头节点,并不会存储数据,只是一个开头,所以如果当前节点的前一个节点是头节点,那就说明上一个节点已经释放锁了,然后当前节点再去尝试获取独占锁,获取成功才开始执行。

在这里插入图片描述

独占式释放锁

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

其中的tryRelease方法也是开放给子类实现的,AQS并没有具体实现。释放成功,就会调用unparkSuccessor方法去唤醒下一个节点。

  • 在获取同步状态(锁)时,AQS 维护一个 CLH 双向队列,获取锁失败的线程都会通过 CAS 操作被加入到队列尾端,并且在队列中无限自旋等待获取锁;
  • 停止自旋(或者说被移除 CLH 队列)的条件是其前驱节点为头节点并且成功获取了独占锁;
  • 当前节点(线程)成功释放掉独占锁后,AQS 就会紧接着唤醒该节点的后继节点,这样,这个后继节点又会开始去尝试获取锁。循此往复。

共享式

共享式获取锁


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




 private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

tryAcquireShared方法式开放给子类实现的,如果返回值式负值,表示获取失败,为零表示后续尝试获取这个共享锁可能失败,为证正数表示后续的线程尝试获取这个共享锁可能成功;如果大于等于0,那就表示当前线程获取到锁了。

如果获取失败,就会调用doAcquireShared方法,先放到CLH队尾中,然后再自旋去尝试获取锁。

共享式释放锁

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




private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

也独占式的释放锁差不多,还是看不懂。

  • 在获取同步状态(锁)时,AQS 维护一个 CLH 双向队列,获取锁失败的线程都会通过 CAS 操作被加入到队列尾端,并且在队列中无限自旋等待获取锁;
  • 停止自旋(或者说被移除 CLH 队列)的条件是其前驱节点为头节点并且成功获取了共享锁;
  • 通过循环 + CAS 操作确保当前节点(线程)成功释放掉共享锁后,AQS 就会紧接着唤醒该节点的后继节点,这样,这个后继节点又会开始去尝试获取锁。循此往复。

ReentrantLock

ReentrantLock 类中有几个内部类,一个是继承了AQS的Sync类,还有两个继承了Sync的NonfairSync类和FairSync类,从名称上就知道这是非公平锁和公平锁。

获取锁

公平锁FairSync类中有重写的tryAcquire方法,也就是在AQS中提到过很多次的开放给子类重写的方法,非公平锁NonfairSync类中的tryAcquire方法则是调用的Sync类中的nonfairTryAcquire;这个方法写在Sync类中,而不是非公平锁NonfairSync类中。并且ReentrantLock 中的tryLock方法中,是没有区分非公平锁和公平锁的,直接调用的是Sync中的nonfairTryAcquire方法。

非公平锁的获取锁方法:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

公平锁的获取锁方法:

       protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    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;
            }
            return false;
        }

两个方法之间很相似,公平锁多了个hasQueuedPredecessors方法的判断,看方法名应该是判断是否有前置节点。

可以发现,ReentrantLock 无论是无参构造还是 tryLock 方法,使用的都是非公平锁的方式。这是为啥呢?因为非公平锁的性能更高。公平锁为了保证公平,保证按照时间顺序来获取锁,就必定要进行频繁的线程上下文切换,而非公平锁不需要,谁 CAS 成功了谁就能拿到锁,极少的线程切换保证了其更大的吞吐量。

可重入锁

ReentrantLock 是可重入锁,synchronized关键字也是可重入锁,但是ReentrantLock 是显式的。

可重入锁就实现的重点就是:

获取锁:锁需要去识别获取锁的线程和当前占据锁的线程是否是同一个,如果是,则重复成功获取

释放锁:如果某个线程重复 n 次获取了锁,则只有在第 n 次释放该锁后,其他线程才能够获取到该锁。这个实现其实很简单,用一个计数器(也就是代码中的同步状态 state)存储下锁被占用的次数,每释放一次就减 1 就行了

ReentrantLock 中使用的同步状态就是Sync类继承AQS得来的state字段。

ReentrantReadWriteLock

ReentrantLock和synchronized关键字都是排他锁,ReentrantReadWriteLock则是共享锁。
ReentrantReadWriteLock类只实现了ReadWriteLock接口,ReadWriteLock接口也很简单,就两个方法:

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();
}

看名称应该是获取读锁和写锁。

在这里插入图片描述

其中的读写锁其实就是两个内部类实现的,这两个内部类都实现了Lock接口。

 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 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);
        }
}

两个类中的lock方法和unlock方法可以看到调用了AQS中不同模式的获取,释放锁的方法。

Condition接口

在使用synchronized关键字的时候,可以使用wait方法,notify和notifyAll方法来挂起和唤醒线程,对应的在Reentrant接口中,使用Condition对象来实现线程的挂起和唤醒。

在这里插入图片描述

在AQS中,有一个ConditionObject类实现了Condition接口,可以看到在ConditionObject中也使用了Node这个对象,并且实现了一个FIFO的单向队列,叫做等待队列。一个Lock对象可以构建出多个Condition对象。

在同步队列中的节点,获取锁和释放锁的方法都会有CAS操作,但是Condition接口的await和signal,signalAll方法不需要,因为只有当前节点是头节点的下一个节点,也就是获取了锁的节点,才可以调用await方法和singal等方法。signal和synchronized关键字中的notify方法一样,也是不会释放锁的。

当前节点调用await方法,说明当前节点进入等待,也就是同步队列的首节点挂在了等待队列的尾节点,而调用signal则是唤醒其他节点去尝试获取锁,也就是将等待队列的头节点挂在了同步队列的尾节点。

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

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

相关文章

【通过点按其他按钮修改按钮的位置 Objective-C语言】

一、继续我们上一个例子 1.下面我们要实现的功能,是通过点按4个方向键,修改最上面那个图片按钮的位置 2.首先,我们把4个方向按钮实现一下 我先拽1个按钮上去,注意,我们不是1上来就拽6个按钮, 而是先拽1个按钮上去 我设置这1个按钮,大家看一下,这个按钮上有文字吗,…

盒子模型中box-sizing: border-box;的作用

我们都知道 盒子模型 它都是由 外边距(margin) 边框(border) 内边距(padding) 内容(content)组成的。 如果我们按照下面的设置&#xff0c;得到的是一个300*300大小的红色正方形 .box {height: 300px;width: 300px;background-color: red; } <body><div class&q…

如何使用阻塞队列BlockingQueue

文章目录什么是阻塞队列阻塞队列的特点BlockingQueue不是新的东西学会使用队列阻塞队列四组APISynchronousQueue 同步队列什么是阻塞队列 阻塞队列本质上还是一种队列&#xff0c;遵循先进先出&#xff0c;后进后出的原则&#xff0c;在此基础上&#xff0c;如果出队时阻塞队列…

BM32 合并二叉树

题目 已知两颗二叉树&#xff0c;将它们合并成一颗二叉树。合并规则是&#xff1a;都存在的结点&#xff0c;就将结点值加起来&#xff0c;否则空的位置就由另一个树的结点来代替。例如&#xff1a; 两颗二叉树是: Tree 1 Tree 2 合并后的树为 数据范围&#xff1a;树上节点数…

全网惟一面向软件测试人员的Python基础教程-在Python中元组有点何用?

全网惟一面向软件测试人员的Python基础教程 起点&#xff1a;《python软件测试实战宝典》介绍 第一章 为什么软件测试人员要学习Python 第二章 学Python之前要搞懂的道理 第三章 你知道Python代码是怎样运行的吗&#xff1f; 第四章 Python数据类型中有那些故事呢&#xff1f;…

批量设置限购

增加批量设置限购功能 第一步&#xff1a;打开后端文件app\services\product\product\StoreProductServices.php 在2046行下方增加 case 10: foreach ($ids as $product_id) { $batchData[] [ id > $product_id, is_limit > $data[is_limit], limit_type > $data[l…

【Linux】gdb基本调试命令

目录 调试程序 gdb命令 list显示代码 break加断点 查看断点位置 删除断点 run启动程序 n单步执行 p打印变量的值 q退出调试 c继续执行 s进入函数&#xff08;n单步执行遇到函数不会进入&#xff09; finish跳出函数 调试程序 可以生成可执行程序运行有误则去调试…

基于springboot+jpa 实现多租户动态切换多数据源 - 基于dynamic-datasource实现多租户动态切换数据源

多租户动态多数据源系列 1、基于springbootjpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表 2、基于springbootjpa 实现多租户动态切换多数据源 - 基于dynamic-datasource实现多租户动态切换数据源 3、基于springbootjpa 实现多租户动态切换多数据源 - 使用Fl…

java:jackson 四:Jackson Property Inclusion Annotations

java&#xff1a;jackson 四&#xff1a;Jackson Property Inclusion Annotations 1 前言 参考文档地址&#xff1a; https://www.baeldung.com/jacksonhttps://www.baeldung.com/jackson-annotationsSpringBoot自带的jackson版本如下&#xff1a; <parent><artif…

数据可视化,21-30岁消费增速最快,年轻人正在成长为白酒消费的主力

2022中国白酒消费报告 中国酿酒的发源距今已经有四千多年的历史&#xff0c;中国有很多酒”酒乡”贵州的茅台、四川泸州的国窖、四川宜宾的五粮液。人们常说“把酒言欢”&#xff0c;这不马上就要过春节了&#xff0c;过节送礼、家庭聚会都非常适合&#xff0c;小编使用在线数据…

VSCode插件大全

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录一、必备插件1.Chinese&#xff08;中文&#xff09;2.Settings Sync&#xff08;配置同步到云端&#xff09;二、效率插件1.HTML Snippets&#xff08;代码提示&…

观察者模式(observer pattern) / 发布-订阅模式(Publish-Subscribe)

一个遍历问题导致的低效率范例 #include <iostream> #include <thread> #include <vector> #include <list> #include <mutex> #include <chrono> #include <algorithm> #include <set> #include <queue>using namespa…

一个三臂非劣效性检验的包简介——“ThreeArmedTrials”

目录0引言1.模型分类2. R语言函数介绍2.1 函数总览2.2 GElesions函数&#xff08;数据集1&#xff09;2.3 opt_alloc_RET函数2.4 power_RET函数2.5 remission&#xff08;数据集2&#xff09;2.6 seizures函数&#xff08;数据集3&#xff09;2.7 T2lesions函数&#xff08;数据…

多线程基础部分

多线程基础部分1. 线程与进程的关系1.1 多线程启动1.2 线程标识1.2.1 Thread与Runnable1.3 线程状态2.线程池入门2.1 ThreadPoolExecutor2.2 创建线程池2.3 关闭线程池创建线程的几种方法参考1. 线程与进程的关系 1个进程包含1个或多个线程。 1.1 多线程启动 线程有两种启动…

骨传导耳机是怎么传声的、骨传导耳机的优点是什么

要说这两年最火的蓝牙耳机是哪款&#xff0c;大火的骨传导耳机绝对可以名列前茅&#xff0c;那可真是运动健身、需长时佩戴耳机党的神器&#xff01;如果你是搞运动的、健身的&#xff0c;或者是需要长时间佩戴耳机上网课的学生党&#xff0c;那一副靠谱的骨传导耳机绝对是必不…

LVGL学习笔记7 - GD32平台优化

目录 1. 颜色深度 2. 更新disp_init 3. 更新disp_flush 4. 改为IPA更新数据 5. 死机问题 学习过程中发现GD32平台的显示效果不佳&#xff0c;而且会出现死机的问题&#xff0c;需要优化一下平台代码。 1. 颜色深度 修改颜色深度为32bit。 #define LV_COLOR_DEPTH 32 2.…

时序引擎架构和实例演练

一、时序引擎介绍 开务数据库时序引擎是一款功能丰富、高性能的时序引擎&#xff0c;专为物联网、工业互联网、数字能源、金融等场景设计并优化。它能让大量设备、数据采集器每天产生的高达 TB 甚至 PB 级的数据得到高效实时的处理&#xff0c;对业务的运行状态进行实时的监测、…

银行卡数据标签的列举与使用

银行卡三要素&#xff1a;银行卡号、姓名、身份证号&#xff0c;银行卡四要素是指银行卡号、姓名、身份证号、手机号。对于从事信贷风控的小伙伴来讲&#xff0c;并不陌生。 银行卡信息的应用可能更熟悉的是客户信息核验&#xff0c;也就是针对信贷客户审批额度发放之前&#x…

SpringCloud系列(七)最详细最全面详述统一网关 Gateway

有道词典上对 Gateway 有大门口, 门道, 通道以及计算机术语中的网关之意, 其实对于网关这个概念是很好理解的, 例如有这样高档的小区车库, 当开车经过闸口的时候会识别你的车牌号, 识别成功后会自动将你的车库门打开; 其实计算机中的网关也是如此, 在 Spring Cloud 中网关的实现…

【1 - 决策树 - 原理部分】菜菜sklearn机器学习

课程地址&#xff1a;《菜菜的机器学习sklearn课堂》_哔哩哔哩_bilibili 第一期&#xff1a;sklearn入门 & 决策树在sklearn中的实现第二期&#xff1a;随机森林在sklearn中的实现第三期&#xff1a;sklearn中的数据预处理和特征工程第四期&#xff1a;sklearn中的降维算法…