【Java】多线程和高并发编程(三):锁(下)深入ReentrantReadWriteLock

文章目录

  • 4、深入ReentrantReadWriteLock
    • 4.1 为什么要出现读写锁
    • 4.2 读写锁的实现原理
    • 4.3 写锁分析
      • 4.3.1 写锁加锁流程概述
      • 4.3.2 写锁加锁源码分析
      • 4.3.3 写锁释放锁流程概述&释放锁源码
    • 4.4 读锁分析
      • 4.4.1 读锁加锁流程概述
        • 4.4.1.1 基础读锁流程
        • 4.4.1.2 读锁重入流程
        • 4.4.1.3 读锁加锁的后续逻辑fullTryAcquireShared
        • 4.4.1.4 读线程在AQS队列获取锁资源的后续操作
      • 4.4.2 读锁的释放锁流程

在这里插入图片描述
个人主页:道友老李
欢迎加入社区:道友老李的学习社区

4、深入ReentrantReadWriteLock

4.1 为什么要出现读写锁

synchronized和ReentrantLock都是互斥锁。

如果说有一个操作是读多写少的,还要保证线程安全的话。如果采用上述的两种互斥锁,效率方面很定是很低的。

在这种情况下,咱们就可以使用ReentrantReadWriteLock读写锁去实现。

读读之间是不互斥的,可以读和读操作并发执行。

但是如果涉及到了写操作,那么还得是互斥的操作。

static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();public static void main(String[] args) throws InterruptedException {new Thread(() -> {readLock.lock();try {System.out.println("子线程!");try {Thread.sleep(500000);} catch (InterruptedException e) {e.printStackTrace();}} finally {readLock.unlock();}}).start();Thread.sleep(1000);writeLock.lock();try {System.out.println("主线程!");} finally {writeLock.unlock();}
}

4.2 读写锁的实现原理

ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。

读锁操作:基于state的高16位进行操作。

写锁操作:基于state的低16为进行操作。

ReentrantReadWriteLock依然是可重入锁。

写锁重入:读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+1操作即可,只要确认持有锁资源的线程,是当前写锁线程即可。只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中写锁范围就变小了。

读锁重入:因为读锁是共享锁。读锁在获取锁资源操作时,是要对state的高16位进行 + 1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数。

写锁的饥饿问题:读锁是共享锁,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。

读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面需要写锁资源的线程,那么后续读线程是无法拿到锁资源的。持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源

4.3 写锁分析

4.3.1 写锁加锁流程概述

image.png

4.3.2 写锁加锁源码分析

写锁加锁流程

// 写锁加锁的入口
public void lock() {sync.acquire(1);
}// 阿巴阿巴!!
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}// 读写锁的写锁实现tryAcquire
protected final boolean tryAcquire(int acquires) {// 拿到当前线程Thread current = Thread.currentThread();// 拿到state的值int c = getState();// 得到state低16位的值int w = exclusiveCount(c);// 判断是否有线程持有着锁资源if (c != 0) {// 当前没有线程持有写锁,读写互斥,告辞。// 有线程持有写锁,持有写锁的线程不是当前线程,不是锁重入,告辞。if (w == 0 || current != getExclusiveOwnerThread())return false;// 当前线程持有写锁。 锁重入。if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 没有超过锁重入的次数,正常 + 1setState(c + acquires);return true;}// 尝试获取锁资源if (writerShouldBlock() ||// CAS拿锁!compareAndSetState(c, c + acquires))return false;// 拿锁成功,设置占有互斥锁的线程setExclusiveOwnerThread(current);// 返回truereturn true;
}// ================================================================
// 这个方法是将state的低16位的值拿到
int w = exclusiveCount(c);
state & ((1 << 16) - 1)
00000000 00000000 00000000 00000001    ==   1
00000000 00000001 00000000 00000000    ==   1 << 16
00000000 00000000 11111111 11111111    ==   (1 << 16) - 1
&运算,一个为0,必然为0,都为1,才为1
// ================================================================
// writerShouldBlock方法查看公平锁和非公平锁的效果
// 非公平锁直接返回false执行CAS尝试获取锁资源
// 公平锁需要查看是否有排队的,如果有排队的,我是否是head的next

4.3.3 写锁释放锁流程概述&释放锁源码

释放的流程和ReentrantLock一致,只是在判断释放是否干净时,判断低16位的值

