【JavaEE】线程安全(难点)

news2024/11/17 13:53:13

目录

前言

1、线程安全的概念 

2、线程不安全的原因 

3、产生抢占式执行与原子性问题

4、产生多个线程修改同一个变量的问题 

5、解决上述三个导致线程不安全的问题

5.1、锁 synchronized关键字

5.1.1、使用synchronized关键字进行加锁

 6、由内存可见性引起的线程不安全问题

7、解决内存可见性产生的线程不安全问题 

7.1、使用volatile关键字暂停优化(volatile保证内存的可见性)

 8、由指令重排序引起的线程不安全问题

9、解决由指令重排序引起的线程不安全问题 


前言

在说线程安全之前,先来了解一下静态方法和类对象这两个概念,静态方法是属于类的方法与对象没有关系。类对象(例如:TestDemo.class)我们写的.Java源代码文件通过javac编译成为.class二进制字节码文件。JVM就可以执行.class文件了,JVM想要执行这个.class文件就得先把文件内容读取到内存中,这个操作叫做类加载,类对象,就可以表示这个.class文件的内容,类对象描述了类的方方面面的详细信息,包括不限于:1、类的名字,2、类的属性,属性的名字、类型、权限,3、类有那些方法,方法的名字,参数,类型,权限,4、类继承自那个类,5、类实现了那些接口。此处类对象,就相当于"对象的图纸",有了这个图纸,才能了解这个对象是啥样的,进一步的才可以使用反射api来获取这里的一些信息。

1、线程安全的概念 

如果在多线程环境下代码运行的结果是符合我们的预期的,即在单线程环境先执行的结果,在多线程环境下执行的结果是不相同的。原因在于多线程的调度是无序的/随机的.所以导致单线程中执行的代码在多线程中执行的结果不同。这种情况就是线程不安全的代码。

本质原因:线程在系统中的调度是无序的/随机的(抢占式执行)

我们通过这个代码来了解一下,写一个count变量来计算,现在创建两个线程,都对这个count变量进行自增,我们的预期结果是通过两次自增,count的结果最终变为10w次,来观察一下实际结果与预期结果。

//线程不安全
class Counter{
    private int count = 0;
    public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        //搞两个线程,两个线程分别对这个counter自增5w次
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        //等待两个线程执行结束,然后看结果
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

 

可以看见代码的执行结果多次执行之后每次的结果都不一样,像是一个随机值,但是都没有达到我们的预期结果10w ,产生实际结果与预期结果不相符就是bug就是由多线程调度的不确定性(抢占式执行)造成的线程不安全问题。

2、线程不安全的原因 

线程不安全的原因

  1. 抢占式执行(罪魁祸首)
  2. 多个线程修改同一个变量
  3. 修改操作,不是原子的(原子:不可分割的最小单位)
  4. 内存可见性,引起的线程不安全
  5. 执行重排序,引起的线程不安全

上述产生线程不去安全的原因可以总结为三点:

1、原子性

  • 多条指令,这些指令之间存在先后依赖关系,在这多条指令执行期间,不能插入其他指令。

2、可见性

  • 系统调用CPU执行线程,一个线程对共享变量的修改,另一个线程能够立刻看见。

3、有序性

  • 程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序) 

我们通过上述代码来分析产生前三种线程不安全的原因。

3、产生抢占式执行与原子性问题

我们将add方法的方法体中的count++操作进行仔细分析,本质上是三个cpu指令构成的。

  1. load:把内存中的数据读取到cpu寄存器中。(count是一个变量,保存在内存中,想要修改count的值,首先要将内存中的值,读取到寄存器中)。
  2. add:就是把寄存器中的值,进行+1运算。
  3. save:把寄存器中的值写回到内存中。

 我们挑选其中的几种分析他们的执行过程。两个线程可以在一个CPU核心上分时复用(并发)进行执行,也可以在两个CPU核心上执行,我们为了方便理解,画图就使用两个CPU核心执行这两个线程。

