悲观锁乐观锁&可重入锁和不可重入锁&读写锁&自旋锁------概念区别

为保证并发情况下线程对临界资源的合理安区的访问,除了互斥锁还有很多其他的锁,这里简单了解下下概念:

1.悲观锁(PCC)

之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起,直到当前线程访问完数据解锁。互斥锁就是一种悲观锁。
悲观锁采用“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是效率低,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个进程如果锁定了某行数据,其他进程就必须等待该进程处理完才可以处理那行数据。
适用于对数据一致性要求高,写多读少,写冲突严重的情况,因为悲观锁是在读取数据的时候就加锁的,读多的场景会需要频繁的加锁和很多的的等待时间,而在写冲突严重的情况下使用悲观锁可以保证数据的一致性。

2.乐观锁(CAS锁)

乐观锁是相对悲观锁而言的,线程每次访问数据都乐观的认为不会有别的进程同时访问修改。乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观并发控制没有实际加锁,所以没有额外开销,也不错出现死锁问题,适用于读多写少的并发场景,因为没有额外开销,所以能极大提高数据库的性能。

锁机制及CAS实现原理

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS是一种非阻塞式的同步方式。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
乐观锁是一种思想。CAS是这种思想的一种实现方式。

以下内容和代码参考:CAS与ABA问题

CAS

一般采用原子级的read-modify-write原语来实现Lock-Free算法,其中LL和SC是Lock-Free理论研究领域的理想原语,但实现这些原语需要CPU指令的支持,非常遗憾的是目前没有任何CPU直接实现了SC原语。根据此理论,业界在原子操作的基础上提出了著名的CAS(Compare-And-Swap)操作来实现Lock-Free算法,Intel实现了一条类似该操作的指令:cmpxchg8。

CAS原语负责将某处内存地址的值(1个字节)与一个期望值进行比较,如果相等,则将该内存地址处的值替换为新值,CAS 操作伪码描述如下:


Bool CAS(T* addr, T expected, T newValue)
{
         if(*addr == expected )
         {
                   *addr=  newValue;
                   returntrue;
         }
        else
                   returnfalse;
}

//CAS实际操作
do
{
         备份旧数据;
         基于旧数据构造新数据;
}while(!CAS(内存地址,备份的旧数据,新数据))

就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出CAS操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

CAS的Linux解法
cmpxchg先比较内存地址的值是否与传入的值相等,如果相等则执行xchg逻辑。

inline int CAS(unsigned long* mem, unsignedlong newval, unsigned long oldval)

{

         __typeof(*mem) ret;
         //这里测试的使用64位系统,如果是32位,这里使用cmpschgl
         __asm__volatile ("lock; cmpxchgq %2,%1"
                                                        :"=a"(ret), "=m"(*mem)
                                                        :"r"(newval), "m"(*mem), "0"(oldval));
         returnret==oldval;
}

CAS举例(简单应用AtomicInc)


#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/time.h>
#include <stdint.h>
 
int count = 0;
 
inline int CAS(unsigned long* mem, unsigned long oldval, unsigned long newval)
{
	__typeof (*mem) ret;
	// 这里测试的使用64位系统,如果是32位,这里使用cmpschgl
	__asm __volatile ("lock; cmpxchgq %2,%1"
						: "=a"(ret), "=m"(*mem)
						: "r"(newval), "m"(*mem), "0"(oldval));
	return ret==oldval;
}
 
void AtomicInc(int* addr)
{
	int oldval;
	int newval;
	do
	{
		oldval = *addr;
		newval = oldval+1;
	} while(!CAS((unsigned long*)addr, oldval, newval));
}
 
void *test_func(void *arg)
{
	int i=0;
	int confict = 0;
	for(i=0;i<2000000;++i)
	{
		AtomicInc(&count);
	}
	return NULL;
}
 
int main(int argc, const char *argv[])
{
	pthread_t id[20];
	int i = 0;
 
	uint64_t usetime;
	struct timeval start;
	struct timeval end;
	
	gettimeofday(&start,NULL);
	
	for(i=0;i<20;++i)
	{
		pthread_create(&id[i],NULL,test_func,NULL);
	}
 
	for(i=0;i<20;++i)
	{
		pthread_join(id[i],NULL);
	}
	
	gettimeofday(&end,NULL);
 
	usetime = (end.tv_sec-start.tv_sec)*1000000+(end.tv_usec-start.tv_usec);
	printf("count = %d, usetime = %lu usecs\n", count, usetime);
	return 0;
}

CAS举例(复杂应用)

struct Node
{
	Node* next;
	int data;
}
Node* head = NULL;
 
void push(int t)
{
	Node* node = new Node(t);
	do
	{
		node->next = head;
	} while (!CAS(&head, node->next, node));
}
 
bool pop(int&t )
{
	Node* current = head;
	while(current)
	{
		if (CAS(&head, current, current->next)) // ABA问题
		{
			t = current->data;
			return true;
		}
		current = head;
	}
	return false;
}

ABA问题
一般的CAS在决定是否要修改某个变量时,会判断一下当前值跟旧值是否相等。如果相等,则认为变量未被其他线程修改,可以改。
但是,“相等”并不真的意味着“未被修改”。另一个线程可能会把变量的值从A改成B,又从B改回成A。这就是ABA问题。
很多情况下,ABA问题不会影响你的业务逻辑因此可以忽略。但有时不能忽略,这时要解决这个问题,一般的做法是给变量关联一个只能递增、不能递减的版本号。在compare时不但compare变量值,还要再compare一下版本号。
Java里的AtomicStampedReference类就是干这个的。

3.可重入锁和不可重入锁

可重入锁:同一个线程对同一个锁可以重复加锁(有计数),然后重复解锁。可重入锁的作用就是为了避免死锁
不可重入锁:同一个线程对同一个锁只能加一次锁,互斥锁即为不可重入锁。

可重入锁的实现原理:
通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,会将该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁。

4.读写锁

读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。读共享,写互斥。适用于读多写少的并发情况。

读锁又称为共享锁(shared locks),简称S锁。读锁(共享锁)就是多个线程对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
写锁又称为排他锁(exclusive locks),简称X锁。写锁(排他锁)就是不能与其他锁并存,一个线程对一个数据有一个独立的锁,保证只有当前进程对数据的读写操作。

5.自旋锁

占据CPU不释放(非挂起等待),循环对枷锁条件进行判断,实时性更高。

参考文章

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值