// 写锁释放锁的tryRelease方法
protected final boolean tryRelease(int releases) {// 判断当前持有写锁的线程是否是当前线程if (!isHeldExclusively())throw new IllegalMonitorStateException();// 获取state - 1int nextc = getState() - releases;// 判断低16位结果是否为0,如果为0,free设置为trueboolean free = exclusiveCount(nextc) == 0;if (free)// 将持有锁的线程设置为nullsetExclusiveOwnerThread(null);// 设置给statesetState(nextc);// 释放干净,返回true。  写锁有冲入,这里需要返回false,不去释放排队的Nodereturn free;
}

4.4 读锁分析

4.4.1 读锁加锁流程概述

  1. 分析读锁加速的基本流程
  2. 分析读锁的可重入锁实现以及优化
  3. 解决ThreadLocal内存泄漏问题
  4. 读锁获取锁自后,如果唤醒AQS中排队的读线程
4.4.1.1 基础读锁流程

image.png

针对上述简单逻辑的源码分析

// 读锁加锁的方法入口
public final void acquireShared(int arg) {// 竞争锁资源滴干活if (tryAcquireShared(arg) < 0)// 没拿到锁资源,去排队doAcquireShared(arg);
}// 读锁竞争锁资源的操作
protected final int tryAcquireShared(int unused) {// 拿到当前线程Thread current = Thread.currentThread();// 拿到stateint c = getState();// 拿到state的低16位,判断 != 0,有写锁占用着锁资源// 并且,当前占用锁资源的线程不是当前线程if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)// 写锁被其他线程占用,无法获取读锁,直接返回 -1,去排队return -1;// 没有线程持有写锁、当前线程持有写锁// 获取读锁的信息,state的高16位。int r = sharedCount(c);// 公平锁:就查看队列是由有排队的,有排队的,直接告辞,进不去if,后面也不用判断(没人排队继续走)// 非公平锁:没有排队的,直接抢。 有排队的,但是读锁其实不需要排队,如果出现这个情况,大部分是写锁资源刚刚释放,// 后续Node还没有来记得拿到读锁资源,当前竞争的读线程,可以直接获取if (!readerShouldBlock() &&// 判断持有读锁的临界值是否达到r < MAX_COUNT &&// CAS修改state,对高16位进行 + 1compareAndSetState(c, c + SHARED_UNIT)) {// 省略部分代码!!!!return 1;}return fullTryAcquireShared(current);
}
// 非公平锁的判断
final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null &&    // head为null,可以直接抢占锁资源(s = h.next)  != null &&    // head的next为null,可以直接抢占锁资源!s.isShared()         &&    // 如果排在head后面的Node,是共享锁,可以直接抢占锁资源。s.thread != null;           // 后面排队的thread为null,可以直接抢占锁资源
}
4.4.1.2 读锁重入流程

=============重入操作

前面阐述过,读锁为了记录锁重入的次数,需要让每个读线程用ThreadLocal存储重入次数

ReentrantReadWriteLock对读锁重入做了一些优化操作

============记录重入次数的核心

ReentrantReadWriteLock在内部对ThreadLocal做了封装,基于HoldCount的对象存储重入次数,在内部有个count属性记录,而且每个线程都是自己的ThreadLocalHoldCounter,所以可以直接对内部的count进行++操作。

=============第一个获取读锁资源的重入次数记录方式

第一个拿到读锁资源的线程,不需要通过ThreadLocal存储,内部提供了两个属性来记录第一个拿到读锁资源线程的信息

内部提供了firstReader记录第一个拿到读锁资源的线程,firstReaderHoldCount记录firstReader的锁重入次数

==============最后一个获取读锁资源的重入次数记录方式

最后一个拿到读锁资源的线程,也会缓存他的重入次数,这样++起来更方便

基于cachedHoldCounter缓存最后一个拿到锁资源现成的重入次数

==============最后一个获取读锁资源的重入次数记录方式

重入次数的流程执行方式:

1、判断当前线程是否是第一个拿到读锁资源的:如果是,直接将firstReader以及firstReaderHoldCount设置为当前线程的信息

2、判断当前线程是否是firstReader:如果是,直接对firstReaderHoldCount++即可。

3、跟firstReader没关系了,先获取cachedHoldCounter,判断是否是当前线程。

3.1、如果不是,获取当前线程的重入次数,将cachedHoldCounter设置为当前线程。

3.2、如果是,判断当前重入次数是否为0,重新设置当前线程的锁从入信息到readHolds(ThreadLocal)中,算是初始化操作,重入次数是0

3.3、前面两者最后都做count++

上述逻辑源码分析

protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// ===============================================================// 判断r == 0,当前是第一个拿到读锁资源的线程if (r == 0) {// 将firstReader设置为当前线程firstReader = current;// 将count设置为1firstReaderHoldCount = 1;} // 判断当前线程是否是第一个获取读锁资源的线程else if (firstReader == current) {// 直接++。firstReaderHoldCount++;} // 到这,就说明不是第一个获取读锁资源的线程else {// 那获取最后一个拿到读锁资源的线程HoldCounter rh = cachedHoldCounter;// 判断当前线程是否是最后一个拿到读锁资源的线程if (rh == null || rh.tid != getThreadId(current))// 如果不是,设置当前线程为cachedHoldCountercachedHoldCounter = rh = readHolds.get();// 当前线程是之前的cacheHoldCounterelse if (rh.count == 0)// 将当前的重入信息设置到ThreadLocal中readHolds.set(rh);// 重入的++rh.count++;}// ===============================================================return 1;}return fullTryAcquireShared(current);
}
4.4.1.3 读锁加锁的后续逻辑fullTryAcquireShared
// tryAcquireShard方法中,如果没有拿到锁资源,走这个方法,尝试再次获取,逻辑跟上面基本一致。
final int fullTryAcquireShared(Thread current) {// 声明当前线程的锁重入次数HoldCounter rh = null;// 死循环for (;;) {// 再次拿到stateint c = getState();// 当前如果有写锁在占用锁资源,并且不是当前线程,返回-1,走排队策略if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;} // 查看当前是否可以尝试竞争锁资源(公平锁和非公平锁的逻辑)else if (readerShouldBlock()) {// 无论公平还是非公平,只要进来,就代表要放到AQS队列中了,先做一波准备// 在处理ThreadLocal的内存泄漏问题if (firstReader == current) {// 如果当前当前线程是之前的firstReader,什么都不用做} else {// 第一次进来是null。if (rh == null) {// 拿到最后一个获取读锁的线程rh = cachedHoldCounter;// 当前线程并不是cachedHoldCounter,没到拿到if (rh == null || rh.tid != getThreadId(current)) {// 从自己的ThreadLocal中拿到重入计数器rh = readHolds.get();// 如果计数器为0,说明之前没拿到过读锁资源if (rh.count == 0)// remove,避免内存泄漏readHolds.remove();}}// 前面处理完之后,直接返回-1if (rh.count == 0)return -1;}}// 判断重入次数,是否超出阈值if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");// CAS尝试获取锁资源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;}}
}
4.4.1.4 读线程在AQS队列获取锁资源的后续操作

1、正常如果都是读线程来获取读锁资源,不需要使用到AQS队列的,直接CAS操作即可

2、如果写线程持有着写锁,这是读线程就需要进入到AQS队列排队,可能会有多个读线程在AQS中。

当写锁释放资源后,会唤醒head后面的读线程,当head后面的读线程拿到锁资源后,还需要查看next节点是否也是读线程在阻塞,如果是,直接唤醒

源码分析

// 读锁需要排队的操作
private void doAcquireShared(int arg) {// 声明Node,类型是共享锁,并且扔到AQS中排队final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {// 拿到上一个节点final Node p = node.predecessor();// 如果prev节点是head,直接可以执行tryAcquireSharedif (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// 拿到读锁资源后,需要做的后续处理setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}// 找到prev有效节点,将状态设置为-1,挂起当前线程if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}private void setHeadAndPropagate(Node node, int propagate) {// 拿到head节点Node h = head; // 将当前节点设置为head节点setHead(node);// 第一个判断更多的是在信号量有处理JDK1.5 BUG的操作。if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {// 拿到当前Node的next节点Node s = node.next;// 如果next节点是共享锁,直接唤醒next节点if (s == null || s.isShared())doReleaseShared();}
}

4.4.2 读锁的释放锁流程

1、处理重入以及state的值

2、唤醒后续排队的Node

源码分析

