《深入理解JAVA虚拟机(第2版)》- 第13章 - 学习笔记【终章】

news2024/11/15 15:44:30

第13章 线程安全与锁优化

13.1 概述

  • 面向过程的编程思想

    将数据和过程独立分开,数据是问题空间中的客体,程序代码是用来处理数据的,这种站在计算机角度来抽象和解决问题的思维方式,称为面向对象的编程思想。

  • 面向对象的编程思想

    将数据和行为看作是对象的一部分,这种站在现实世界的角度去抽象和解决问题的思维方式,称为面向对象的编程思想。

13.2 线程安全

首先我们先来看下在《Java Concurrency In Practice》中,作者Brian Goetz是如何来定义线程安全的:

当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都能获得正确的结果,那这个对象是线程安全的。

通过以上的定义从中梳理出线程安全的代码所具备的一个必须特征是:代码本身封装了所有必要的正确性保障手段(例如:互斥同步),调用者无需考虑多线程的问题,更无须自己采取任何措施来保证多线程的正确调用

13.2.1 Java语言中的线程安全

了解了什么是现成安全之后,让我们来基于Java语言说下,线程安全是如何实现的?哪些操作是线程安全的。

按着线程安全的“安全程度”由强至弱来排序,Java语言中操作共享的数据可以分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变

    • 不可变的对象一定是线程安全的。

    • 如果共享数据是基本数据类型,那么它被final关键字修饰的话就可以保证它是不可变的。

    • 如果共享数据是对象,那就需要保证对象的行为不会对它自身的状态产生影响。拿java.lang.String为例,它是一个典型的不可变对象,我们调用它的substring()、replace()和contract()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。

  2. 绝对线程安全

    绝对线程安全要完全满足Brian Goetz给出的对线程安全的定义,即无论任何运行时环境,调用者都不需要进行额外的同步措施

    Java API中标注自己为线程安全的类,实际上都不是绝对线程安全的,我们拿java.lang.Vector为例,众所周知它的add()、size()、remove()和get()方法都是被synchronized修饰的,即使这样也不能保证任何时候调用它的都不需要同步手段了。以下面的代码段为例:

    private static Vector<Integer> vector = new Vector<Integer>();
    
    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 10; i++) {
                  vector.add(i);
            }
    
    ​​        Thread removeThread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (int i = 0; i < vector.size(); i++) {
                            vector.remove(i);
                      }
                  }
            });
    
    ​​        Thread printThread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (int i = 0; i < vector.size(); i++) {
                            System.out.println((vector.get(i)));
                      }
                  }
            });
    
    ​​        removeThread.start();
            printThread.start()
    
    ​​        //不要同时产生过多的线程,否则会导致操作系统假死
            while (Thread.activeCount() > 20);
        }
    }​​
    

    运行结果可能如下:

    Exception in thread "Thread-59775" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 18
    	at java.base/java.util.Vector.get(Vector.java:750)
    	at com.datapix.dao.platform.mapper.VectorTest$2.run(VectorTest.java:32)
    	at java.base/java.lang.Thread.run(Thread.java:842)​​
    

    通过运行结果,我们可以看到,如果不在调用端加上额外的同步措施,这段代码仍然是不安全的。想象下在removeThread线程中删除一条元素,导致序号i不在可用的话,printThread线程通过序号i再去访问就会抛出ArrayIndexOutOfBoundsException异常。

    要想保证代码段正确执行下去,我们需要在调用端进行一些同步处理,如下: 在这里插入图片描述

  3. 相对线程安全

    • 我们通常所讲的线程安全说的就是相对线程安全。

    • 相对线程安全需要保证对象的单独操作是现成安全的,我们在调用的时候不需要进行额外的保障措施。

      而对于一些特定的顺序的连续性调用,就需要调用端采用一些额外的同步手段来保证调用的正确性,例如:上边绝对线程安全中提到的那个代码段。

  4. 线程兼容

    • 我们通常说的线程不安全指的就是这类情况。
    • 线程兼容是指对象本身是现成不安全的,但是可以通过在调用端采取一些同步手段来保证对象在并发的环境下可以安全的使用。
    • 在Java中的ArrayList、HashMap都属于线程兼容。
  5. 线程对立

    线程对立是指无论调用端是否采用额外的同步措施,都无法在多线程的环境中并发使用的代码。

