Java高并发编程卷二(二) 锁

news2024/11/18 20:44:28

文章目录

  • 三 锁
    • 3.1 偏向锁
      • 3.1.1 为啥需要偏向锁?
      • 3.1.2 原理
      • 3.1.3 解释
      • 3.1.4 案例
      • 3.1.5 撤销与膨胀
        • 3.1.5.1 撤销
        • 3.1.5.2 膨胀
    • 3.2 轻量级锁
      • 3.2.1 为啥需要轻量级锁?
      • 3.2.2 原理
      • 3.2.3 案例
      • 3.2.4 分类
      • 3.2.5 膨胀
    • 3.3 重量级锁
      • 3.3.1 原理
      • 3.3.2 对象监视器详细介绍
        • 3.3.2.1 Cxq
        • 3.3.2.2 EntryList
        • 3.3.2.3 OnDeck Thread与Owner Thread
        • 3.3.2.4 WaitSet
      • 3.3.3 代码案例
    • 3.4 总结

三 锁

锁.xmind

3.1 偏向锁

3.1.1 为啥需要偏向锁?

在实际场景中,如果一个同步块(或方法)没有多个线程竞争,而且总是由同一个线程多次重入获取锁,如果每次还有阻塞线程,唤醒CPU从用户态转为核心态,那么对于CPU是一种资源的浪费,为了解决这类问题,就引入了偏向锁的概念。

3.1.2 原理

  • 如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,此时Mark Word的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完成)。
  • 以后该线程获取锁时判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。

3.1.3 解释

  • 无竞争时,之前获得锁的线程再次获得锁时会判断偏向锁的线程ID是否指向自己。
  • 如果是,那么该线程将不用再次获得锁,直接就可以进入同步块;如果未指向当前线程,当前线程就会采用CAS操作将Mark Word中的线程ID设置为当前线程ID,如果CAS操作成功,那么获取偏向锁成功,执行同步代码块,如果CAS操作失败,那么表示有竞争,抢锁线程被挂起,撤销占锁线程的偏向锁,然后将偏向锁膨胀为轻量级锁。

3.1.4 案例

package com.shu.BiasedLocking;

import com.shu.Lock.ObjectLock;
import org.openjdk.jol.vm.VM;

import java.util.concurrent.CountDownLatch;

/**
* @description:
* @author: shu
* @createDate: 2022/11/15 14:09
* @version: 1.0
*/
public class BiasedLockingDemo {
    static final int MAX_TREAD = 10;
    static final int MAX_TURN = 1000;

    CountDownLatch latch = new CountDownLatch(MAX_TREAD);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("JVM信息:\n"+VM.current().details());
        //JVM延迟偏向锁
        Thread.sleep(5000);
        ObjectLock lock = new ObjectLock();
        System.out.println("抢占锁前, lock 的状态: ");
        lock.printObjectStruct();
        Thread.sleep(5000);
        CountDownLatch latch = new CountDownLatch(1);
        Runnable runnable = () ->
            {
                for (int i = 0; i < MAX_TURN; i++) {
                    synchronized (lock) {
                        lock.increase();
                        if (i == MAX_TURN / 2) {
                            System.out.println("占有锁, lock 的状态: ");
                            lock.printObjectStruct();
                            //读取字符串型输入,阻塞线程
                            //                        Print.consoleInput();
                        }
                    }
                    //每一次循环等待10ms
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                latch.countDown();
            };
        new Thread(runnable, "biased-demo-thread").start();
        //等待加锁线程执行完成
        latch.await();
        System.out.println("释放锁后, lock 的状态:");
        lock.printObjectStruct();

    }
}







  • 线程启动之前

image.png

  • 线程启动中

image.png

 05 10 28 26 (00000101 00010000 00101000 00100110) (640159749)
 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
转换一下小端序:
 64 bit 
(00000000 00000000 00000000 00000000 00100110 00101000 00010000 00000101)
我们参考之前MarkWord的偏向锁结构	
解析字节

线程ID 54 bit 00000000 00000000 00000000 00000000 00100110 00101000 001000
epoch 2bit 00
未使用 1bit 0
分代年龄 4bit 0000
biased 1bit 1
locl 3bit 01
  • 线程释放之后

image.png

注意

虽然抢锁的线程已经结束,但是ObjectLock实例的对象结构仍然记录了其之前的偏向线程ID,其锁状态还是偏向锁状态101

3.1.5 撤销与膨胀