1️⃣第一种情况,t1线程执行完毕之后执行t2线程

  此时看到,两个线程,各自自增一次,最终结果是2,此时是没有bug的,结果正确。

2️⃣第二种情况:t1线程和t2线程并发式执行。 

 此时我们按照上述执行过程,两个线程自增两次,最后结果是1,说明出现了bug,其中一次自增的结果,被另一次给覆盖了(无效自增)。

上述的多种情况按照这样的方式执行完成之后,只有两种没有出现bug,其余的都会出现bug.


4、产生多个线程修改同一个变量的问题 

 上述的第二个情况的说明,两个线程并发式执行,两个线程修改同一个变量,再一个线程没有将数据修改完成之前,有一个线程将内存中的数据读取进行修改,最后两个线程将数据都返回给内存。这个时候就产生了bug.

在上述代码的背景下,这些操作的安全性。


5、解决上述三个导致线程不安全的问题

如何解决线程不安全问题,还是需要从问题产生的原因入手,抢占式执行的问题我们是没有办法修改的,多个线程修改同一变量,在有些情况下是可以安全执行的,具体要看我们的代码如何编写,最后来看原子性问题,将上述代码中的count++操作变成原子的(不可分割的最小单位)。

上述的抢占式执行的问题,我们无法解决,多个线程修改同一个变量的问题在某些情况下,可以安全执行,所以我们只能从原子性原因入手,解决线程不安全的问题。 

我们可以通过加锁,将count++操作变成原子的。


5.1、锁 synchronized关键字

💥锁的作用:能够起到保证"原子性"的效果。

❗❗❗锁的两个核心操作:先以一个生活中的例子来了解加锁和解锁。

  • 加锁:相当于你进入一个房间之后,将门锁了起来,别人就不能进入这个房间,想要进入只能等待。
  • 解锁:只有我们在将锁释放,打开门之后,其他人才能进入这个房间之中。

就代码而言,一旦某个线程加锁了之后,其他线程也想加锁,就不能直接加上,就需要阻塞等待,一直等到拿到锁的线程释放了锁为止。我们可以将上述例子中的人比作线程,当先拿到锁的人,将锁打开(释放)之后,其他人才能获得锁。也就是上面的进入房间。

 我们的线程调度是抢占式执行的,所以当第一个线程将锁释放了之后,等待加锁的线程,谁先拿到锁,加锁成功,这是不确定的。


5.1.1、使用synchronized关键字进行加锁

1️⃣第一种写法,使用代码块

我们上述说到锁有两个核心操作,加锁和解锁,此处使用代码块的方式来表示,进入synchronized修饰的代码块的时候,就会触发加锁,出了synchronized代码块,就会触发解锁。这里的{}就相当于我们上述所说的房间,线程进入这个房间,其他线程想进入这个代码块,就需要当前线程,将代码块中的程序执行完成。

📕锁对象表示的是针对那个对象加锁

❗❗❗注意()里面的锁对象可以写作任意一个Object对象(内置类型不行,但是类类型是可以的)

如果两个线程针对一个对象(counter)加锁,此时就会出现"锁竞争"(一个线程先拿到了锁,另一个线程阻塞等待) ;如果两个线程,针对不同对象(counter、counter2、counter3)加锁,此时不会出现锁竞争,各自索取各自的锁。

2️⃣ 第二种写法,直接给方法加锁

❗❗❗ 注意:

如果直接给方法使用synchronized修饰普通方法此时就相当于以this为锁对象。

如果synchronized修饰静态方法(static),此时就不是给this加锁了,而是针对类对象加锁

当然我们更常见的还是手动指定锁对象。 

 在上述代码中,t1和t2线程是在竞争同一个锁对象,此时就会产生锁竞争(t1拿到锁,t2就得阻塞)。此时就可以保证++操作就是原子的,不受影响了。对上述代码中的两个线程执行过程进行画图理解。

❓❓❓说到这里很多同学会想到这个加锁操作和等待一个线程的效果一样(join),为什么不直接使用join呢?


❗❗❗其实这个说法是错误的,join是让两个线程完整的进行串行;加锁,是让线程的某个小部分串性了,大部分都是并发的。