13.2 线程安全的实现方法

13.2.1 同步

同步是指多个线程并发访问共享数据的时候,保证共享数据在同一时刻只被一个线程使用。

13.2.2 互斥同步
  1. 互斥作为实现同步的一个手段。

  2. 互斥同步又称为“阻塞同步”,使用的是悲观并发策略。

  3. 互斥的实现方式分为:临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)。

  4. Java中互斥同步手段有:关键字synchronized和J.U.C(java.util.concurrent)的重入锁(ReentrantLock)。

    • 关键字synchronized

      synchronized编译后,会在同步块的前后增加monitorenter和monitorexit两个字节码指令,这两个字节码需要reference类型的参数作为锁定和解锁的对象。

      monitorenter和monitorexit字节码指令执行过程描述如下:

      当执行monitorenter指令的时候,当前线程首先会去尝试获取对象锁。如果对象没有被锁定,或者当前线程已持有该对象锁,则锁的计数器+1。相应的,当执行monitorexit指令的时候,锁的计数器-1,直到计数器为0,锁被释放。如果对象锁获取失败,则当前线程会阻塞等待,直到其他线程将锁释放掉为止。

      synchronized同步块对于同一条线程来说是可重入的,不会出现自己把自己锁死的情况出现。

      通过下面的代码段进一步说明不会出现自己把自己锁死的情况:

       public class ReentrantTest {     
           public synchronized void reentrant() { // 标记①
               synchronized(this) { // 标记②
                   // do something ....
               }
           }     
       } 
      

      让我们来试想下reentrant()方法在一个线程中(假设这个线程的名字是【线程A】)的调用:【线程A】调用reentrant()方法(即「标记①」)的时候,【线程A】获取了this对象锁(即对象锁中会标记已被【线程A】占用),在执行到「标记②」的时候,需要再次获取this对象锁,但是由于此时this对象锁已被【线程A】所占用(且「标记②」又是【线程A】中的一个步骤)。

      这里岂不是出现了自己把自己锁死了的情况了嘛?!

      但是基于synchronized是可重入的特性,即获取锁的线程(即占用锁的线程)与此时正要获取锁的线程是同一个,那么就不需要阻塞等待了。事实上在获取this对象锁的线程与「标记②」所在的线程也是同一个线程(都是【线程A】),所以这里也就不需要阻塞等待,也就不会出现死锁了!

    • J.U.C(java.util.concurrent)的重入锁(ReentrantLock)

      在基本语法上,ReentrantLock与synchronized很相似,它俩都是线程可重入的。只是在代码写法上有所不同,ReentrantLock需要显性的编写lock()和unlock()方法,而synchronized则不需要

      相比synchronized,ReentrantLock还提供了一些高级功能,主要有:等待可中断、可实现公平锁(ReentrantLock默认是非公平锁)、锁可以绑定多个条件

13.2.3 非阻塞同步
  1. 非租塞同步是使用了乐观并发策略

  2. 简单说来,非租塞同步就是先进行操作,如果操作期间没有其他线程使用共享数据,则操作成功;如果操作期间有其他线程也使用了共享数据,出现了共享数据争用的情况,那就需要采取其他补救措施了(例如:重试直到成功为止),通常这个过程不需要将线程挂起。

  3. 比较并交换(Compare-and-Swap,下文称CAS),CAS指令有三个操作数:变量的内存地址、旧值和新值。当CAS指令执行时,比较旧值与变量的内存地址中的值,如果相同,则将变量的内存地址的值更新为新值。如果不相同就不更新。无论更新与否,都将返回旧值。

    CAS指令虽然有两个动作(比较和更新),但这个指令是一个原子操作(靠硬件来实现的)

    CAS语义上并不完美,存在一个逻辑漏洞(即“ABA”问题):在CAS进行更新前的比较操作时,我们发现此时变量的内存地址上的值与旧值相同,就此我们能断定变量的内存地址上的值没有变动过吗?如果这期间有其他线程将变量的内存地址上的值先变为C,又变回了A。CAS是无法感知这个变化过程的,它会认为变量的内存地址上的值没有发生过变化。

    针对这个逻辑漏洞(即“ABA”问题),我们该如何解决呢?下面介绍三个方法:

    • 使用J.U.C提供的原子引用类(AtomicStampedReference),它通过变量值的版本来确保CAS的正确性。
    • 直接采用互斥同步
    • 直接无视(根据具体的情况),大部分情况ABA问题不会影响程序并发的正确性。
