【记录】什么是CAS机制?

CAS(compareAndSwap):比较并替换

在具体了解cas机制之前,我们先来看两个案例:

(1)示例:启动两个线程,每个线程中让静态变量count循环累加100次。
那么最终count结果是多少?一定是200吗?

public static int count = 0;
public static void main(String[] args){
	// 开启两个线程
	for(int i = 0;i < 2;i++){
		new Thread(
			new Runable(){
				public void run(){
					try{
						Thread.sleep(10);
					} catch (InterruptedException e){
						e.printStackTrace();
					}
					for(int j = 0;j < 100;j++){
						count++;
					}
				}
			}
		).start();
	}
	try{
		Thread.sleep(2000);
	} catch(InterruptedException e){
		e.printStackTrace();
	}
	System.out.println("count = " + count);
}

这段代码并不是线程安全的,所以结果count有可能是小于200的
为了保证count输出结果是200,我们可以加上Synchronuzed同步锁

public static int count = 0;
public static void main(String[] args){
	// 开启两个线程
	for(int i = 0;i < 2;i++){
		new Thread(
			new Runable(){
				public void run(){
					try{
						Thread.sleep(10);
					} catch (InterruptedException e){
						e.printStackTrace();
					}
					for(int j = 0;j < 100;j++){
						Synchronized(当前类.class){
							count++;
						}		
					}
				}
			}
		).start();
	}
	try{
		Thread.sleep(2000);
	} catch(InterruptedException e){
		e.printStackTrace();
	}
	System.out.println("count = " + count);
}

加入同步锁之后,count自增就变成了原子性操作,所以最终输出的结果一定是200,代码实现了线程安全。

但是Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,在争夺到锁资源后恢复为runnable状态,这个过程之后牺牲的性格比较大,尽管java1.6对其进行了优化(偏向锁->轻量级锁->重量级锁),但是Synchronized仍然存在性能较低的情况

(2)我们经常使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如说一个常见的操作a++。这个操作其实可以细分成三个步骤:

(1)从内存中读取a
(2)对a进行加1操作
(3)将a的值重新写入内存中

在单线程状态下这个操作没有问题,但是在多线程中就会出现各种各样的问题。因为可能一个线程对a进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值。造成了线程的不安全现象。如何去解决这个问题呢?

解决办法:
通过java中的原子操作类:
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以atomic开头的包装类,例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于boolean,integer,long类型的原子性操作。

通过原子操作类,我们可以将案例1中的代码修改为

public static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args){
	// 开启两个线程
	for(int i = 0;i < 2;i++){
		new Thread(
			new Runable(){
				public void run(){
					try{
						Thread.sleep(10);
					} catch (InterruptedException e){
						e.printStackTrace();
					}
					for(int j = 0;j < 100;j++){
						count.incrementAndGet();
					}
				}
			}
		).start();
	}
	try{
		Thread.sleep(2000);
	} catch(InterruptedException e){
		e.printStackTrace();
	}
	System.out.println("count = " + count);
}

这样最终count的输出结果同样可以保证是200,而且性能比Synchronized要高。

incrementAndGet方法底层
在这里插入图片描述
在这里插入图片描述
Atomic操作类的底层,compareAndSwapInt方法其实就是CAS机制

分析CAS

cas包含 3 个参数 CAS(V,A,B),V表示内存地址,A表示旧的预期值(可以理解为原值),B表示要修改的新值(更改成功后的值)。仅当 V值等于A值时(内存地址中对应的值与原值相同),才会将V的值设为B,如果V值和A值不同,则说明已经有其他线程做了更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值(其实此时内存地地址存储的已经是其他线程更改之后的最新值)。

示例:
V = 1,A = 1,B = 2
1.线程1想要将原有值+1,即A = 2,在提交更新之前
2.此时线程2抢先一步,将V中的值率先更新为2(其实线程1要做的事)
3.线程1提交更新失败,因为此时V = 2,A = 1,A != V;
4.接下来线程1重新获取V值,并计算想要修改的新值,此时对于线程1来说,V=2,A=2,B=3,这个重新尝试的过程称为自旋
5.这时没有其他线程抢先更新,V = A ,线程1成功提交更新,最终V = 3;

Synchronized属于悲观锁,CAS属于乐观锁

在java中,Atomic系列类,Lock系列类的底层实现都采用了CAS机制
java1.6以上版本,Synchronized转变为重量级锁之前,也会采用CAS机制

CAS优点

cas是一种非阻塞的轻量级的乐观锁,什么是非阻塞式的呢?其实就是一个线程想要获得锁,对方会给一个回应表示这个锁能不能获得。在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。

CAS缺点

1:cpu开销比较大
在并发量比较大的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,会给cpu带来较大的压力
在某些并发量较大的场景,synchronized反而效率更高