// 读锁释放锁流程
public final boolean releaseShared(int arg) {// tryReleaseShared:处理state的值,以及可重入的内容if (tryReleaseShared(arg)) {// AQS队列的事!doReleaseShared();return true;}return false;
}// 1、 处理重入问题  2、 处理state
protected final boolean tryReleaseShared(int unused) {// 拿到当前线程Thread current = Thread.currentThread();// 如果是firstReader,直接干活,不需要ThreadLocalif (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} // 不是firstReader,从cachedHoldCounter以及ThreadLocal处理else {// 如果是cachedHoldCounter,正常--HoldCounter rh = cachedHoldCounter;// 如果不是cachedHoldCounter,从自己的ThreadLocal中拿if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;// 如果为1或者更小,当前线程就释放干净了,直接remove,避免value内存泄漏if (count <= 1) {readHolds.remove();// 如果已经是0,没必要再unlock,扔个异常if (count <= 0)throw unmatchedUnlockException();}// -- 走你。--rh.count;}for (;;) {// 拿到state,高16位,-1,成功后,返回state是否为0int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}
}// 唤醒AQS中排队的线程
private void doReleaseShared() {// 死循环for (;;) {// 拿到头Node h = head;// 说明有排队的if (h != null && h != tail) {// 拿到head的状态int ws = h.waitStatus;// 判断是否为 -1 if (ws == Node.SIGNAL) {// 到这,说明后面有挂起的线程,先基于CAS将head的状态从-1,改为0if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;   // 唤醒后续节点unparkSuccessor(h);}// 这里不是给读写锁准备的,在信号量里说。。。else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;}// 这里是出口if (h == head)   break;}
}

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

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

相关文章

【R语言】相关系数

一、cor()函数 cor()函数是R语言中用于计算相关系数的函数&#xff0c;相关系数用于衡量两个变量之间的线性关系强度和方向。 常见的相关系数有皮尔逊相关系数&#xff08;Pearson correlation coefficient&#xff09;、斯皮尔曼秩相关系数&#xff08;Spearmans rank corre…

Web - CSS3过渡与动画

过渡 基本使用 transition过渡属性是css3浓墨重彩的特性&#xff0c;过渡可以为一个元素在不同样式之间变化自动添加补间动画。 过渡从kIE10开始兼容&#xff0c;移动端兼容良好&#xff0c;网页上的动画特效基本都是由JavaScript定时器实现的&#xff0c;现在逐步改为css3过…

Unity 高度可扩展的技能与多 Buff 框架详解

一、框架设计 1.1 核心思想 组件化设计: 将技能和 Buff 抽象为可复用的组件&#xff0c;通过组合不同的组件实现复杂的效果。 数据驱动: 使用 ScriptableObject 或 JSON 等数据格式定义技能和 Buff 的属性&#xff0c;方便配置和修改。 事件驱动: 利用 Unity 的事件系统或自…

编译和链接【一】

文章目录 编译和链接【一】从翻译单元到二进制文件 编译和链接【一】 在我大一的时候&#xff0c; 我使用VC6.0对C语言程序进行编译链接和运行 &#xff0c; 然后我接触了VS&#xff0c; VS code等众多IDE&#xff0c; 这些IDE界面友好&#xff0c; 使用方便&#xff0c; 例如…

图像锐化(QT)

如果不使用OpenCV&#xff0c;我们可以直接使用Qt的QImage类对图像进行像素级操作来实现锐化。锐化算法的核心是通过卷积核&#xff08;如拉普拉斯核&#xff09;对图像进行处理&#xff0c;增强图像的边缘和细节。 以下是一个完整的Qt应用程序示例&#xff0c;展示如何使用Qt…

迅雷下载的原理和使用协议的分析

迅雷作为一款广泛使用的下载工具&#xff0c;其核心原理是通过整合多种下载协议和资源分发技术来提升下载速度。以下是对其原理及协议的详细分析&#xff1a; 一、迅雷下载的核心原理 多协议混合下载&#xff08;P2SP&#xff09; P2SP&#xff08;Peer-to-Server-Peer&#xf…

【动手学运动规划】5.4 二次规划问题:QP优化

站在天堂看地狱&#xff0c;人生就像情景剧&#xff1b;站在地狱看天堂&#xff0c;为谁辛苦为谁忙。 —武林外传 白展堂 &#x1f3f0;代码及环境配置&#xff1a;请参考 环境配置和代码运行! 在运动规划算法中, QP优化是非常常见的优化问题形式, 本节我们将进行介绍. 5.4.1…

Linux: ASoC 声卡硬件参数的设置过程简析

文章目录 1. 前言2. ASoC 声卡设备硬件参数2.1 将 DAI、Machine 平台的硬件参数添加到声卡2.2 打开 PCM 流时将声卡硬件参数配置到 PCM 流2.3 应用程序对 PCM 流参数进行修改调整 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&am…

ansible使用学习

一、查询手册 1、官网 ansible官网地址&#xff1a;https://docs.ansible.com 模块查看路径&#xff1a;https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html#plugins-in-ansible-builtin 2、命令 ansible-doc -s command二、相关脚本 1、服务…

jmap使用

