线程安全
众所周知,Java是多线程的。但是,Java对多线程的支持其实是一把双刃剑。一旦涉及到多个线程操作共享资源的情况时,处理不好就可能产生线程安全问题。线程安全性可能是非常复杂的,在没有充足的同步的情况下,多个线程中的操作执行顺序是不可预测的。
Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。
Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里不再详细介绍JMM及锁的其他相关知识。但是我们要讨论一个问题,那就是锁到底是不是有利无弊的?
锁存在的问题
Java在JDK1.5之前都是靠synchronized
关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized
是悲观锁。
悲观锁机制存在以下问题:
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
一个线程持有锁会导致其它所有需要此锁的线程挂起。
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
与锁相比,volatile
变量是一个更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile
不能解决原子性问题,因此当一个变量依赖旧值时就不能使用volatile
变量。因此对于同步最终还是要回到锁机制上来。
乐观锁
乐观锁( Optimistic Locking
)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是Compare and Swap(CAS
)。
CAS
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
Java对CAS的支持
在JDK1.5 中新增java.util.concurrent
(J.U.C)就是建立在CAS之上的。相对于对于synchronized
这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
我们以java.util.concurrent
中的AtomicInteger
为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement
方法,该方法的作用相当于 ++i
操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1 ;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet( int expect, int update) {
return unsafe.compareAndSwapInt( this , valueOffset, expect, update);
}
} |
在没有锁的机制下需要字段value要借助volatile原语,保证线程间的数据是可见的。这样在获取变量的值的时候才能直接读取。然后来看看++i
是怎么做到的。
getAndIncrement
采用了CAS操作,每次从内存中读取数据然后将此数据和+1
后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet
利用JNI来完成CPU指令的操作。
ABA问题
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁的实现是通过版本号(version
)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1
操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。
总结
Java中的线程安全问题至关重要,要想保证线程安全,就需要锁机制。锁机制包含两种:乐观锁与悲观锁。悲观锁是独占锁,阻塞锁。乐观锁是非独占锁,非阻塞锁。有一种乐观锁的实现方式就是CAS ,这种算法在JDK 1.5中引入的java.util.concurrent
中有广泛应用。但是值得注意的是这种算法会存在ABA问题。
CAS与对象创建
另外,CAS还有一个应用,那就是在JVM创建对象的过程中。对象创建在虚拟机中是非常频繁的。即使是仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能正在给对象A分配内存空间,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题的方案有两种,其中一种就是采用CAS配上失败重试的方式保证更新操作的原子性
相关推荐
常见的乐观锁实现方式有两种:版本号机制和CAS(Compare and Swap)算法。 1. 版本号机制:在数据表中增加一个版本字段,每次读取数据时都记录当前版本号,当更新数据时,会比较当前版本号与数据库中的版本号是否...
CAS(Compare and Swap)是乐观锁的一种实现方式,常用于并发控制,以确保数据在被修改时不会发生冲突。在 Redis 的事务中,虽然没有像 SQL 事务那样的显式锁定机制,但通过 CAS 操作,可以实现类似的功能。 Redis ...
CAS(Compare and Swap,比较并交换)是乐观锁的一种常见实现机制,尤其在多线程和并发编程中被广泛应用。它是一个原子操作,由硬件支持,可以无锁地完成对内存位置的更新。CAS包含三个参数:旧值、预期值和新值。当...
CAS(Compare and Swap)算法是一种常用的乐观锁实现方式,尤其适用于Java语言中。CAS涉及三个参数:需要读写的内存值V、进行比较的值A以及拟写入的新值B。只有当内存值V等于A时,CAS才会用B更新V的值。如果V的值...
在Java开发中,乐观锁常见的实现方式有以下几种: 1. **版本号机制**:每个记录都有一个版本号字段,每次更新时都会将版本号加一。当尝试更新时,如果读取到的版本号与数据库中的版本号不一致,就认为数据已被其他...
所谓乐观锁,重视假设最好的情况,每次去拿数据都认为别人不会修改,所以不会上锁,但是会在更新时判断一下在此期间这个数据有没有更改,使用版本号机制和CAS算法实现,乐观锁适用于多读的应用类型,这样可以提高...
本文主要讨论了四种锁类型:乐观锁、悲观锁、自旋锁以及Java中的synchronized同步锁,并深入解析了synchronized锁的内部机制,包括其核心组件、实现方式以及锁的状态。 1. **乐观锁**:乐观锁假设在多线程环境下,...
Atomic类提供了一些原子操作的方法,如compareAndSet()方法,可以实现CAS操作。 自旋锁的优点是可以提高系统的性能,因为它可以减少锁争用的次数。自旋锁的缺点是可能会导致busy waiting的问题,即线程会一直等待锁...
乐观锁常见的实现方式有版本号机制和CAS(Compare And Swap)算法。乐观锁适用于读操作远多于写操作的场景,能够提高系统吞吐量。然而,如果冲突频繁,乐观锁可能导致大量重试,反而降低性能。 **版本号机制**: ...
- 考虑使用无锁数据结构或乐观锁的变种,如CAS(Compare and Swap)算法,以减少锁冲突。 5. **面试中可能涉及的问题**: - 请解释乐观锁和悲观锁的区别。 - 何时应该使用乐观锁,何时应该使用悲观锁? - 举例...
乐观锁有两种实现方式,一种是版本号机制,另一种是 CAS 算法。版本号机制是指在数据表中加上一个数据版本号字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的...
这种机制是乐观锁的一种实现,因为它假设大多数情况下不会有冲突,即使有冲突也能快速解决。 CAS操作包含三个参数:内存位置V、预期值A和新值B。如果内存位置V的值等于预期值A,则将V的值设置为B,否则不做任何操作...
在Java中,`java.util.concurrent.atomic`包下的原子变量类如AtomicInteger、AtomicLong等,使用了CAS算法来实现乐观锁。 **版本号机制**是乐观锁的一种实现方式,通过添加一个版本号字段来跟踪数据的修改。当读取...
乐观锁的实现通常是基于版本号或者时间戳的方式,通过比较版本号或者时间戳来确定数据是否被修改。 在选择锁的机制时,需要考虑的因素有很多,包括锁的开销、加锁失败后的处理方式、线程的切换成本等。选择合适的锁...
5. 掌握在分布式系统中如何实现锁,如分布式乐观锁(如基于CAS的乐观锁)和分布式悲观锁(如Zookeeper、Redis提供的分布式锁)。 在面试中,候选人应该能够深入讨论这些知识点,并结合实际工作经验分享如何在项目中...
本文将详细讲解几种常见的锁机制:悲观锁、乐观锁、共享锁和排他锁,并简要介绍分布式锁以及锁降级原理。 1. **悲观锁**: 悲观锁是一种保守的策略,它假设在读取数据时,数据极有可能被其他线程修改。因此,悲观...