13.2.4 无同步方案
  1. 要保证线程安全,并不一定要同步,两者并无因果关系。

  2. 同步只是为了确保共享数据在被争用时的正确性

  3. 如果一段代码根本不涉及共享数据,也就无需靠同步措施来保证正确性,这样的代码天生就是线程安全的。下面简单介绍下两种天生线程安全的代码:

    • 可重入代码(Reentrant Code)

      这种代码又叫做纯代码(Pure Code),可以在代码执行的任何时候中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。

    • 线程本地存储(Thread Local Storage)

      如果一段代码中的数据必须与其他代码共享,那就要看看使用共享数据的代码是否能在一条线程中执行,如果可以,就无需考虑同步问题了。

      举个下边的例子再来对照着去理解下,如下图: 在这里插入图片描述
      从图中我们看到,【代码段1】中的「操作人」是【代码段2】中也要用到的,所以「操作人」是两段代码(代码段1和代码段2)的共享数据,【代码段1】和【代码段2】又是在一条线程当中执行,所以共享数据(即「操作人」)无需同步。

13.3 锁优化

锁优化技术的目的是为了在线程之间更高效的共享数据,以及解决冲突问题,从而提高程序的执行效率。

13.3.1 自旋锁与自适应自旋
  1. 自旋锁

    通过上文【13.2.2】中关于互斥同步的介绍,我们可以知道互斥同步最大的性能消耗在与对阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态去完成,这样会给系统的并发性能带来很大的压力。实际上,通常共享数据的锁定状态只会持续很短的时间,为了避免这段时间的开销而引进挂起和恢复的开销,是有些得不偿失的。

    为了解决上诉的问题,自旋锁这项技术就出现了。它使后面(时间概念上)请求锁的线程忙循环(即自旋)一段时间(也就是等待一段时间),而不是直接放弃处理器的执行时间去挂起,看看持有锁的线程能否很快的释放锁。

    试想一下,如果持有锁的线程一直不释放锁,那另外一个线程就要一直忙循环(即自旋)下去,这样该线程也将一直占用处理器的执行时间,被占用的处理器也就无法去处理其他线程了,这样肯定是不适合的。所以对于自旋的时间是要有限制的,如果超过了这个时间限制(实际上是自旋的次数)还没有获得锁,那就还是采用传统的方式将线程挂起。

    默认情况下自旋次数为10,我们可以通过参数-XX:PreBlockSpin来修改自旋次数。

  2. 自适应的自旋锁

    在JDK1.6中还引入了自适应的自旋锁。

    所谓自适应就是自旋时间不再固定,自旋时间是通过上一次获取该锁的自旋时间以及该锁拥有者的状态来决定

    我们假设【线程A】想去获取一个锁对象,该锁刚刚被【线程B】通过自旋等待获得,且【线程B】正在执行中,那么虚拟机就会认为【线程A】通过自旋等待也可以成功获得该锁,进而虚拟机会允许【线程A】的自旋等待时间更长,比如100次循环。

    我们再假设另外一个场景:某个锁,通过自旋很少成功获得,结果将如何呢?答案是:再之后获取锁的时候会直接省去自旋的过程,避免造成处理器资源的浪费。

13.3.2 锁消除

在之前的《深入理解JAVA虚拟机(第2版)》- 第11章 - 学习笔记》笔记中在总结基于逃逸分析结果会有哪些优化的时候,提到的同步消除就是锁消除

锁消除是指虚拟机的即时编译器在运行时,对那些代码上要求同步,但被检测到不存在共享数据竞争的锁进行消除。