常用命令 jmap -heap PID jmap -histo PID | head -20 jmap -dump:formatb,fileheap_dump.hprof PID jmap 是 Java 开发工具包&#xff08;JDK&#xff09;提供的一个命令行工具&#xff0c;用于生成 Java 进程的内存映射信息。它可以帮助开发者分析 Java 堆内存的使用情况…

RabbitMQ 如何设置限流?

RabbitMQ 的限流&#xff08;流量控制&#xff09;主要依赖于 QoS&#xff08;Quality of Service&#xff09; 机制&#xff0c;即 prefetch count 参数。这个参数控制每个消费者一次最多能获取多少条未确认的消息&#xff0c;从而避免某个消费者被大量消息压垮。 1. RabbitMQ…

第四十八章:黄山之行:与小一的奇妙冒险

自从小泽泽满月酒过后&#xff0c;小冷一家的生活又恢复了往日的温馨与忙碌。小泽泽在家人的悉心照料下茁壮成长&#xff0c;而小冷和小颖也在工作与家庭之间努力平衡着。2024 年 11 月&#xff0c;秋意正浓&#xff0c;山林间五彩斑斓&#xff0c;空气中弥漫着清爽的气息。小冷…

基于 SpringBoot 和 Vue 的智能腰带健康监测数据可视化平台开发(文末联系,整套资料提供)

基于 SpringBoot 和 Vue 的智能腰带健康监测数据可视化平台开发 一、系统介绍 随着人们生活水平的提高和健康意识的增强&#xff0c;智能健康监测设备越来越受到关注。智能腰带作为一种新型的健康监测设备&#xff0c;能够实时采集用户的腰部健康数据&#xff0c;如姿势、运动…

2025.2.8 寒假综合训练赛2题解

A. 博弈 Link&#xff1a;P1290 欧几里德的游戏 博弈类的题目&#xff0c;首先考虑找找有什么性质&#xff0c;从而找到“必胜态”和“必败态”。 其中&#xff0c;面对“必胜态”不一定取胜&#xff08;看个人操作的好坏&#xff09;&#xff0c;但面对“必败态”一定输&am…

docker离线安装及部署各类中间件(x86系统架构)

前言&#xff1a;此文主要针对需要在x86内网服务器搭建系统的情况 一、docker离线安装 1、下载docker镜像 https://download.docker.com/linux/static/stable/x86_64/ 版本&#xff1a;docker-23.0.6.tgz 2、将docker-23.0.6.tgz 文件上传到服务器上面&#xff0c;这里放在…

Spring Boot 中的日志配置

文章目录 Spring Boot 中日志配置的源码分析1. Spring Boot 日志框架的选择与自动配置2. 日志自动配置与默认行为3. 日志系统的核心组件&#xff1a;Logger 和 LoggerFactory4. 日志配置文件的解析配置日志级别配置日志输出格式和目标 5. 日志级别的控制自定义日志级别 6. 自定…

从零到一:我的元宵灯谜小程序诞生记

缘起&#xff1a;一碗汤圆引发的灵感 去年元宵节&#xff0c;我正捧着热腾腾的汤圆刷朋友圈&#xff0c;满屏都是"转发锦鲤求灯谜答案"的动态。看着大家对着手机手忙脚乱地切换浏览器查答案&#xff0c;我突然拍案而起&#xff1a;为什么不做一个能即时猜灯谜的微信…

CSS3+动画

浏览器内核以及其前缀 css标准中各个属性都要经历从草案到推荐的过程&#xff0c;css3中的属性进展都不一样&#xff0c;浏览器厂商在标准尚未明确的情况下提前支持会有风险&#xff0c;浏览器厂商对新属性的支持情况也不同&#xff0c;所有会加厂商前缀加以区分。如果某个属性…

2025.2.8——二、Confusion1 SSTI模板注入|Jinja2模板

题目来源&#xff1a;攻防世界 Confusion1 目录 一、打开靶机&#xff0c;整理信息 二、解题思路 step 1&#xff1a;查看网页源码信息 step 2&#xff1a;模板注入 step 3&#xff1a;构造payload&#xff0c;验证漏洞 step 4&#xff1a;已确认为SSTI漏洞中的Jinjia2…

c++初始

目录 一数据类型 1. 2.sizeof 3.布尔 4.字符串类型 二.数据输入与输出 1.输出 2.输入 三.运算 1.加减乘除取模&#xff0c;&#xff0c;--都一样 2.逻辑非与或&#xff0c;与C语言一样 3.比较运算符&#xff0c;与C语言一样 4.三目运算符&#xff08;与C语言一样&a…