Java面试题
- 所有的final修饰的字段都是编译期常量吗?
不是的,只有被final修饰的字段同时满足是基本数据类型或者String,并且在编译期间就可以确定值的情况下,才能被视为编译期常量。例如,final int x = 1;
和final String s = "Hello";
是编译期常量,但final Object o = new Object();
就不是,因为o的值需要在运行时才能确定。
- 如何理解private所修饰的方法是隐式的final?
这是因为private方法只能在当前类中被访问,不能被其他类访问,更不能被其他类重写。因此,虽然我们没有显式地将private方法标记为final,但它们的行为类似于final方法。
- final类型的类如何拓展?
final类型的类不能被继承,这是final关键字的一个重要特性。如果你想写一个类,复用所有String中的方法,同时增加一个新的toMyString()的方法,你可以创建一个新的类,该类内部持有一个String对象,然后提供所有String类的方法的代理实现,同时添加toMyString()方法。这种模式被称为组合。
- final方法可以被重载吗? 可以父类的final方法能不能够被子类重写?
final方法可以在同一个类中被重载,这是因为重载是在同一个类中,方法名相同但参数列表不同的多个方法共存。但是,final方法不能被子类重写,这是final关键字的主要特性之一。
- final域重排序规则?
final域重排序规则是指在构造函数中对final字段的赋值操作,必须在构造函数执行完毕之前完成。这是为了保证在对象被其他线程引用之前,final字段已经被初始化完毕,从而保证了final字段的不变性。
- final的原理?
final关键字的原理主要体现在Java编译器和JVM的实现上。在Java编译器层面,对于final变量,如果编译时可以确定其值,那么编译器会在编译时就将这个值写入到使用该常量的字节码中。对于final方法,编译器会禁止子类重写这个方法。对于final类,编译器会禁止其他类继承这个类。
在JVM层面,对于final字段,JVM会保证在构造函数执行完毕之前,这个字段已经被初始化。此外,JVM还会为final字段生成专门的字节码指令。
- 使用final的限制条件和局限性?
final关键字有以下的限制条件和局限性:
- final变量:一旦被初始化后,其值就不能被修改。
- final方法:不能被子类重写,但可以被子类继承和使用。
- final类:不能被继承,但可以被实例化。
- final参数:在方法内部不能修改参数的值。
这些限制条件在某些情况下可能会限制代码的灵活性,但是它们也提供了一些好处,比如提高了代码的安全性和可读性,以及在某些情况下可以提高代码的执行效率。
volatile
关键字的作用是什么?
volatile
是Java中的一种类型修饰符,用于标识一个变量可能会被多个线程同时修改和访问,其主要作用是保证变量的可见性和有序性。
volatile
能保证原子性吗?
volatile
不能保证复杂操作的原子性。例如,对于volatile int i
,i++
这种操作不能保证原子性,因为它实际上是一个复合操作,包括读取、修改和写入三个步骤。但是,对于单个的读操作或写操作,volatile
可以保证其原子性。
- 之前32位机器上共享的
long
和double
变量的为什么要用volatile
?
在32位的JVM中,long
和double
类型的变量的读写可能会被拆分成两个32位的操作,这可能会导致其他线程在读取这个变量的时候,看到一个既不是旧值也不是新值的中间状态。使用volatile
可以防止这种情况发生,因为volatile
可以保证对这些变量的读写操作的原子性。
- 现在64位机器上是否也要设置呢?
在64位的JVM中,long
和double
类型的变量的读写操作默认就是原子的,所以在大多数情况下,我们不再需要使用volatile
来保证它们的原子性。但是,如果这些变量需要被多个线程共享,我们仍然需要使用volatile
来保证它们的可见性。
i++
为什么不能保证原子性?
i++
实际上是一个复合操作,包括读取、修改和写入三个步骤。在这三个步骤中,都有可能被其他线程的操作打断,从而导致结果不符合预期。只有通过同步机制(例如synchronized
或Lock
),或者使用原子类(例如AtomicInteger
),才能保证i++
操作的原子性。
volatile
是如何实现可见性的?
volatile
通过内存屏障来实现可见性。当一个线程修改了一个volatile
变量后,它会立即将修改写入到主内存中,而不是缓存在CPU的缓存中。当另一个线程读取这个volatile
变量时,它会直接从主内存中读取,而不是从CPU的缓存中读取,从而保证了可见性。
volatile
是如何实现有序性的?
volatile
通过禁止指令重排来实现有序性。在JVM中,为了提高执行效率,编译器和处理器可能会对指令进行重排。但是,如果两个操作之间存在数据依赖性,这种重排可能会导致结果不符合预期。volatile
可以阻止这种重排,从而保证有序性。
volatile
的应用场景?
volatile
主要用于多线程环境,当一个变量需要被多个线程共享,且不需要同步,只需要保证可见性和有序性时,可以使用volatile
。例如,一个线程修改了一个标志位,另一个线程需要根据这个标志位来决定是否继续执行,这种情况下就可以使用volatile
。
Java的并发(Concurrency)工具类库Java.util.concurrent(JUC)主要包含以下几个部分:
基础设施:提供了Java并发编程的基本接口和类,如Runnable、Callable、Future和Thread等。
同步器:这部分主要提供了各种同步工具类,如Semaphore(信号量)、CountDownLatch(倒计时门闩)、CyclicBarrier(循环栅栏)、Exchanger(数据交换器)等。
并发集合:这部分提供了多种并发集合类,如ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue等。
Executor框架:这部分提供了线程池框架,包括ThreadPoolExecutor、ScheduledThreadPoolExecutor等。
锁:提供了多种锁相关的类,如ReentrantLock、ReentrantReadWriteLock、StampedLock等。
原子变量:提供了一系列的原子操作类,如AtomicInteger、AtomicLong、AtomicReference等。
每个部分的核心类如下:
基础设施:Thread、Runnable、Callable、Future。
同步器:Semaphore、CountDownLatch、CyclicBarrier、Exchanger。
并发集合:ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue。
Executor框架:Executor、ExecutorService、ThreadPoolExecutor、ScheduledExecutorService。
锁:Lock、ReentrantLock、ReentrantReadWriteLock、StampedLock。
原子变量:AtomicInteger、AtomicLong、AtomicReference。
最最核心的类有:
- 基础设施:Thread、Future。
- 同步器:Semaphore、CountDownLatch。
- 并发集合:ConcurrentHashMap、BlockingQueue。
- Executor框架:ExecutorService、ThreadPoolExecutor。
- 锁:ReentrantLock。
- 原子变量:AtomicInteger。
以上类是并发编程中经常会用到的类,它们提供了丰富的功能,可以满足大部分并发编程的需求。
- 线程安全的实现方法有哪些?
线程安全主要有以下几种实现方式:
- 同步(Synchronized):通过Java的关键字
synchronized
实现,它保证了同一时刻对数据的访问和操作只有一个线程。 - 锁(Lock):Java提供了更加灵活的锁机制,如ReentrantLock,可以实现公平锁和非公平锁,还提供了条件锁等高级功能。
- 原子类(Atomic):Java提供了一系列的原子类,如AtomicInteger、AtomicLong、AtomicReference等,它们通过CAS(Compare And Swap)操作实现线程安全。
- 并发容器:Java提供了一系列并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等,它们内部都实现了线程安全。
- 线程局部变量(ThreadLocal):通过为每个线程创建独立的变量副本,实现线程安全。
- 什么是CAS?
CAS,全称Compare And Swap,即比较并交换。它是一种无锁的线程安全实现方式。CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的当前值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。
- CAS使用示例,结合AtomicInteger给出示例?
1 |
|
在这个例子中,我们首先创建了一个AtomicInteger对象,并设置其初始值为0。然后我们期望它的值仍然为0(expect),如果是,我们将其更新为1(update)。
- CAS会有哪些问题?
CAS主要有以下几个问题:
- ABA问题:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现该值没有发生变化,但是实际上该值已经被其他线程修改过了。
- 循环时间长开销大:CAS是一种自旋操作,如果不断地CAS失败,会消耗大量的CPU资源。
- 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,CAS能够保证该变量的原子性,但是对于多个共享变量,CAS就无法保证其原子性。
- 针对这些问题,Java提供了哪几个解决的?
- ABA问题:Java提供了AtomicStampedReference和AtomicMarkableReference等类来解决ABA问题,它们通过给每个值加上一个版本号或标记来实现。
- 循环时间长开销大:可以通过限制CAS重试的次数来减少CPU的消耗。
- 只能保证一个共享变量的原子操作:可以通过AtomicReference来保证多个共享变量的原子性。
- AtomicInteger底层实现?
AtomicInteger的底层主要依赖Unsafe类提供的原生方法,如compareAndSwapInt
来实现CAS操作,从而实现原子性。
- CAS+volatile请阐述你对Unsafe类的理解?
Unsafe类提供了一些低层次的、操作系统级别的API,包括直接访问系统内存、执行类似于C语言的指针操作等。在AtomicInteger等原子类的实现中,主要利用了Unsafe类提供的CAS操作和volatile的内存语义,来实现线程安全的原子操作。
- 说说你对Java原子类的理解?
Java的原子类主要包括AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,它们提供了一种无锁的线程安全实现方式。这些类内部主要依赖Unsafe类提供的CAS操作来实现线程安全。它们通常用于在高并发环境下替代synchronized或Lock,因为它们的性能通常更高。
- AtomicStampedReference是什么?
AtomicStampedReference是一个带有”时间戳”(或者说是版本号)的引用类型的原子类,它可以解决CAS操作的ABA问题。它通过将版本号和值一起进行CAS操作,来保证线程安全。
- AtomicStampedReference是怎么解决ABA的?
AtomicStampedReference通过给每个值加上一个版本号(或者说是时间戳)来解决ABA问题。在每次操作的时候,不仅要比较值是否相等,还要比较版本号是否相等。如果版本号不相等,即使值相等,CAS操作也会失败。
- 内部使用Pair来存储元素值及其版本号java中还有哪些类可以解决ABA的问题?
除了AtomicStampedReference外,Java还提供了AtomicMarkableReference来解决ABA问题。AtomicMarkableReference通过给每个值加上一个布尔标记来解决ABA问题。在每次操作的时候,不仅要比较值是否相等,还要比较标记是否相等。如果标记不相等,即使值相等,CAS操作也会失败。
- 为什么LockSupport也是核心基础类?
LockSupport是Java并发编程的核心类之一,它提供了基本的线程同步原语。LockSupport类是构建许多同步组件和工具的基础,包括ForkJoinPool、FutureTask、AbstractQueuedSynchronizer(AQS)和ReentrantLock等。LockSupport中的park()和unpark()方法提供了一种安全和高效的方式来暂停和恢复线程。
- AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)
AQS是AbstractQueuedSynchronizer的简称,它是Java并发包中的一个基础框架,用于构建锁和同步组件。AQS内部通过一个int成员变量来表示同步状态,并提供了修改同步状态的方法。这些方法基于Unsafe类提供的CAS操作,以实现同步状态管理的线程安全。
同时,AQS使用了LockSupport的park()和unpark()方法来挂起和唤醒线程。当获取同步状态失败时,线程会被AQS通过LockSupport.park()方法挂起;当同步状态释放时,被挂起的线程会被AQS通过LockSupport.unpark()方法唤醒。
- 写出分别通过wait/notify和LockSupport的park/unpark实现同步的代码
通过wait/notify实现同步:
1 |
|
通过LockSupport的park/unpark实现同步:
1 |
|
- LockSupport.park()会释放锁资源吗?那么Condition.await()呢?
LockSupport.park()不会释放任何锁资源,它只是让当前线程进入等待状态。而Condition.await()方法会释放与Condition相关联的Lock锁。
- Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别?
- Thread.sleep():使当前线程进入TIMED_WAITING状态,不释放锁,指定时间后线程自动恢复。
- Object.wait():使当前线程进入WAITING状态,同时会释放锁,需要被其他线程调用notify/notifyAll唤醒。
- Condition.await():使当前线程进入WAITING状态,同时会释放与Condition相关联的Lock锁,需要被其他线程调用Condition的signal/signalAll唤醒。
- LockSupport.park():使当前线程进入WAITING状态,不释放任何锁资源,需要被其他线程调用LockSupport的unpark方法唤醒。
- 如果在wait()之前执行了notify()会怎样?如果在park()之前执行了unpark()会怎样?
如果在wait()之前执行了notify(),那么这个notify()的唤醒效果就会被忽略,因为没有线程在等待状态,所以wait()方法会让线程进入无限期的等待状态,除非有其他线程再次调用notify()。
而如果在park()之前执行了unpark(),那么下一次调用park()时,不会阻塞,因为unpark()具有许可的特性,可以先于park()调用,也就是说unpark()的唤醒效果可以被先行保留。
- 什么是AQS?
AQS,全称为AbstractQueuedSynchronizer,是Java并发包中的一个抽象类,它为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(如semaphores、events)提供了一个框架。AQS解决了实现同步器的大部分细节问题,自定义同步组件只需要实现共享和独占模式下获取和释放同步状态的方法即可。
- 为什么它是核心?
AQS是Java并发包中的核心组件,许多高级同步工具,如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask等,都是基于AQS实现的。AQS通过内部的同步队列来管理阻塞的线程,提供了丰富的状态操作方法,简化了同步器的实现。
- AQS的核心思想是什么?
AQS的核心思想是,如果被请求的状态未被当前线程持有,则该线程将会被加入到队列中,并在其线程调度的turn时被唤醒;如果恰好获取到了可以满足其需求的状态,那么该线程将会从该等待队列中移除,并且得以执行。
- AQS是怎么实现的?底层数据结构等
AQS的底层数据结构是一个双向链表(FIFO),用于维护等待获取资源的线程。AQS通过一个int类型的成员变量state来控制同步状态,通过内部的ConditionObject类来支持条件变量。
- AQS有哪些核心的方法?
AQS的核心方法主要有以下几个:
- acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态失败,则会进入同步队列等待,同时会阻塞当前线程。
- release(int arg):独占式释放同步状态,会唤醒后继节点。
- acquireShared(int arg):共享式获取同步状态,如果当前线程获取同步状态失败,则会进入同步队列等待,同时会阻塞当前线程。
- releaseShared(int arg):共享式释放同步状态,会唤醒后继节点。
- AQS定义什么样的资源获取方式?
AQS定义了两种资源获取方式:独占模式和共享模式。
- 独占模式:只有一个线程能执行,又根据是否按队列的顺序分为公平锁和非公平锁,如ReentrantLock。
- 共享模式:多个线程可同时执行,如Semaphore、CountDownLatch、CyclicBarrier。ReentrantReadWriteLock可以看成是组合式,允许多个线程同时对某一资源进行读。
- AQS底层使用了什么样的设计模式?
AQS底层使用了模板方法设计模式,定义了一套同步器的操作流程。子类通过实现AQS的几个protected方法来改变同步器的行为。
- AQS的应用示例?
ReentrantLock和Semaphore都是AQS的典型应用。ReentrantLock是一种基于AQS实现的可重入独占锁,Semaphore则是一种基于AQS实现的多线程并发控制工具。
- 什么是可重入,什么是可重入锁?
可重入,又称为递归调用,是指在同一个线程在外层方法获取锁的时候,再进入内层方法会自动获取锁。换句话说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
可重入锁,是指一个线程可以多次获取同一把锁。例如,一个线程在持有锁的情况下,再次请求同一把锁,如果这把锁是可重入的,那么请求会成功。
- 它用来解决什么问题?
可重入锁主要解决了方法或者代码块的重复调用的问题,避免了因重入造成的死锁问题。
- ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?说说其类内部结构关系。
ReentrantLock的核心是AQS,它通过内部类(FairSync和NonfairSync)继承AQS来实现的。这两个内部类分别实现了公平锁和非公平锁的逻辑。ReentrantLock在创建时,会根据传入的公平性参数,实例化对应的Sync对象。
- ReentrantLock是如何实现公平锁的?ReentrantLock是如何实现非公平锁的?
- 公平锁:通过队列来实现公平性。当锁被请求时,如果有其他线程正在等待,或者锁已经被其他线程持有,那么请求的线程会被加入到队列的尾部,并且不会试图去获取锁,直到它的turn。
- 非公平锁:当锁被请求时,会首先尝试去获取锁,如果获取失败,才会进入等待队列。
- ReentrantLock默认实现的是公平还是非公平锁?
ReentrantLock默认实现的是非公平锁。
- 使用ReentrantLock实现公平和非公平锁的示例?
1 |
|
- ReentrantLock和Synchronized的对比?
- ReentrantLock和synchronized都是可重入锁,都支持公平和非公平锁,但是ReentrantLock需要显式解锁,synchronized会在代码块结束时自动解锁。
- ReentrantLock提供了更高级的功能,例如能够中断等待锁的线程,或者限时等待获取锁,而synchronized则不具备这些功能。
- ReentrantLock通过Condition类提供了分组唤醒需要唤醒的线程的能力,而synchronized则只能随机唤醒一个线程或者唤醒所有线程。
- ReentrantLock的性能通常比synchronized要好。
- 为什么有了ReentrantLock还需要ReentrantReadWriteLock?
ReentrantLock是一个独占锁,同一时间只能有一个线程获取锁。在某些读多写少的场景下,如果使用ReentrantLock可能会导致并发性能较差,因为读操作并不需要互斥。ReentrantReadWriteLock引入了读锁和写锁的概念,读锁是共享的,写锁是独占的,这样在读多写少的场景下,读操作可以并发进行,提高了并发性能。
- ReentrantReadWriteLock底层实现原理?
ReentrantReadWriteLock的实现也是基于AQS的。它通过一个int变量来表示读锁和写锁的状态,高16位表示读锁,低16位表示写锁。读锁是共享的,写锁是独占的。
- ReentrantReadWriteLock底层读写状态如何设计的?
ReentrantReadWriteLock的状态是由一个int变量表示的,这个int变量的高16位表示读锁的重入次数,低16位表示写锁的重入次数。
- 读锁和写锁的最大数量是多少?
由于ReentrantReadWriteLock的状态是由一个int变量表示的,所以读锁和写锁的最大数量是2^16-1,即65535。
- 本地线程计数器ThreadLocalHoldCounter是用来做什么的?
ThreadLocalHoldCounter用于保存当前线程获取读锁的重入次数。
- 缓存计数器HoldCounter是用来做什么的?
HoldCounter是ThreadLocalHoldCounter的一个优化,用于减少ThreadLocal的查询次数。
- 写锁的获取与释放是怎么实现的?
写锁的获取和释放是通过AQS的acquire和release方法实现的。
- 读锁的获取与释放是怎么实现的?
读锁的获取和释放是通过AQS的acquireShared和releaseShared方法实现的。
- RentrantReadWriteLock为什么不支持锁升级?
锁升级是指一个线程在持有读锁的情况下,再尝试获取写锁,这种情况在ReentrantReadWriteLock中是不被允许的,因为这可能会导致死锁。例如,线程A持有读锁,线程B尝试获取写锁但失败,然后阻塞;此时,如果线程A尝试获取写锁(即尝试锁升级),那么线程A也会阻塞,这样就形成了死锁。
- 什么是锁的升降级?
锁的升级是指一个线程在持有读锁的情况下,再尝试获取写锁。锁的降级是指一个线程在持有写锁的情况下,再尝试获取读锁,然后释放写锁。ReentrantReadWriteLock支持锁的降级,但不支持锁的升级。
- 为什么HashTable慢?它的并发度是什么?
HashTable是线程安全的,它的线程安全是通过在修改数据时锁定整个HashTable来实现的。这意味着任何时候只能有一个线程能访问HashTable,这就是HashTable的并发度。这种方式在并发度较低时效率是可以接受的,但是在并发度较高时,由于大量线程阻塞等待获取锁,效率会变得非常低。
- ConcurrentHashMap并发度是什么?
ConcurrentHashMap的并发度是它的分段锁的数量。在JDK1.7中,ConcurrentHashMap内部使用一个Segment数组,每个Segment对象持有一个锁,一个Segment内部包含一个HashEntry数组,每个HashEntry对象就是一个键值对。不同的Segment之间可以并发操作。在JDK1.8中,ConcurrentHashMap取消了Segment的概念,直接用Node数组实现,每个Node就是一个键值对,对Node数组的不同部分可以并发操作。
- ConcurrentHashMap在JDK1.7和JDK1.8中实现有什么差别?
JDK1.7的ConcurrentHashMap使用分段锁机制,每个Segment持有一个锁,Segment的数量就是并发度。JDK1.8的ConcurrentHashMap取消了Segment的概念,直接使用Node数组和CAS操作来实现并发操作,提高了并发度。
- JDK1.8解决了JDK1.7中什么问题?
JDK1.8的ConcurrentHashMap解决了JDK1.7中并发度受限于Segment数量的问题,通过取消Segment的概念,提高了并发度。同时,JDK1.8的ConcurrentHashMap在处理哈希冲突时,引入了红黑树,提高了在高哈希冲突时的性能。
- ConcurrentHashMap JDK1.7实现的原理是什么?
JDK1.7的ConcurrentHashMap使用分段锁机制,内部使用一个Segment数组,每个Segment对象持有一个锁,每个Segment内部包含一个HashEntry数组,每个HashEntry对象就是一个键值对。对不同的Segment的操作可以并发进行,每个Segment的操作是互斥的。
- ConcurrentHashMap JDK1.8实现的原理是什么?
JDK1.8的ConcurrentHashMap取消了Segment的概念,直接使用Node数组和CAS操作来实现并发操作。对Node数组的不同部分可以并发操作,每个Node就是一个键值对。在处理哈希冲突时,如果链表长度超过一定阈值(默认为8),链表会转化为红黑树,提高查找效率。
- ConcurrentHashMap JDK1.7中Segment数(concurrencyLevel)默认值是多少?
JDK1.7中ConcurrentHashMap的默认并发级别(concurrencyLevel)是16,也就是说默认会创建16个Segment。
- 为何一旦初始化就不可再扩容?
Segment数组的长度一旦初始化,就不能再进行扩容,这是因为Segment数组的长度决定了ConcurrentHashMap的并发度,如果允许扩容,那么在扩容过程中,需要重新分配键到新的Segment,这会带来较大的性能开销,并且在扩容过程中,需要锁定整个ConcurrentHashMap,降低并发性能。
- ConcurrentHashMap JDK1.7说说其put的机制?
JDK1.7中ConcurrentHashMap的put操作首先会定位到具体的Segment(通过hash值),然后对该Segment加锁,接着在Segment内部的HashEntry数组中查找是否已经存在该键,如果存在则更新值,如果不存在则在数组对应的位置创建新的HashEntry。
- ConcurrentHashMap JDK1.7是如何扩容的?
JDK1.7中ConcurrentHashMap的扩容是针对Segment内部的HashEntry数组进行的,当数组的元素数量超过阈值时,会触发扩容操作,扩容后数组的长度是原来的两倍。扩容操作需要对Segment加锁,保证扩容操作的线程安全。
- ConcurrentHashMap JDK1.8是如何扩容的?
JDK1.8中ConcurrentHashMap的扩容是针对整个Node数组进行的,当数组的元素数量超过阈值时,会触发扩容操作,扩容后数组的长度是原来的两倍。扩容操作使用CAS操作,无需锁定整个数组,提高了并发性能。
- ConcurrentHashMap JDK1.8链表转红黑树的时机是什么?临界值为什么是8?
当链表的长度超过阈值(默认为8)时,链表会转化为红黑树。这个阈值是通过经验得出的,当链表长度超过8时,链表的查找性能开始下降,而红黑树的查找性能则优于链表。
- ConcurrentHashMap JDK1.8是如何进行数据迁移的?
JDK1.8中ConcurrentHashMap的数据迁移是在扩容时进行的。在扩容过程中,会创建一个新的Node数组,然后将旧数组的元素迁移到新数组中。迁移操作使用CAS操作,无需锁定整个数组,提高了并发性能。