13.3.3 锁粗化

虚拟机如果发现一串操作都是对同一个对象进行加锁,会将加锁同步的范围到扩展(粗化)至整个操作序列的外部,这样加一次锁就可以了

以下边的代码段为例:

​​public String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1); // 标注①
    sb.append(s2); // 标注②
    sb.append(s3); // 标注③
    return sb.toString();
}​​

StringBuffer的append()方法是被关键字synchronized修饰的。代码段中的标注①、标注②、标注③,这一串操作都是第对象sb加锁,所以最终会将加锁同步的范围扩展为sb.append(s1)操作之前直到sb.append(s3)操作之后。

13.3.4 轻量级锁
  1. JDK1.6中引入的一项锁优化。

  2. 轻量级锁不是为了替代重量级锁而出现的,它是为了在不存在多线程竞争的情况下,减少重量级锁使用系统互斥量所带来的性能消耗。

  3. 谈轻量级锁和后边的偏向锁的时候,我们是绕不开实现它们的关键——对象头(Object Head)。HotSpot虚拟机的对象头(Object Head)有两部分组成:

    • 存储对象自身的运行时数据,例如:对象的HashCode、GC分代年龄,这部分数据官方称为Mark Word,它的长度在32位和64位虚拟机中分别为32bit和64bit。
    • 存储指向方法区的对象的类型数据指针。
    • 如果对象是数组的话,还需要有额外的部分用来存储数组的长度。
  4. 考虑虚拟机到空间效率问题,Mark Word被设计成一个非固定的数据结构,即根据对象当前所处的不同状态,存储的数据也不同,如下:

    状态锁标志位存储的数据
    未锁定01对象HashCode、GC分代年龄
    轻量级锁定00指向锁记录(Lock Record)的指针
    重量级锁定(膨胀)10指向重量级锁(互斥量)的指针
    GC标记11空(不需要记录信息)
    可偏向01偏向线程ID、偏向时间戳、GC分代年龄

    让我们来看下一个处于未锁定状态的对象在32位虚拟机中的它的Mark Word内存布局是什么样的,如下:
    在这里插入图片描述

  5. 轻量级锁的加锁过程,如下图: 在这里插入图片描述
    我们再具体的看下CAS操作前后Lock Record和对象的状态变化,如下图: 在这里插入图片描述

  6. 轻量级锁的解锁过程(解锁过程和加锁过程一样都是通过CAS操作来进行的):如果对象的Mark Word仍然指向Lock Record,那就使用CAS操作将Lock Record中的Displace Mark Word替换掉此时对象的Mark Word。如果替换成功,则整个同步过程完成。如果失败,则说明有其他线程尝试获取锁,那么后续就要在释放锁的同时唤醒被挂起的线程。

13.3.5 偏向锁
  1. JDK1.6引入的一项锁优化机制。

  2. 偏向锁会偏向于获取该锁的第一个线程,如果后续没有其他线程来获取该锁,则持有偏向锁的线程将永远不需要再进行同步。

  3. 偏向锁的原理是:

    假设当前虚拟机启用了偏向锁(启用参数-XX:+UseBiasedLocking,这是JDK 1.6的默认值)。

    当对象第一次被线程获取的时候,将对象头中的锁标志位设置为“01”,即偏向模式。同时将获取该对象锁的线程ID通过CAS操作记录在对象的Mark Word中。如果CAS操作成功,则持有该锁的线程以后每次进入该锁相关的同步块时,虚拟机都不会进行任何同步措施(例如:Locking、Unlocking以及Mark Word的Update操作)。

    当有另外的线程获取该锁时,则偏向模式结束。根据锁对象当前是否处于被锁定状态,撤销偏向锁后恢复到未锁定状态或轻量级锁的状态。

13.3.6 锁升级

对于一个重量级锁,通过锁优化我们了解到,它其实是一个升级的过程:偏向锁 -> 轻量级锁 -> 重量级锁,而不是不管什么情况都直接采用重量级锁(互斥同步)。