例如上述线程t1和t2,这两个线程中大概的一些操做是

  1. 创建i
  2. 判定i<5000
  3. 调用add
  4. count++
  5. add返回
  6. i++
  7. ......

线程t1和t2在前三步的时候,会是并发执行,但是执行到第四步的时候,因为所对象是同一个counter,所以会发生阻塞。当一个线程将4执行完成之后,另一个线程就会执行4,两个线程将4执行完成之后,第5,6..步又会以并发的方式执行。

❗❗❗ 总结:加锁可能导致阻塞,代码阻塞,对于程序的效率肯定还是会有影响的,此处虽然加了锁,比不加锁慢一点,但是不加锁算的更准一点,比彻底串行化要快一点。

 📗锁对象的作用只是为了标识是否针对同一个对象加锁。如果锁对象不同,那么就不会存在锁竞争了,加不加锁就没有意义了。我们使用锁,就是为了保证某一段代码的原子性,保证线程的安全性,保证多线程执行不出现问题,我们就得引入锁竞争,要产生锁竞争就需要对同一个对象加锁。


 6、由内存可见性引起的线程不安全问题

我们通过下面的代码来了解内存可见性的问题。

❗❗❗预期效果:

t1通过flag == 0作为循环条件,初始情况下,将进入循环。t2通过控制台输入一个整数,一旦用户输入了一个非0的值,此时t1的循环就会立即结束。从而t1线程退出!

❗❗❗实际效果:

输入非0的值之后,t1线程并没有退出,循环没有结束,可以通过jconsole可以看到t1线程任然在执行,处在Runnable(运行)的状态。

实际效果不等于预期效果,我们这个代码产生了bug.

import java.util.Scanner;

public class ThreadDemo14 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0){

            }
            System.out.println("循环结束!t1结束!");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

❗❗❗上述代码中出现内存可见性的原因:

两个线程先执行t1线程,while(flag == 0)这个操作存在两个执行指令,一个为load(CPU寄存器从内存中读取数据),另一个为将寄存器中的数据与0进行比较。但是load指令相对于CPU而言时间开销很大,而将寄存器中的数据与0进行比较的操作的开销想读与load很小,而且while循环执行的时间非常短,这样while循环每次执行的时候都要执行这两个指令,且load读取的数据相同。此时编译器认为load操作可以优化掉所以在第一次循环的时候执行一次load,其他次循环的时候load操作直接被省略掉,只进行一个比较的操作(相当于复用之前寄存器中load的值)。

由于t1线程先执行,且循环的速度非常快,t2线程在调度运行的时候,t1线程已经执行了一段时间了这个时候while循环也已经执行了不止一次了,并且t2线程在修改数据的时候,t1线程还在运行过程中,此时while循环已经有很多次了,编译器这个时候认为load读取的数据是相同的,已经将load优化掉了。

但是编译器在将while循环中的load优化掉之后,t2线程中对flag进行了修改,这个时候由于编译器将load指令去掉了,t1线程中的while循环无法得到这个被修改的flag,t1线程中的循环无法结束,t1线程也就无法结束,就会造成我们看到的控制台输入数字,之后没有显示t1线程结束和jconsole中观察到t1线程在运行状态。

上述的bug可以归结为编译器优化产生的bug.

编译器优化:能够智能的调整你的代码执行逻辑,保证程序结果不变的前提下,通过加减语句,通过语句变换,通过一些操作,让整个程序执行的效率大大提升。

编译器对于单线程判定是非常准确的,但是在多线程下就不一定了,可能导致,调整之后效率提高了,但是结果变了(编译器出现了误判)引起bug.

❗❗❗所谓的内存可见性, 就是多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug,进一步导致了代码出现bug.

这里再来了解一个小的知识点

❓❓❓如果针对上述代码我们给while循环中添加一个sleep(1000)效果还会和while循环体中是空的效果一样吗?