JVM 安全点

OpenJDK官方定义如下:

  • 安全点是在程序执行期间的所有GC Root已知并且所有堆对象的内容一致的点。
  • 从全局的角度来看,所有线程必须在GC运行之前在安全点阻塞。 (作为一种特殊情况,运行JNI代码的线程可以继续运行,因为它们只使用句柄。但在安全点期间,它们必须阻塞而不是加载句柄的内容。)
  • 从本地的角度来看,安全点是一个显着的点,它位于执行线程可能阻止GC的代码块中。 大多数调用点都能当做安全点。
  • 在每个安全点都存在强大的不变量永远保持true不变,而在非安全点可能会被忽视。 编译的Java代码和C / C ++代码都在安全点之间进行了优化,但跨安全点时却不那么优化。 JIT编译器在每个安全点发出GC映射。 VM中的C / C ++代码使用程式化的基于宏的约定(例如,TRAPS)来标记潜在的安全点。
  • 总的来说,安全点就是指,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。

3.1.5.1 撤销

  • 在一个安全点停止拥有锁的线程。
  • 遍历线程的栈帧,检查是否存在锁记录。
  • 如果存在锁记录,就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的Mark Word,清除其线程ID。
  • 将当前锁升级成轻量级锁。
  • 唤醒当前线程。

3.1.5.2 膨胀

  • 如果偏向锁被占据,一旦有第二个线程争抢这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到内置锁偏向状态,这时表明在这个对象锁上已经存在竞争了。JVM检查原来持有该对象锁的占有线程是否依然存活,如果挂了,就可以将对象变为无锁状态,然后进行重新偏向,偏向为抢锁线程。
  • 如果JVM检查到原来的线程依然存活,就进一步检查占有线程的调用堆栈是否通过锁记录持有偏向锁。如果存在锁记录,就表明原来的线程还在使用偏向锁,发生锁竞争,撤销原来的偏向锁,将偏向锁膨胀(INFLATING)为轻量级锁。

3.2 轻量级锁

引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过CAS机制竞争锁减少重量级锁产生的性能损耗。重量级锁使用了操作系统底层的互斥锁(Mutex Lock),会导致线程在用户态和核心态之间频繁切换,从而带来较大的性能损耗。

3.2.1 为啥需要轻量级锁?

  • 轻量锁存在的目的是尽可能不动用操作系统层面的互斥锁,因为其性能比较差。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁地阻塞和唤醒对CPU来说是一件负担很重的工作。
  • 同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了轻量级锁。
  • 轻量级锁是一种自旋锁,因为JVM本身就是一个应用,所以希望在应用层面上通过自旋解决线程同步问题。

3.2.2 原理

  • 在抢锁线程进入临界区之前,如果内置锁(临界区的同步对象)没有被锁定,JVM首先将在抢锁线程的栈帧中建立一个锁记录(Lock Record),用于存储对象目前Mark Word的拷贝

epub_38103745_41.jpg

  • 然后抢锁线程将使用CAS自旋操作,尝试将内置锁对象头的Mark Word的ptr_to_lock_record(锁记录指针)更新为抢锁线程栈帧中锁记录的地址,如果这个更新执行成功了,这个线程就拥有了这个对象锁。
  • 然后JVM将Mark Word中的lock标记位改为00(轻量级锁标志),即表示该对象处于轻量级锁状态。抢锁成功之后,JVM会将Mark Word中原来的锁对象信息(如哈希码等)保存在抢锁线程锁记录的Displaced Mark Word(可以理解为放错地方的Mark Word)字段中,再将抢锁线程中锁记录的owner指针指向锁对象。

epub_38103745_42.jpg

3.2.3 案例

package com.shu.BiasedLocking;

import com.shu.Lock.ObjectLock;
import org.openjdk.jol.vm.VM;

import java.util.concurrent.CountDownLatch;

/**
* @description: 轻量级锁
* @author: shu
* @createDate: 2022/11/16 11:16
* @version: 1.0
*/
public class LightWeightLockingDemo {
    static final int MAX_TURN = 1000;