锁的升级过程:对象锁只有一个线程持有的时候,这个锁是偏向锁。当有两个以上的线程交替持有该锁的时候,此时锁是轻量级锁。当发生两个以上的线程同时要持有锁的时候(即并发获取锁),此时才会升级为重量级锁。

上一篇:《深入理解JAVA虚拟机(第2版)》- 第12章 - 学习笔记
下一篇:无(本篇为最终章)

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

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

相关文章

一劳永逸:用脚本实现夸克网盘内容自动更新

系统环境&#xff1a;debian/ubuntu 、 安装了python3 原作者项目&#xff1a;https://github.com/Cp0204/quark-auto-save 感谢 缘起 我喜欢看电影追剧&#xff0c;会经常转存一些资源到夸克网盘&#xff0c;电影还好&#xff0c;如果是电视剧&#xff0c;麻烦就来了。 对于一…

Kettle的安装及简单使用

Kettle的安装及简单使用一、kettle概述二、kettle安装部署和使用Windows下安装案例1&#xff1a;MySQL to MySQL案例2&#xff1a;使用作业执行上述转换&#xff0c;并且额外在表stu2中添加一条数据案例3&#xff1a;将hive表的数据输出到hdfs案例4&#xff1a;读取hdfs文件并将…

Jboss常⻅中间件漏洞

一.CVE-2015-7501 环境搭建 cd vulhub-master/jboss/JMXInvokerServlet-deserialization docker-compose up -d 1.POC&#xff0c;访问地址 172.16.1.4:8080/invoker/JMXInvokerServlet 返回如下&#xff0c;说明接⼝开放&#xff0c;此接⼝存在反序列化漏洞 2.下载 ysose…

7.C++程序中的基本数据类型-数据类型之间的转换

在C中&#xff0c;类型转换是将一个数据类型转为另外一个数据类型&#xff0c;其转换过程比较复杂&#xff0c;目前只讨论基本数据类型之间的转换。 类型转换分为两部分&#xff1a;隐式转换和显示转换 隐式转换又称为自动转换&#xff0c;显示转换又称为强制转换。 隐式转换…

[Linux] Linux进程PCB内部信息的深入理解

标题&#xff1a;[Linux] Linux进程PCB内部信息的深入理解 个人主页&#xff1a;水墨不写bug &#xff08;图片来自网络&#xff09; 目录 一.查看进程 二.认识并了解进程的关键信息 I&#xff0c;PID/PPID II&#xff0c;exe III&#xff0c;cwd 三、fork&#xff08;&…

vue源码分析(九)—— 合并配置

文章目录 前言1.vue cli 创建一个基本的vue2 项目2.将mian.js文件改成如下3. 运行结果及其疑问&#xff1f; 一、使用 new Vue 创建过程的 2 种场景二、margeOption的详细说明1.margeOption的方法地址2.合并策略的具体使用3.defaultStrat 默认策略方法 三&#xff1a;以生命周期…

OpenResty安装及使用

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

调用本地大模型服务出现PermissionDeniedError的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

【机器学习】---神经架构搜索(NAS)

这里写目录标题 引言1. 什么是神经架构搜索&#xff08;NAS&#xff09;1.1 为什么需要NAS&#xff1f; 2. NAS的三大组件2.1 搜索空间搜索空间设计的考虑因素&#xff1a; 2.2 搜索策略2.3 性能估计 3. NAS的主要方法3.1 基于强化学习的NAS3.2 基于进化算法的NAS3.3 基于梯度的…

ICM20948 DMP代码详解(38)

接前一篇文章&#xff1a;ICM20948 DMP代码详解&#xff08;37&#xff09; 上一回继续解析inv_icm20948_set_slave_compass_id函数&#xff0c;解析了第3段代码&#xff0c;本回解析接下来的代码。为了便于理解和回顾&#xff0c;再次贴出该函数源码&#xff0c;在EMD-Core\so…

队列+宽搜专题篇

目录 N叉树的层序遍历 二叉树的锯齿形层序遍历 二叉树最大宽度 在每个树行中找最大值 N叉树的层序遍历 题目 思路 使用队列层序遍历来解决这道题&#xff0c;首先判断根节点是否为空&#xff0c;为空则返回空的二维数组&#xff1b;否则&#xff0c;就进行层序遍历&#x…