❗❗❗答案肯定是不一样的,因为sleep相对于load这个指令来说消耗的时间是非常大的。加了sleep函数之后循环执行的速度就非常慢了,同一时间循环次数下降了,此时load操作 不再是负担了,load就不是主要矛盾了,sleep才是主要矛盾,编译器就没有必要优化load了。


7、解决内存可见性产生的线程不安全问题 

此时我们的处理方式,就是让编译器针对这个场景暂停优化。保证t1线程能过立即看见t2线程对flag的修改。

7.1、使用volatile关键字暂停优化(volatile保证内存的可见性)

被volatile修饰的变量,此时编译器就会禁止上述的优化,就能够保证每次循环,flag的值都是从内存中重新读取的数据与0进行比较。

 此时t2线程修改flag的值,t1线程就能感知到了,t1线程也就能够结束了。

虽然说将变量用volatile修饰,像上述的while循环中load不被优化掉,这只是让编译器不对代码做优化而已,volatile不保证原子性。

❗❗❗总结:

  • volatile使用的场景,是一个线程进行读操作,一个线程进行写操作的情况。
  • synchronized则是多个线程进行写的操作的情况。
  • volatile的这个效果,称为"保证内存可见性"。

 8、由指令重排序引起的线程不安全问题

 指令重排序也是编译器优化的策略,调整了代码的执行顺序,让程序更高效。

编译器优化的前提:保证代码逻辑不变,并且调整之后的结果要和之前是一样的。

指令重排序,我们通过生活中的例子来说明,代码不好演示,因为一个代码执行时,不能保证那次代码在执行过程中采用了指令重排序的优化方式。

 指令重排序的优化,单线程情况下可以保证调整之前和之后的结果是不变的。但是多线程情况下就不能保证了。我们通过这个伪代码来了解。

Class Student{
    public void learn(){
        System.out.println("正在学习!");
    }
}
public Class Test{
    public static void main(String[] args){
        Student s ;
        Thread t1 = new Thread(()->{
            s = new Student();
        });
        Thread t2 = new Thread(()-> {
            if(s != null){
                s.learn();
            }
        });
        t1.start();
        t2.start();
    }
}

上述代码中创建Student对象的操作大题的可以分成三步操作

  1. 申请内存空间。
  2. 调用构造方法(初始化内存的数据)
  3. 把对象的引用赋值给s(内存地址的赋值)。
  • 如果是单线程环境,此处就可以进行指令重排序1先执行,2和3谁先执行都可以。
  • 多线程环境下,假设t1按照1、3、2的顺序执行(最后初始化)。当t1执行完1、3之后,即将执行2的时候,t2线程开始执行,由于t1的3已经执行过了,这个引用已经非空了,(s这个引用现在只是指向了内存中的一块地址)t2开始尝试调用s.learn(),但是由于t1并没有被s所引用的对象进行初始化,此时learn会变成什么样子,就不知道了,很可能产生bug了。

以上就是指令重排序产生线程不安全的问题 


9、解决由指令重排序引起的线程不安全问题 

1️⃣上述的场景可以通过加锁来解决指令重排序引起的线程不全问题

上述这个例子没有保证创建对象时候的原子性,可以通过对创建对象这个操做进行加锁,给t1和t2线程设置相同的锁对象,让t1和t2产生锁竞争。还是上述的伪代码。


public class Test{
    public Object locker = new Object();
    public static void main(String[] args){
        Student s ;
        Thread t1 = new Thread(()->{
            synchronized(locker){
                s = new Student();
            }
        });
        Thread t2 = new Thread(()-> {
            synchronized(locker){
                if(s != null){
                    s.learn();
                    break;
                }
            }
                
        });
        t1.start();
        t2.start();
    }
}