2:不能保证代码块的原子性
CAS机制保证的只是一个变量的原子性操作,并不能保证代码块的原子性,如果需要多变量同时进行原子性的更新,需要使用synchronized

3:ABA问题
假设一个变量 A ,修改为 B之后又修改为 A,CAS 的机制是无法察觉的,但实际上已经被修改过了

示例:老王有100元存款,需要使用一个提款机来提取50元
提款机年久失修,将提款操作提交了两次,开启了两个线程

线程1:获取当前值100元->更新为50元 V = 100,A = 100,B = 50
线程2:获取当前值100元->更新为50元 V = 100,A = 100,B = 50
正常情况下线程1成功后,线程2会失败,因为此时 V = 50,A = 100;

现在线程1执行成功了,但是线程2在获取到V = 100 后阻塞了,在线程2阻塞的同时,老王的媳妇刚好给老王汇款50元

线程3:获取当前值50->更新为100元。 V = 50,A = 50,B = 100

在现成执行成功后,线程2突然不阻塞了,但是由于阻塞之前获取的V=100,而此时A刚好也是100,所以把老王的余额更新为50。

老王回家之后跟媳妇已对账发现钱不对,挨了一顿毒打,这就是ABA问题引起的

解决办法:添加版本号

线程1:获取当前值100元->更新为50元 V = 100,A = 100,B = 50; verson = 01
线程2:获取当前值100元->更新为50元 V = 100,A = 100,B = 50; verson = 01
线程3:获取当前值50->更新为100元。 V = 50,A = 50,B = 100

线程1跟线程2初始版本号都为01,线程1提交成功,变量版本号变为02,
线程2阻塞,版本号为01
线程3提交成功,变量版本号变成03
线程2阻塞恢复,想要更新变量,变量版本号01!=03,更新失败

java中,AtomicStampedReference类实现了采用版本号作比较的CAS机制