    public static void main(String[] args) throws InterruptedException {

        System.out.println("JVM信息:\n"+VM.current().details());
        //JVM延迟偏向锁
        Thread.sleep(5000);

        ObjectLock lock = new ObjectLock();

        System.out.println("抢占锁前, lock 的状态: ");
        lock.printObjectStruct();

        Thread.sleep(5000);
        CountDownLatch latch = new CountDownLatch(2);
        Runnable runnable = () ->
            {
                for (int i = 0; i < MAX_TURN; i++) {
                    synchronized (lock) {
                        lock.increase();
                        if (i == 1) {
                            System.out.println("第一个线程占有锁, lock 的状态: ");
                            lock.printObjectStruct();
                        }
                    }

                }
                //循环完毕
                latch.countDown();

                //线程虽然释放锁,但是一直存在
                for (int j = 0; ; j++) {
                    //每一次循环等待1ms
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            };
        new Thread(runnable).start();


        Thread.sleep(1000);

        Runnable lightweightRunnable = () ->
            {
                for (int i = 0; i < MAX_TURN; i++) {
                    synchronized (lock) {
                        lock.increase();
                        if (i == MAX_TURN / 2) {
                            System.out.println("第二个线程占有锁, lock 的状态: ");
                            lock.printObjectStruct();
                        }
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
                //循环完毕
                latch.countDown();
            };
        new Thread(lightweightRunnable).start();
        //等待加锁线程执行完成
        latch.await();
        Thread.sleep(2000);  //等待2s
        System.out.println("释放锁后, lock 的状态: ");
        lock.printObjectStruct();

    }
}

  • 分析结果

偏向锁状态image.png
无竞争锁状态
image.png
存在锁竞争状态
image.png

(10001000 11110010 00010111 00100110)
(00000000 00000000 00000000 00000000)
转换为小端序 00000000 00000000 00000000 00000000 00100110 00010111 11110010 10001000
参考MarkWord 轻量级锁结构
ptr_to_lock_record 62bit  00000000 00000000 00000000 00000000 00100110 00010111 11110010 100010
lock 2bit 00

释放锁之后
image.png

3.2.4 分类

普通自旋锁

  • 所谓普通自旋锁,就是指当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁。
  • 锁在原地循环等待的时候是会消耗CPU的,就相当于在执行一个什么也不干的空循环。所以轻量级锁适用于临界区代码耗时很短的场景,这样线程在原地等待很短的时间就能够获得锁了。默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin选项来进行更改。

自适应自旋锁

  • 所谓自适应自旋锁,就是等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
  • 如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间。
  • 如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

JDK 1.6的轻量级锁使用的是普通自旋锁,且需要使用-XX:+UseSpinning选项手工开启。JDK 1.7后,轻量级锁使用自适应自旋锁,JVM启动时自动开启,且自旋时间由JVM自动控制。

3.2.5 膨胀

轻量级锁的本意是为了减少多线程进入操作系统底层的互斥锁(Mutex Lock)的概率,并不是要替代操作系统互斥锁。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁。

3.3 重量级锁

在JVM中,每个对象都关联一个监视器,这里的对象包含Object实例和Class实例。监视器是一个同步工具,相当于一个许可证,拿到许可证的线程即可进入临界区进行操作,没有拿到则需要阻塞等待。重量级锁通过监视器的方式保障了任何时间只允许一个线程通过受到监视器保护的临界区代码。

3.3.1 原理

  • JVM中每个对象都会有一个监视器,监视器和对象一起创建、销毁。
  • 监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块。
  • (1)同步。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
  • (2)协作。监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。

ObjectMonitor:监视器是由C++类ObjectMonitor实现

//Monitor结构体
ObjectMonitor::ObjectMonitor() {  
    _header      = NULL;  
    _count       = 0;  
    _waiters     = 0,  

        //线程的重入次数
        _recursions  = 0;      
    _object       = NULL;  

    //标识拥有该Monitor的线程
    _owner        = NULL;   

    //等待线程组成的双向循环链表
    _WaitSet             = NULL;  
    _WaitSetLock  = 0 ;  
    _Responsible  = NULL ;  
    _succ                = NULL ;  

    //多线程竞争锁进入时的单向链表
    cxq                  = NULL ; 
    FreeNext             = NULL ;  

    //_owner从该双向循环链表中唤醒线程节点
    _EntryList           = NULL ; 
    _SpinFreq            = 0 ;  
    _SpinClock           = 0 ;  
    OwnerIsThread = 0 ;  
}

3.3.2 对象监视器详细介绍

Cxq、EntryList、WaitSet这三个队列的说明如下:
(1)Cxq:竞争队列(Contention Queue),所有请求锁的线程首先被放在这个竞争队列中。(2)EntryList:Cxq中那些有资格成为候选资源的线程被移动到EntryList中。
(3)WaitSet:某个拥有ObjectMonitor的线程在调用Object.wait()方法之后将被阻塞,然后该线程将被放置在WaitSet链表中。
epub_38103745_43.jpg

3.3.2.1 Cxq

  • Cxq并不是一个真正的队列,只是一个虚拟队列,原因在于Cxq是由Node及其next指针逻辑构成的,并不存在一个队列的数据结构。每次新加入Node会在Cxq的队头进行,通过CAS改变第一个节点的指针为新增节点,同时设置新增节点的next指向后续节点;从Cxq取得元素时,会从队尾获取。显然,Cxq结构是一个无锁结构。
  • 因为只有Owner线程才能从队尾取元素,即线程出列操作无争用,当然也就避免了CAS的ABA问题。

3.3.2.2 EntryList

EntryList与Cxq在逻辑上都属于等待队列。Cxq会被线程并发访问,为了降低对Cxq队尾的争用,而建立EntryList。在Owner线程释放锁时,JVM会从Cxq中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为OnDeck Thread(Ready Thread)。EntryList中的线程作为候选竞争线程而存在。

3.3.2.3 OnDeck Thread与Owner Thread

JVM不直接把锁传递给Owner Thread,而是把锁竞争的权利交给OnDeck Thread,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大地提升系统的吞吐量,在JVM中,也把这种选择行为称为“竞争切换”。
OnDeck Thread获取到锁资源后会变为Owner Thread。无法获得锁的OnDeck Thread则会依然留在EntryList中,考虑到公平性,OnDeck Thread在EntryList中的位置不发生变化(依然在队头)。在OnDeck Thread成为Owner的过程中,还有一个不公平的事情,就是后来的新抢锁线程可能直接通过CAS自旋成为Owner而抢到锁。

3.3.2.4 WaitSet

如果Owner线程被Object.wait()方法阻塞,就转移到WaitSet队列中,直到某个时刻通过Object.notify()或者Object.notifyAll()唤醒,该线程就会重新进入EntryList中。

3.3.3 代码案例

package com.shu.BiasedLocking;

import com.shu.Lock.ObjectLock;
import org.openjdk.jol.vm.VM;

import java.util.concurrent.CountDownLatch;

/**
 * @description: 重量级锁
 * @author: shu
 * @createDate: 2022/11/16 14:08
 * @version: 1.0
 */
public class HeavyWeightLockingDemo {

    static final int MAX_TURN = 1000;

    public static void main(String[] args) throws InterruptedException {
        System.out.println("JVM信息:\n"+VM.current().details());
        //JVM延迟偏向锁
        Thread.sleep(5000);
        ObjectLock counter = new ObjectLock();
        System.out.println("抢占锁前, counter 的状态: ");
        counter.printObjectStruct();
        Thread.sleep(5000);
        CountDownLatch latch = new CountDownLatch(3);
        Runnable runnable = () ->
        {
            for (int i = 0; i < MAX_TURN; i++) {
                synchronized (counter) {
                    counter.increase();
                    if (i == 0) {
                        System.out.println("第一个线程占有锁, counter 的状态: ");
                        counter.printObjectStruct();
                    }
                }

            }
            //循环完毕
            latch.countDown();

            //线程虽然释放锁,但是一直存在
            for (int j = 0; ; j++) {
                //每一次循环等待1ms
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        new Thread(runnable).start();


        Thread.sleep(1000); //等待2s

        Runnable lightweightRunnable = () ->
        {
            for (int i = 0; i < MAX_TURN; i++) {
                synchronized (counter) {
                    counter.increase();
                    if (i == 0) {
                        System.out.println("占有锁, counter 的状态: ");
                        counter.printObjectStruct();
                    }
                    //每一次循环等待10ms
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            //循环完毕
            latch.countDown();
        };
        new Thread(lightweightRunnable, "抢锁线程1").start();
        Thread.sleep(100);  //等待2s
        new Thread(lightweightRunnable, "抢锁线程2").start();

        //等待加锁线程执行完成
        latch.await();
        Thread.sleep(2000);  //等待2s
        System.out.println("释放锁后, counter 的状态: ");
        counter.printObjectStruct();
    }
}

偏向锁状态
image.png
偏向锁状态
image.png
轻量级锁状态
image.png
重量级锁状态
image.png
无锁状态
image.png

3.4 总结

  • 线程抢锁时,JVM首先检测内置锁对象Mark Word中的biased_lock(偏向锁标识)是否设置成1,lock(锁标志位)是否为01,如果都满足,确认内置锁对象为可偏向状态。
  • 在内置锁对象确认为可偏向状态之后,JVM检查Mark Word中的线程ID是否为抢锁线程ID,如果是,就表示抢锁线程处于偏向锁状态,抢锁线程快速获得锁,开始执行临界区代码。
  • 如果Mark Word中的线程ID并未指向抢锁线程,就通过CAS操作竞争锁。如果竞争成功,就将Mark Word中的线程ID设置为抢锁线程,偏向标志位设置为1,锁标志位设置为01,然后执行临界区代码,此时内置锁对象处于偏向锁状态。
  • 如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁,进而升级为轻量级锁。
  • JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针,如果成功,抢锁线程就获得锁。如果替换失败,就表示其他线程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录指针,如果自旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
  • 如果JVM的CAS替换锁记录指针自旋失败,轻量级锁就膨胀为重量级锁。

epub_38103745_45.jpg
epub_38103745_46.jpg

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

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

相关文章

zookeeper学习笔记

zookeeper学习笔记1.Zookeeper概念2.Zookeeper命令操作2.1数据模型2.1.1数据结构2.1.2节点类型2.2服务端命令2.3客户端命令-基本命令2.4客户端命令-高级点命令3.Zookeeper JavaAPI操作3.1Cutor介绍3.2Cutor API常用操作-增删改查3.2.1建立连接3.2.2创建节点3.2.3删除节点3.2.4修…

js 回到顶部逻辑实现和elementUI源码解析

回到顶部 大家或多或少都会遇到“回到顶部”这样的需求&#xff0c;在此分享这个技术点以及可能遇到的问题。再分析element源码。 回到顶部代码实现 <!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><meta http-…

基于PHP+MySQL的个人博客系统毕设

随着时代和网络的发展,人们越来越希望通过多种模式来展示自己。于是个人博客就出现了,它可以更好的让人们来记录自己的工作和学习方式。博客不仅仅可以让自己抒发个人感情,还可以展示自己真实的生活,从而建立起一种友好的交友平台。 PHP个人博客系统毕设系统分为前台和后台两部…

Python每日一练 02

Python每日一练 02 文章目录Python每日一练 02一、对象二、对象属性三、赋值一、对象 Python中所有数字、序列、集合、映射、类、实例、异常、模块、类与类的实例、函数、方法、布尔值、空值等都被称为对象。 二、对象属性 每个对象都有3个基本属性&#xff1a; 类型(type)…

Flink-源算子Source(获取数据源)的使用

5.1 整体介绍 获取执行环境读取数据源定义基于数据的转换操作定义计算结果的输出位置触发程序执行 5.2 创建集成环境 5.2.1 获取执行环境 批处理getExecutionEnvironment 提交命令行设置 bin/flink run -Dexecution.runtime-modeBATCH ...代码 StreamExecutionEnvironme…

pip Command Not Found – Mac 和 Linux 错误被解决

使用Python时&#xff0c;可能需要安装和使用某些软件包。有一个命令可用于’pip‘ 使用pip&#xff0c;您可以安装、升级和卸载各种Python包。在本文中&#xff0c;您将学习如何使用它&#xff0c;以及如何处理pip错误。 如何使用 pip Pip是一个可以在Linux或Mac命令行上使用…

HTTP(http+抓包Fiddler+协议格式+请求+响应)

目录 &#x1f984;1. 了解HTTP &#x1f984;2. 抓包 &#x1f984;3. http协议格式 &#x1f432;3.1 完整的HTTP请求格式 &#x1f432;3.2 完整的HTTP响应的格式 HTTP请求 &#x1f984;4. 认识URL &#x1f984;5. http中的"方法" &#x1f432;5.1…

智能与工程学院2022级计算机朱元华

智能与工程学院 《高级语言程序设计》 小组学习任务书 第 1 次 专业年级&#xff1a; 2022级计算机 指导教师&#xff1a; 朱元华 2022-2023学年 第 1 学期 一、任务 XXX信息管理系统的需求分析和功能设计 二、分组形式 学生自由组合&#xff0c;5-8人为一组&#xff0c;根据…

Tuxera NTFS2023Mac读写ntfs磁盘工具

Tuxera Ntfs for mac2023是Mac中专用于读写外置存储的工具&#xff0c;具有强大的磁盘管理和修复功能&#xff0c;它在Mac上完全读写NTFS格式硬盘&#xff0c;快捷的访问、编辑、存储和传输文件。能够在 Mac 上读写 Windows NTFS 文件系统。Tuxera NTFS 实现在Mac OS X系统读写…

【Spring】——6、按照条件向Spring容器中注册bean

&#x1f4eb;作者简介&#xff1a;zhz小白 公众号&#xff1a;小白的Java进阶之路 专业技能&#xff1a; 1、Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理 2、熟悉Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理&#xff0c;具备⼀定的线…

静态时序分析简明教程(六)]时钟组与其他时钟特性

生成时钟的sdc约束方法一、写在前面1.1 快速导航链接二、时钟组2.1 引入时钟组2.2 set_clock_group2.2.1 -name2.2.2 -group clock_list2.2.3 -logically_exclusive|-physically_exclusive|-asynchronous2.2.4 -allow_path2.2.5 -comment三、其他时钟特性3.1 过渡时间3.2 偏移与…

【Linux】进程间通信——管道

目录 一、概念 二、管道函数 1.popen函数 2.pclose函数 3.文件函数 三、管道的操作 1.管道的分类 无名管道 有名管道 管道的特点 四、管道的实现 操作系统对进程之间相互保护 两个进程之间相互通信 前言&#xff1a; 进程间通信的方法/IPC机制都有哪些&#xff1a; …

求二进制中1的个数的三种方法

求二进制中的1的个数 文章目录第一种方法&#xff1a;模2除2第二种方法&#xff1a;利用操作符右移后与1第三种方法&#xff1a;该数与上比它小1的数&#xff08;最优的方法&#xff09;第一种方法&#xff1a;模2除2 首先明白如何得到一个数的十进制的每一位&#xff1f; 以1…

PHP代码审计入门-DVWA靶场CSRF篇

0x00 写在前面 从零学习php&#xff0c;最终目的实现代码审计入门&#xff0c;软件采用sublime text&#xff0c;环境使用phpstudy搭建&#xff0c;数据库是navicat&#xff0c;需要有基本的前端基础、简单的phpmysql后端基础、渗透知识和漏洞原理&#xff0c;文章跟随流沙前…

bizlog通用操作日志组件(使用篇)

引言 如上图所示&#xff0c;产品的新需求&#xff0c;需要将操作人在系统中具体编辑操作的变更内容记录下来。 按正常思路来说&#xff0c;无非就是将修改前后的对象字段逐个比较&#xff0c;再拼接为详细的操作描述记录到操作日志表中。如果是一个模块的需求&#xff0c;单独…

用HTML+CSS做一个学生抗疫感动专题网页设计作业网页

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

springboot如何修改thymeleaf中默认的页面路径及默认的后缀名呢?

转自: springboot如何修改thymeleaf中默认的页面路径及默认的后缀名呢? 下文讲述springboot修改thymefleaf修改页面默认路径及后缀名的方法分享&#xff0c;如下所示: 实现思路:只需在配置文件中修改相应的配置即可&#xff0c;如&#xff1a;application.yaml spring:thym…

MySQL单表查询操作详解,不做CRUD程序员

在我们对数据进行操作时&#xff0c;查询无疑是至关重要的&#xff0c;查询操作灵活多变&#xff0c;我们可以根据开发的需求&#xff0c;设计高效的查询操作&#xff0c;把数据库中存储的数据展示给用户。 文章目录前言1. 基础查询1.1 基础查询语法1.2 基础查询练习2. 条件查询…

大数据路线

一、概念部分 1.1 大数据、数仓、数据湖、中台的概念 区别数仓数据湖使用场景批处理&#xff0c;BI&#xff0c;数据可视化机器学习、预测分析、数据分析Schema写入型读取型数据源类型OLTP为主的结构化数据loT&#xff0c;日志&#xff0c;各个端等结构非结构均可性价比需要快…

牛客刷题总结——Python入门08:面向对象、正则表达式

&#x1f935;‍♂️ 个人主页: 北极的三哈 个人主页 &#x1f468;‍&#x1f4bb; 作者简介&#xff1a;Python领域优质创作者。 &#x1f4d2; 系列专栏&#xff1a;《牛客题库-Python篇》 &#x1f310;推荐《牛客网》——找工作神器|笔试题库|面试经验|实习经验内推&am…