2️⃣使用volatile对s进行修饰,创建Student对象的时候就会禁止指令重排序。(重点掌握整个方法解决指令重排序问题)

 public static void main(String[] args){
        volatile Student s ;//创建对象的时候,让代码严格按照上述的1、2、3步骤执行

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

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

相关文章

KubeEdge节点分组特性简介

01 边缘应用跨地域部署场景及问题 应用生命周期管理复杂导致运维成本提高 02 边缘节点分组管理 节点分组&#xff1a;将不同地区的边缘节点按照节点组的形式组织 边缘应用&#xff1a;将应用资源整体打包并满足不同节点组之间的差异化部署需求 流量闭环&#xff1a;将服务流量…

Oracle内存管理

文章目录 概念内存管理方式自动内存管理自动共享内存管理手工内存管理 内存管理的转换方式相关内存参数相关数据字典 概念 为满足数据库的需求&#xff0c;通过内存管理来维护 Oracle 实例内存结构的最优大小。Oracle数 据库基于与内存相关的初始化参数设置来管理内存。 内存管…

【LeetCode】654. 最大二叉树

1.问题 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。 递归地在最大值 左边 的 子数组前缀上 构建左子树。 递归地在最大值 右边 的 子数组后缀上 构建右子树。 返回 nums 构建的 最…

美颜SDK的隐私保护与安全性分析

随着智能手机和移动应用的普及&#xff0c;美颜SDK已经成为了很多应用的标配。美颜SDK的使用可以让用户在拍照或者视频聊天时&#xff0c;实现自拍美颜、滤镜、磨皮、瘦脸等效果。但是&#xff0c;在享受美颜SDK带来的便利的同时&#xff0c;我们也需要关注美颜SDK的隐私保护与…

跟着排序学时间复杂度

带着排序学时间/空间复杂度 排序和时间复杂度 带着排序学时间/空间复杂度冒泡排序选择排序选择排序法2原理&#xff1a; 插入排序希尔排序&#xff08;缩小增量排序&#xff09;堆排序快速排序归并排序不基于比较的排序计数排序 桶排序稳定性 时间复杂度是打开数据结构大门的第…

从0搭建Vue3组件库(十二):引入现代前端测试框架 Vitest

Vitest 是个高性能的前端单元测试框架,它的用法其实和 Jest 差不多,但是它的性能要优于 Jest 不少,还提供了很好的 ESM 支持,同时对于使用 vite 作为构建工具的项目来说有一个好处就是可以公用同一个配置文件vite.config.js。因此本项目将会使用 Vitest 作为测试框架。 安装 …

FS2956A 8V~120V降压5V2A4.2V3A恒压芯片

FS2956A 内置MOS 100V开关降压型DC-DC转换器&#xff0c;FS2956A 8-100V用于电动车 滑板车 液晶仪表 5V-USB充电IC方案2A 概述&#xff1a; FS2956A 是一款支持宽电压输入的开关降压型DC-DC&#xff0c;芯片内置100V/5A功率MOS&#xff0c;最高输入电压90V。FS2956A 具有低待…

初识滴滴交易策略之一:交易市场

初识系列前言 滴滴作为一家共享出行公司&#xff0c;利用信息技术构建了实时的、智能的在线交易市场&#xff0c;在这个庞大运转的市场之中&#xff0c;滴滴秉承着用户价值至上的宗旨&#xff0c;不断通过技术提升来实现更高效的运转效率和更贴心的用户体验。 为了使得大家能够…

蚂蚁实时低代码研发和流批一体的应用实践

摘要&#xff1a;本文整理自蚂蚁实时数仓架构师马年圣&#xff0c;在 Flink Forward Asia 2022 流批一体专场的分享。本篇内容主要分为四个部分&#xff1a; 1. 实时应用场景与研发体系 2. 低代码研发 3. 流批一体 4. 规划展望 Tips&#xff1a;点击「阅读原文」查看原文视频&a…

KingbaseES V8R6备份恢复系列之 -- system-Id不匹配备份故障

​ KingbaseES V8R6备份恢复案例之---system-Id不匹配备份故障 案例说明&#xff1a; 在KingbaseES V8R6执行备份时&#xff0c;在sys_log日志中出现system-id不一致的故障并伴随有归档失败&#xff0c;故障如下图所示&#xff1a; 适用版本&#xff1a; KingbaseES V8R6 一、问…

React 学习笔记

文章目录 React 简介React 特点React 学习前提React 第一个实例 React 简介 React 是一个用于构建用户界面的 JAVASCRIPT 库。 React主要用于构建UI&#xff0c;很多人认为 React 是 MVC 中的 V&#xff08;视图&#xff09;。 React 起源于 Facebook 的内部项目&#xff0c;用…

Flink窗口

目录 窗口 Flink “存储桶” 窗口分类 按照驱动类型分类 按照窗口分配数据的规则分类 滚动窗口 滑动窗口 会话窗口 全局窗口 窗口的生命周期 窗口 窗口&#xff1a;将无限数据切割成有限的“数据块”进行处理&#xff0c;以便更高效地处理无界流 在处理无界数据流时…

3.1 掌握绘图基础语法与常用参数

3.1 掌握绘图基础语法与常用参数 3.1.1 掌握pyplot基础语法1.创建画布与创建子图2.添加画布内容3.保存与展示图形 3.1.2 设置pyplot的动态rc参数线条常用的rc参数 Matplotlib库介绍 Matplotlib是Python中最常用的可视化工具之一&#xff0c;可以非常方便地创建海量类型的2D图表…

实验二十、压控电压源二阶 LPF 幅频特性的研究

一、题目 研究压控电压源二阶低通滤波电路品质因数 Q Q Q 对频率特性的影响。 二、仿真电路 电路如图1所示。集成运放采用 LM324AJ&#xff0c;其电源电压为 15V。 图 1 压控电压源二阶低通滤波电路幅频特性的测试 图1\,\,压控电压源二阶低通滤波电路幅频特性的测试 图1压控…

记录一次重装系统配置工作环境

128G固态换大硬盘&#xff0c;偷懒不想重装系统&#xff0c;利用diskgenius迁移系统&#xff0c;热迁移和PE都没能成功迁移&#xff0c;还不小心删掉了机械盘的所有分区。利用diskgenius搜索分区&#xff0c;恢复文件&#xff0c;勉强把一些数据文件保存下来了。但是软件又得重…

DOM是什么(DOM的节点类型)

学到DOM时&#xff0c;看到关于文档&#xff08;结构树&#xff09;、节点&#xff08;node&#xff09;、和DOM提供的一些方法获取&#xff08;找到&#xff09;所需的节点、还有DOM属性&#xff0c;我很混乱&#xff0c;我无法弄清节点的关系层级属性和方法的关系&#xff0c…

SQL常用语句总结

一&#xff0c;简介 1.1 数据库是用来存放数据的&#xff0c;对数据库的操作需要用到SQL语句 1.2 数据库种类有也非常多&#xff1a; 关系型数据库&#xff1a; Oracle、DB2、Microsoft SQL Server、Microsoft Access、MySQL、SQLite 非关系型数据库&#xff1a; NoSql、Cl…

Vue Cli 之 环境变量和模式

一、环境变量 ​ 我们在使用 Vue-cli 创建的Vue项目中&#xff0c;可以在构建和运行时为项目设置环境变量&#xff0c;这些环境变量会根据环境&#xff08;模式&#xff09;的不同&#xff0c;而自动注入到项目中&#xff0c;也就是说我们可以根据环境不同&#xff0c;设置不同…

二进制单节点搭建 Kubernetes v1.20

目录 第一章.操作系统初始化配置 1.1.安装环境部署 1.2.部署 docker引擎 第二章.部署 etcd 集群 2.1.ETCD简述 2.2.准备签发证书环境 在 master01 节点上操作 2.3. 生成Etcd证书​​​​​​​ 2.4.在 node01 节点上操作 在 node02 节点上操作 2.5.部署 Master 组件…

SpringBoot ( 四 ) 接值

2.5.接值 通过方法的参数来接收请求传来值 请求时传值的方式有三种方式 : URL?namevalueform表单Ajax 异步传值 接收传来的值有三类 : 单一值对象数组 2.5.0.传值 2.5.0.1.URL?传值 URL?标识1值1&标识2值2 URL后面使用 ? 连接参数, 每组参数使用 连接标识与值, 多…