<think>好的,我现在需要回答用户关于数据库为什么需要锁机制,以及有哪些锁机制的问题。首先,我需要回想一下之前用户问过排他锁和读写锁,还有死锁的例子,所以这次的问题应该是在那基础上的扩展。 用户的问题分两部分:为什么需要锁机制,以及有哪些锁机制。我需要先回答为什么需要,再列举不同的锁机制,可能还要简单说明每种机制的作用。 首先,为什么需要锁机制?数据库通常会有多个事务同时操作数据,如果不加控制,可能会出现数据不一致的问题。比如两个事务同时修改同一数据,可能导致覆盖或者错误的结果。锁机制的作用就是确保并发操作时的数据一致性,避免脏读、不可重复读、幻读等问题。另外,锁还能保证事务的隔离性,这是ACID里的一个特性。所以,我需要从并发控制、数据一致性、事务隔离性这几个方面来阐述原因。 接下来是锁机制的种类。根据之前的回答,用户已经了解排他锁(X锁)和共享锁(S锁),也就是读写锁。但可能还有其他类型的锁需要介绍,比如表级锁、行级锁、意向锁、乐观锁、悲观锁等。需要区分不同的分类方式,比如按锁的粒度分,按锁的性质分,或者按加锁策略分。 需要注意用户可能对某些术语不太熟悉,比如意向锁是什么,或者乐观锁和悲观锁的区别。可能需要用简单的例子来解释。 另外,用户可能想知道锁机制如何工作,比如行锁和表锁的应用场景,以及它们对性能的影响。比如行级锁粒度小,并发高,但开销大;表级锁反之。意向锁是为了在更高粒度上协调锁,比如事务在加行锁前先加意向锁,避免其他事务加表锁。 还有乐观锁和悲观锁,这两种属于并发控制的策略,乐观锁假设冲突少,通过版本号或时间戳实现;悲观锁则假设冲突多,先加锁再操作。需要解释它们的应用场景,比如乐观锁适合读多写少的情况。 总结来说,我需要结构清晰地分两部分:为什么需要锁,以及锁的种类,每个种类下分点说明,可能用例子帮助理解。同时,要避免过于技术化的术语,用简单的中文表达,保持逻辑连贯。 现在需要检查是否有遗漏的锁类型,比如间隙锁(Gap Lock),或者在MySQL中使用的Next-Key Lock,这些属于行锁的扩展,用于解决幻读问题。不过用户可能不需要太深入的细节,所以可能简要提及即可。 此外,可能需要提到数据库的隔离级别,不同隔离级别下锁的使用方式不同,比如读未提交、读已提交、可重复读、串行化,但这个问题可能更偏向锁机制本身,而不是隔离级别,所以可能不需要展开。 最后,确保回答符合之前的格式要求,使用中文,结构清晰,分点说明,正确使用LaTeX公式(如果有的话),但用户这次的问题可能不需要数学公式,所以重点在文字描述和例子。</think>### **数据库为什么需要锁机制?** 数据库的锁机制是**解决并发操作引发数据冲突**的核心工具,主要为了解决以下问题: 1. **保障数据一致性** 当多个事务同时读写数据时,可能引发**脏读**(读到未提交的修改)、**不可重复读**(同一事务内多次读取结果不同)、**幻读**(其他事务插入/删除数据导致结果集变化)等问题。锁机制通过控制并发访问顺序,确保事务的隔离性(ACID中的“I”)。 2. **维护事务隔离级别** 不同隔离级别(如读未提交、读已提交、可重复读、串行化)依赖锁机制实现。例如: - **读已提交**:通过共享锁阻止其他事务修改已读数据。 - **可重复读**:通过长期持有锁或范围锁避免幻读。 3. **防止资源竞争冲突** 并发事务可能因争夺同一数据导致结果错误。例如: - **丢失更新**:两个事务同时修改同一数据,后提交的覆盖前者的结果。 - **写冲突**:未加锁时,事务可能基于过期数据做出错误决策。 --- ### **数据库中的常见锁机制** #### **1. 按锁的互斥性分类** - **共享锁(Shared Lock,S锁)** - **作用**:允许多个事务并发读取同一数据,但阻止其他事务加排他锁。 - **场景**:高频读操作(如 `SELECT`)。 - **示例**: ```sql SELECT * FROM 订单表 WHERE 状态 = '待支付' LOCK IN SHARE MODE; ``` - **排他锁(Exclusive Lock,X锁)** - **作用**:独占数据,禁止其他事务加任何锁(包括共享锁和排他锁)。 - **场景**:数据修改(如 `UPDATE`、`DELETE`)。 - **示例**: ```sql SELECT * FROM 库存表 WHERE 商品ID = 'P100' FOR UPDATE; UPDATE 库存表 SET 数量 = 数量 - 1 WHERE 商品ID = 'P100'; ``` #### **2. 按锁的粒度分类** - **表级锁** - **特点**:直接锁定整张表,实现简单但并发度低。 - **场景**:批量数据操作(如全表更新)。 - **示例**: ```sql LOCK TABLES 用户表 WRITE; -- MySQL表级排他锁 ``` - **行级锁** - **特点**:仅锁定特定行,并发度高但管理开销大。 - **场景**:高并发单行操作(如电商扣减库存)。 - **示例**: ```sql -- InnoDB行级锁(通过索引实现) SELECT * FROM 账户表 WHERE 账户号 = 'A001' FOR UPDATE; ``` - **页级锁/块级锁** - **特点**:锁定数据页(物理存储单元),粒度介于表锁和行锁之间。 - **场景**:较少使用,如某些旧版数据库(如SQL Server)。 #### **3. 按锁的策略分类** - **乐观锁(Optimistic Locking)** - **原理**:假设冲突概率低,通过版本号或时间戳检测数据变更。 - **实现方式**: - **CAS(Compare and Swap)**:更新时校验版本号是否匹配。 - **示例**: ```sql UPDATE 商品表 SET 库存 = 库存 - 1, 版本号 = 版本号 + 1 WHERE 商品ID = 'P100' AND 版本号 = 当前版本; ``` - **适用场景**:读多写少,冲突较少(如评论点赞)。 - **悲观锁(Pessimistic Locking)** - **原理**:假设冲突概率高,预先加锁阻止其他事务访问。 - **实现方式**:通过 `SELECT ... FOR UPDATE` 或 `LOCK IN SHARE MODE` 显式加锁。 - **适用场景**:写操作频繁,冲突较多(如支付扣款)。 #### **4. 按锁的扩展功能分类** - **意向锁(Intention Locks)** - **作用**:快速判断表中是否存在行级锁,避免逐行检查。 - **类型**: - **意向共享锁(IS锁)**:事务计划在表中某些行加共享锁。 - **意向排他锁(IX锁)**:事务计划在表中某些行加排他锁。 - **示例**: 事务A对某行加排他锁前,会先对表加IX锁,其他事务若想加表级锁,需检查IX锁是否冲突。 - **间隙锁(Gap Lock)** - **作用**:锁定索引记录间的“间隙”,防止其他事务插入数据(解决幻读)。 - **场景**:可重复读隔离级别下的范围查询。 - **示例**: ```sql SELECT * FROM 订单表 WHERE 金额 BETWEEN 100 AND 200 FOR UPDATE; -- 锁住金额在100~200之间的所有现有记录及间隙 ``` --- ### **锁机制的核心价值** - **平衡性能与一致性**:通过不同粒度的锁,在并发效率和数据安全间取得平衡。 - **支持复杂事务**:确保事务的原子性、隔离性和持久性(ACID特性)。 **实际案例**: 银行转账场景中,事务A和事务B同时操作账户A和账户B: - 若未加锁,可能导致余额计算错误(如两者同时读取旧值后更新)。 - 通过行级排他锁,强制事务串行执行,保证最终结果正确。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值