chapter 12 Bandgap References

chapter 12 Bandgap References 这一章我们介绍电压和电流基准. 首先学习和输入电压无关的偏置和起动问题. 然后介绍和温度有关的基准电压, 考虑offset对输出电压的影响. 最后学习恒定gm偏置和state-of-the-art电压基准. 12.1 General Considerations 一般基准电流有三种形式…

鸿蒙OpenHarmony【轻量系统芯片移植案例】标准系统方案之瑞芯微RK3566移植案例

标准系统方案之瑞芯微RK3566移植案例 本文章是基于瑞芯微RK3566芯片的khdvk_3566b开发板&#xff0c;进行标准系统相关功能的移植&#xff0c;主要包括产品配置添加&#xff0c;内核启动、升级&#xff0c;音频ADM化&#xff0c;Camera&#xff0c;TP&#xff0c;LCD&#xff…

数据集-目标检测系列-摩托车检测数据集 motorcycle >> DataBall

数据集-目标检测系列-摩托车检测数据集 motorcycle >> DataBall 数据集-目标检测系列-摩托车&#xff08;motorcycle&#xff09;检测数据集 数据量&#xff1a;1W 想要进一步了解&#xff0c;请联系 DataBall。 DataBall 助力快速掌握数据集的信息和使用方式&#xf…

AI 智能名片链动 2+1 模式商城小程序中的体验策略

摘要&#xff1a;本文探讨了在 AI 智能名片链动 21 模式商城小程序中&#xff0c;体验策略如何服务于用户体验&#xff0c;以及与产品策略的区别。重点分析了该小程序如何通过关注用户在使用过程中的流畅度、视觉体感等方面&#xff0c;实现“让用户用得爽”的目标&#xff0c;…

MWD天气图像多分类数据集,用于图像分类总共6个类别,多云,下雨,下雪,雾天,正常天气,共60000张图像数据

MWD天气图像多分类数据集&#xff0c;用于图像分类 总共6个类别&#xff0c;多云&#xff0c;下雨&#xff0c;下雪&#xff0c;雾天&#xff0c;正常天气&#xff0c;共60000张图像数据 MWD天气图像多分类数据集 (Multi-Weather Dataset, MWD) 数据集描述 MWD天气图像多分类…

哔哩哔哩自动批量删除抽奖动态解析篇(三)

前面两节已经介绍了如何获取抽奖动态列表和根据动态id_str索引值判断是否开奖了&#xff0c;接下来我们要完成的就是删除已经开奖了的动态 一、删除已经开奖的抽奖动态 老规矩&#xff0c;我们按F12健进入网页源代码&#xff0c;删除一条动态&#xff0c;观察网络接口 这次…

论文阅读 | 可证安全隐写(网络空间安全科学学报 2023)

可证安全隐写&#xff1a;理论、应用与展望 一、什么是可证安全隐写&#xff1f; 对于经验安全的隐写算法&#xff0c;即使其算法设计得相当周密&#xff0c;隐写分析者&#xff08;攻击者&#xff09;在观察了足够数量的载密&#xff08;含有隐写信息的数据&#xff09;和载体…

Redis数据结构之list列表

一.list列表 列表相当于数组或者顺序表 它里面的元素是有序的&#xff0c;也就是可以通过下标进行访问。这里的有序的含义是要根据上下文区分的&#xff0c;有的时候&#xff0c;有序指的是升序/降序&#xff0c;有的时候有序指的是顺序很关键&#xff0c;俩个元素交换后就不…

Spring6梳理10—— 依赖注入之注入数组类型属性

以上笔记来源&#xff1a; 尚硅谷Spring零基础入门到进阶&#xff0c;一套搞定spring6全套视频教程&#xff08;源码级讲解&#xff09;https://www.bilibili.com/video/BV1kR4y1b7Qc 目录 10 依赖注入之注入数组类型属性 10.1 创建Emp实体类&#xff0c;Dept实体类 10.2…