网站首页 > java教程 正文
难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”Java原子操作第四章:AtomicBoolean介绍与使用》一章中介绍了什么是原子操作类AtomicBoolean。
在《“全栈2019”Java原子操作第五章:AtomicInteger介绍与使用》一章中介绍了什么是原子操作类AtomicInteger。
在《“全栈2019”Java原子操作第六章:AtomicInteger灵活的运算方式》一章中介绍了使用原子操作类AtomicInteger的方法实现更灵活的运算方式。
在《“全栈2019”Java原子操作第七章:AtomicLong介绍与使用》一章中介绍了什么是原子操作类AtomicLong。
在《“全栈2019”Java原子操作第八章:AtomicReference介绍与使用》一章中介绍了什么是原子操作类AtomicReference<V>。
现在介绍原子数组AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray<E>。
2.什么是原子数组?
顾名思义,原子数组就是实现了原子操作的数组。
原子操作的概念和必备知识在该系列的《“全栈2019”Java原子操作第一章:内存可见性volatile关键字解析》和《“全栈2019”Java原子操作第二章:i++是原子操作吗?何为原子性》以及《“全栈2019”Java原子操作第三章:比较并交换CAS技术详解》三章中已经详细介绍过了,这里就不在赘述。不清楚的小伙伴请前去查阅相关章节。
3.原子数组有哪些?
java.util.concurrent.atomic包下一共有三个原子数组类:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray<E>。
其中:
AtomicIntegerArray是一个以原子方式操作int数组中的元素的类。
AtomicLongArray是一个以原子方式操作long数组中的元素的类。
AtomicReferenceArray<E>是一个以原子方式操作对象数组中的元素的类。
4.原子数组创建方式大同小异
AtomicIntegerArray的构造方法:
- AtomicIntegerArray(int length)
- AtomicIntegerArray(int[] array)
AtomicLongArray的构造方法:
- AtomicLongArray(int length)
- AtomicLongArray(long[] array)
AtomicReferenceArray<E>的构造方法:
- AtomicReferenceArray(int length)
- AtomicReferenceArray(E[] array)
可以看到的是三个原子数组的构造方法除了类名不同以外,第一个构造方法都是指定数组初始长度;第二个构造方法都是可以指定相应类型自定义数组。
5.为什么需要原子数组?
如果没有原子数组的话,在多线程操作数组元素的情况下,会出现并发问题。
就以三个原子数组中的AtomicIntegerArray来举例说明,我们可以先来看一个可以来看一个普通int数组操作元素时的场景。
首先,创建一个长度为10的int数组:
int数组一旦被创建出来,那么数组里面的元素初始值就为0。
接着,创建一个操作元素的任务:
然后,在run()方法里面对数组下标为0的元素进行自增:
run()方法书写完毕。
接下来,我们循环创建10个线程去执行操作元素的任务:
最后,获取数组下标为0的元素:
不过,延迟1秒钟再进行获取:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
数组下标为0的元素被自增了10次,结果为10,正确。
但是我们只要对程序稍加改动,结果就有所不同。
下面,我们改写例子。
在操作元素的任务中,每次自增元素之前使当前线程睡100毫秒:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
程序稍加改动之后,我们操作元素的结果就和之前的不同,这就是多线程操作同一数组元素产生的问题。
怎么解决呢?
我们只要对数组内的元素进行原子方式操作的话即可。
有什么方法可以实现对数组元素原子操作呢?
对于int数组而言,使用AtomicIntegerArray即可。
下面,我们就使用AtomicIntegerArray替代int数组:
既然我们用AtomicIntegerArray替代了int数组,那么数组内元素自增的写法也有所不同:
其中,getAndIncrement(int i)方法是对指定下标的元素进行自增操作,该操作是原子的。
当然了,我们获取AtomicIntegerArray里面的元素方式也有说改变:
其中,get(int i)方法是获取指定下标的元素。
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
因为AtomicIntegerArray是以原子方式操作元素的形式,所以即使在多线程的情况下也不会数据不一致产生问题。
以上就是普通int数组和AtomicIntegerArray的区别。
接下来,看看三个原子数组中的常用方法,它们大多数是一样的,只是参数类型不同而已。
6.AtomicIntegerArray常用方法
- get(int i)方法的作用是获取指定下标元素;
- set(int i, int newValue)方法的作用是设置指定下标元素的值。参数i是下标,newValue是我们可以指定的新值。
- getAndIncrement(int i)方法的作用是递增指定下标元素的值,返回递增前的值。
- incrementAndGet(int i)方法的作用是递增指定下标元素的值,返回递增后的值。
- getAndDecrement(int i)方法的作用是递减指定下标元素的值,返回递减前的值。
- decrementAndGet(int i)方法的作用是递减指定下标元素的值,返回递减后的值。
- getAndAdd(int i, int delta)方法的作用是指定下标元素与delta相加,返回相加前的值。
- addAndGet(int i, int delta)方法的作用是指定下标元素与delta相加,返回相加后的值。
- getAndUpdate(int i, IntUnaryOperator updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新前的值。
- updateAndGet(int i, IntUnaryOperator updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新后的值。
- getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算前的值。
- getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算后的值。
- compareAndSet(int i, int expectedValue, int newValue)方法的作用是如果当value==expectedValue,那么就将newValue赋给value,否则什么也不做。
7.AtomicLongArray常用方法
- get(int i)方法的作用是获取指定下标元素;
- set(int i, long newValue)方法的作用是设置指定下标元素的值。参数i是下标,newValue是我们可以指定的新值。
- getAndIncrement(int i)方法的作用是递增指定下标元素的值,返回递增前的值。
- incrementAndGet(int i)方法的作用是递增指定下标元素的值,返回递增后的值。
- getAndDecrement(int i)方法的作用是递减指定下标元素的值,返回递减前的值。
- decrementAndGet(int i)方法的作用是递减指定下标元素的值,返回递减后的值。
- getAndAdd(int i, long delta)方法的作用是指定下标元素与delta相加,返回相加前的值。
- addAndGet(int i, long delta)方法的作用是指定下标元素与delta相加,返回相加后的值。
- getAndUpdate(int i, LongUnaryOperator updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新前的值。
- updateAndGet(int i, LongUnaryOperator updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新后的值。
- getAndAccumulate(int i, long x, LongBinaryOperator accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算前的值。
- accumulateAndGet(int i, long x, LongBinaryOperator accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算后的值。
- compareAndSet(int i, long expectedValue, int newValue)方法的作用是如果当value==expectedValue,那么就将newValue赋给value,否则什么也不做。
8.AtomicReferenceArray<E>常用方法
- get(int i)方法的作用是获取指定下标元素;
- set(int i, E newValue)方法的作用是设置指定下标元素的值。参数i是下标,newValue是我们可以指定的新值。
- getAndUpdate(int i, UnaryOperator<E> updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新前的值。
- updateAndGet(int i, UnaryOperator<E> updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新后的值。
- getAndAccumulate(int i, E x, BinaryOperator<E> accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算前的值。
- accumulateAndGet(int i, E x, BinaryOperator<E> accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算后的值。
- compareAndSet(int i, E expectedValue, E newValue)方法的作用是如果当value==expectedValue,那么就将newValue赋给value,否则什么也不做。
接下来是方法示例。
注意:三个原子数组的方法用法一样只是参数类型不同,所以这里选取AtomicIntegerArray对象进行方法演示。
9.获取指定下标元素get(int i)方法
get(int i)方法的作用是获取AtomicIntegerArray对象中指定下标元素;
例如,我们创建一个AtomicIntegerArray对象:
然后,调用其get(int i)方法获取元素:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
获取完元素之后,怎么设置指定下标元素的值呢?
10.设置指定下标元素的值set(int i, int newValue)方法
set(int i, int newValue)方法的作用是设置指定下标元素的值。参数i是下标,newValue是我们可以指定的新值。
例如,我们创建一个AtomicIntegerArray对象:
然后,调用其set(int i, int newValue)方法将下标为0的元素的值设置为99:
接着,调用其get(int i)方法获取下标为0的元素:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
设置完元素之后,怎么指定下标元素自增呢?
11.指定下标元素递增方法
AtomicIntegerArray类中有两个递增方法:
- getAndIncrement(int i)
- incrementAndGet(int i)
其中,getAndIncrement(int i)方法相当于i++,incrementAndGet(int i)方法相当于++i。
getAndIncrement(int i)方法的作用是返回递增前的值。
incrementAndGet(int i)方法的作用是返回递增后的值。
例如,我们创建一个AtomicIntegerArray对象:
先调用getAndIncrement(int i)方法使下标为0的元素递增一次并输出结果:
接着,调用incrementAndGet(int i)方法再使下标为0的元素递增一次并输出结果:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
下标为0的元素初始值为0,getAndIncrement(int i)方法返回递增前的值,也就是0,然后递增之后下标为0的元素的值变为1,接着调用incrementAndGet(int i)方法使其在1的基础上再递增一次变为2,结果为正确。
指定下标元素递增之后,怎么指定下标元素递减呢?
12.指定下标元素递减方法
AtomicIntegerArray类中有两个递减方法:
- getAndDecrement(int i)
- decrementAndGet(int i)
其中,getAndDecrement(int i)方法相当于i--,decrementAndGet(int i)方法相当于--i。
getAndDecrement(int i)方法的作用是返回递减前的值。
decrementAndGet(int i)方法的作用是返回递减后的值。
例如,我们创建一个AtomicIntegerArray对象:
先调用getAndDecrement(int i)方法使下标为0的元素递减一次并输出结果:
接着,调用decrementAndGet(int i)方法再使下标为0的元素递减一次并输出结果:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
下标为0的元素初始值为0,getAndDecrement(int i)方法返回递减前的值,也就是0,然后递减之后下标为0的元素的值变为-1,接着调用decrementAndGet(int i)方法使其在-1的基础上再递减一次变为-2,结果为正确。
指定下标元素递减之后,怎么指定下标元素与任意值相加呢?
13.指定下标元素与任意值相加方法
前面两小节演示的是递增和递减情况,如果想要指定下标元素加上任意数,比如+5,或者你想要减去任意数,比如-5,使用前面那几个方法是做不到的,所以AtomicIntegerArray类也为我们提供相应方法。
AtomicIntegerArray类中有两个指定下标元素与任意值相加方法:
- getAndAdd(int i, int delta)
- addAndGet(int i, int delta)
其中:
getAndAdd(int i, int delta)方法的作用是返回相加前的值。
addAndGet(int i, int delta)方法的作用是返回相加后的值。
例如,我们创建一个AtomicIntegerArray对象:
先调用getAndAdd(int i, int delta)方法使下标为0的元素与2相加并输出结果:
接着,调用addAndGet(int i, int delta)方法再使下标为0的元素与-3相加并输出结果:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
下标为0的元素初始值为0,getAndAdd(int i, int delta)方法返回相加前的值,也就是0,然后相加之后下标为0的元素的值变为2,接着调用addAndGet(int i, int delta)方法使其在2的基础上再相加一次-3变为-1,结果为正确。
在了解指定下标元素与任意值相加之后,我想使用自定义方式更新指定下标元素该怎么做呢?
14.使用自定义方式更新指定下标元素
前面几个小节演示的都是和指定数的加法或减法,如果我想自定义更新指定下标元素的值怎么做呢?
为此,AtomicIntegerArray类提供了两个使用自定义方式更新指定下标元素的值的方法:
- getAndUpdate(int i, IntUnaryOperator updateFunction)
- updateAndGet(int i, IntUnaryOperator updateFunction)
其中:
getAndUpdate(int i, IntUnaryOperator updateFunction)方法的作用是返回更新前的值。
updateAndGet(int i, IntUnaryOperator updateFunction)方法的作用是返回更新后的值。
getAndUpdate(int i, IntUnaryOperator updateFunction)方法和updateAndGet(int i, IntUnaryOperator updateFunction)方法都需要一个IntUnaryOperator类型的参数,而IntUnaryOperator是一个接口:
我们只需实现IntUnaryOperator接口中的applyAsInt(int operand)方法即可:
applyAsInt(int operand)方法是写如何更新元素的地方。
下面,我们就来试试getAndUpdate(int i, IntUnaryOperator updateFunction)方法和updateAndGet(int i, IntUnaryOperator updateFunction)方法。
首先,我们创建一个AtomicIntegerArray对象:
先调用getAndUpdate(int i, IntUnaryOperator updateFunction)方法使下标为0的元素与5相加并输出结果:
接着,调用updateAndGet(int i, IntUnaryOperator updateFunction)方法再使下标为0的元素与2相乘并输出结果:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
下标为0的元素初始值为0,getAndUpdate(int i, IntUnaryOperator updateFunction)方法返回递减前的值,也就是0,然后下标为0的元素与5相加变为5,接着调用updateAndGet(int i, IntUnaryOperator updateFunction)方法使其在5的基础上再乘以2变为10,结果为正确。
在了解使用自定义方式更新指定下标元素之后,我想指定下标元素与指定值进行二元运算该怎么做呢?
15.指定下标元素与指定值进行二元运算
如果我们想指定下标元素与指定值进行二元运算该怎么做呢?
为此,AtomicIntegerArray类提供了两个指定下标元素与指定值进行二元运算的方法:
- getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)
- accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction)
其中:
getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)方法的作用是返回运算前的值。
accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction)方法的作用是返回运算后的值。
参数解释
int i:指定参与运算的下标元素,即left值(结合IntBinaryOperator即可来看)。
int x:指定参与运算的值,即right值(结合IntBinaryOperator接口来看)。
IntBinaryOperator accumulatorFunction:二元运算的方式。
其中,IntBinaryOperator是一个接口:
我们只需实现IntBinaryOperator接口中的applyAsInt(int left, int right)方法即可:
applyAsInt(int left, int right)方法是写如何将两个元素进行二元运算的地方。
示例
首先,我们创建一个AtomicIntegerArray对象:
先调用getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)方法使下标为0的元素与4相加并输出结果:
接着,调用accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction)方法再使下标为0的元素与5相乘并输出结果:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
下标为0的元素初始值为0,先调用getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)方法返回递减前的值,也就是0,然后下标为0的元素与4相加变为4,接着调用accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction)方法使其在4的基础上再乘以5变为20,结果为正确。
在了解指定下标元素与指定值进行二元运算之后,我想知道AtomicIntegerArray有体现CAS算法吗?
16.CAS算法体现
我们之前在《“全栈2019”Java原子操作第三章:比较并交换CAS技术详解》一章中学习过什么是CAS算法。
在AtomicIntegerArray类中也有体现:
compareAndSet(int i, int expectedValue, int newValue)方法的作用是如果当value==expectedValue,那么就将newValue赋给value,否则什么也不做。
参数解释
int i:操作指定的下标元素。
int expectedValue:预期值。
int newValue:新值。
示例
首先,我们创建一个AtomicIntegerArray对象并指定初始值,初始值为0:
然后,在调用compareAndSet(int i, int expectedValue, int newValue)方法之前获取一次下标为0的元素:
接着,调用compareAndSet(int i, int expectedValue, int newValue)方法并输出方法返回结果:
最后,我们再获取一次下标为0的元素:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
当然了,你也可以将预期值改为一个非原值的数,这样赋值就不成功。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/atomic/AtomicIntegerArray
总结
- AtomicIntegerArray是一个以原子方式操作int数组中的元素的类。
- AtomicLongArray是一个以原子方式操作long数组中的元素的类。
- AtomicReferenceArray<E>是一个以原子方式操作对象数组中的元素的类。
- get(int i)方法的作用是获取指定下标元素;
- set(int i, int newValue)/set(int i, long newValue)/set(int i, E newValue)方法的作用是设置指定下标元素的值。参数i是下标,newValue是我们可以指定的新值。
- getAndIncrement(int i)方法的作用是递增指定下标元素的值,返回递增前的值。【注:AtomicReferenceArray<E>无此方法】
- incrementAndGet(int i)方法的作用是递增指定下标元素的值,返回递增后的值。【注:AtomicReferenceArray<E>无此方法】
- getAndDecrement(int i)方法的作用是递减指定下标元素的值,返回递减前的值。【注:AtomicReferenceArray<E>无此方法】
- decrementAndGet(int i)方法的作用是递减指定下标元素的值,返回递减后的值。【注:AtomicReferenceArray<E>无此方法】
- getAndAdd(int i, int delta)/getAndAdd(int i, long delta)方法的作用是指定下标元素与delta相加,返回相加前的值。【注:AtomicReferenceArray<E>无此方法】
- addAndGet(int i, int delta)/addAndGet(int i, long delta)方法的作用是指定下标元素与delta相加,返回相加后的值。【注:AtomicReferenceArray<E>无此方法】
- getAndUpdate(int i, IntUnaryOperator updateFunction)/getAndUpdate(int i, LongUnaryOperator updateFunction)/getAndUpdate(int i, UnaryOperator<E> updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新前的值。
- updateAndGet(int i, IntUnaryOperator updateFunction)/updateAndGet(int i, LongUnaryOperator updateFunction)/updateAndGet(int i, UnaryOperator<E> updateFunction)方法的作用是更新指定下标元素,更新方式由参数updateFunction决定,方法返回更新后的值。
- getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)/getAndAccumulate(int i, long x, LongBinaryOperator accumulatorFunction)/getAndAccumulate(int i, E x, BinaryOperator<E> accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算前的值。
- getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction)/accumulateAndGet(int i, long x, LongBinaryOperator accumulatorFunction)/accumulateAndGet(int i, E x, BinaryOperator<E> accumulatorFunction)方法的作用是指定下标元素与参数x进行二元运算,运算方式由参数accumulatorFunction决定,方法返回运算后的值。
- compareAndSet(int i, int expectedValue, int newValue)/compareAndSet(int i, long expectedValue, int newValue)/compareAndSet(int i, E expectedValue, E newValue)方法的作用是如果当value==expectedValue,那么就将newValue赋给value,否则什么也不做。
至此,Java中原子数组AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray<E>相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java原子操作第八章:AtomicReference介绍与使用
下一章
“全栈2019”Java原子操作第十章:atomic包下原子更新器介绍
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!
猜你喜欢
- 2024-10-08 数组作为容器底层的数据结构,还是了解一下吧
- 2024-10-08 04、数组的注意事项 #Java(数组函数的使用方法)
- 2024-10-08 Java中的数组使用(java中数组用法)
- 2024-10-08 C++|实例解析函数指针数组的声明、初始化和使用
- 2024-10-08 小高分享(47)Java中的基本数据类型与数组
- 2024-10-08 Java基础:数组的特别之处(java,数组)
- 2024-10-08 定义结构体数组并初始化,定义结构体数组struct stu
- 2024-10-08 ArrayList初始化-Java那些事儿(java arraylist用法)
- 2024-10-08 看完这篇,轻松弄懂STM32 C语言变量的定义和初始化
- 2024-10-08 Java编程基础阶段笔记 day05 数组
你 发表评论:
欢迎- 最近发表
-
- pyinstaller打包python程序高级技巧
- 将python打包成exe的方式(python打包成exe的方法)
- Python打包:如何将 Flask 项目打包成exe程序
- py2exe实现python文件打包为.exe可执行程序(上篇)
- 如何将 Python 项目打包成 exe,另带卸载功能!
- Python打包成 exe,太大了该怎么解决?
- 可视化 Python 打包 exe,这个神器绝了!
- 案例详解pyinstaller将python程序打包为可执行文件exe
- Cocos 3.x 菜鸟一起玩:打包window程序
- 怎么把 Python + Flet 开发的程序,打包为 exe ?这个方法很简单!
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)