Android-Daily-Interview icon indicating copy to clipboard operation
Android-Daily-Interview copied to clipboard

2019-08-30:什么是悲观锁和乐观锁?

Open Moosphan opened this issue 4 years ago • 8 comments

Moosphan avatar Aug 30 '19 01:08 Moosphan

锁是为了避免自己在修改资源的时候,别人同时在修改,导致同一时间产生两份修改,不知道如何处理的情况而设置的独占资源,避免多个操作同时处理同一资源的技术。

乐观锁:默认为,某个线程在自己处理共享资源的时候,不会出现同一时刻来修改此资源的前提,只在处理完毕,最后写入内存的时候,检测是否此资源在之前未被修改。类似于读写锁的读锁就是乐观锁。

悲观锁:默认为,某个线程在自己处理共享资源的时候,一定会出现同一时刻来修改此资源,所以刚拿到这个资源就直接加锁,不让其他线程来操作,加锁在逻辑处理之前。类似,synchronized关键字,条件锁,数据库的行锁,表锁等就是悲观锁。

fogcoding avatar Aug 30 '19 04:08 fogcoding

这次试试用故事解释一下: 在很久很久以前,某一台机器里有成千上万个线程,他们勤劳的工作着,处理着那些复杂的任务。 某一天,一个线程2333要将一个变量a的值加一,2333读到了a的值为1,现在a的值应该为2了,2333将a的值写为2,然后就离开了,可是他不知道,在他计算的时候,其他线程也动了a的值,执行的也是a++,现在a的值发生错乱了! 后来,程序员们设计了一种锁机制,当有线程需要操作共享变量时,必须申请一把锁,只有申请到的线程才能去操作,其他线程则等待锁。 又一天,还是线程2333,还是操作a的值,不同的是需要申请锁,线程2333发现这把锁被别的线程占用了,于是等,可等了很久还是没拿到锁,2333等到了天荒地老。 程序员们见这样可不行,于是实现了一种新的方式:当线程需要当前值去操作该值时(比如a++),先得到a的值,计算出a+1的值,然后将之前得到的值和当前a的值比较,如果相等就更改值,否则重新来过(注:这一步应该是原子操作,比如CAS,否则还是会乱)

总结:

  • 第一个办法就是悲观锁,悲观地认为一定会发生并发修改,所以用一把锁阻止其他线程同时去操作
  • 第二个办法就是乐观锁,认为发生并发修改的几率很小,所以只在最后去判断是否可以修改,不然就重新来过(其实还有ABA问题什么的)

推荐文章: 悲观锁和乐观锁 编程世界的那把锁 加锁还是不加锁,这是一个问题

canyie avatar Aug 30 '19 04:08 canyie

乐观锁:认为并发修改的几率很小,在将修改的数据写进内存的时候判断当前资源之前是否被修改。 悲观锁:认为,在进行数据修改时,会出现并发修改,所以在这个线程修改数据的时候加上一把锁,防止其他线程在同一时间进行数据的修改。

suagger avatar Nov 14 '19 11:11 suagger

悲观锁:在每次线程对共享数据资源进行处理时,总是认为会出现并发修改,即有其他线程也在对此共享资源数据进行修改。每次在进行共享数据修改时要先获得相应的锁,才能对其进行修改。使用此种锁机制的时候,当一个线程在对共享数据行进修改时,其他线程都进入等待,只有通知当前运行完毕,其他线程需要通过不断地争夺执行权,等待,直到获取到执行权为止。

乐观锁:总是认为在进行共享资源数据改动时,不会出现有其他线程同时修改的情况,但是当存入内存,进行保存的时候,需要判断是否再保存数据之前对数据进行了修改。即当不同线程对数据进行修改时,可同时进行修改,但是保存时要改动相应的版本号,以证明此数据已被修改,如果在修改之前和修改之后的版本号不同时,则需要重新对数据进行修改,直到保存成功

syx103 avatar Nov 14 '19 13:11 syx103

锁:为了避免多个线程在同一时间修改同一公共数据时发生混乱 悲观锁:总认为一定会有线程来修改数据,于是在修改数据之前就给线数据加上一把锁,在线程修改该数据之前先要获取到该锁,才可以修改(导致别的线程在要修改共享数据时等待时间过长) 乐观锁:总认为不会有线程来修改数据,于是将要修改的数据准备好,在修改之前判断该数据是否被修改过,如果被修改过了,就重新获取被修改过数据,再准备数据,在修改之前再去检查数据有没有再次被更改过,如果没有,就去修改数据。

chunlinchulv avatar Nov 14 '19 14:11 chunlinchulv

悲观锁 每次取的时候都会加锁 乐观锁 每次取得时候都不加锁,只有修改的时候判断是否被改变,如果被改变则等待重试

manondidi avatar Jan 19 '20 06:01 manondidi

简单点来说 synchronized相当于就是悲观锁, 也就是你获取到锁的时候别人都不能操作,你释放锁之后别人才可以操作. Atomic之类的原子操作类就是乐观锁, 也就是CAS, 效率相比悲观锁要高很多

zhengjiong avatar Apr 13 '20 08:04 zhengjiong

#基本概念 乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,然后进行重试,否则执行操作。

悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

#实现方式 悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

乐观锁的实现方式主要有两种:CAS机制和版本号机制

#优缺点和适用场景 功能限制 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

2、竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

#乐观锁加锁吗?

(1)乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。

(2)有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。

#CAS有哪些缺点 1、ABA问题 假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

(1)线程1读取内存中数据为A;

(2)线程2将该数据修改为B;

(3)线程2将该数据修改为A;

(4)线程1对数据进行CAS操作

在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

在AtomicInteger的例子中,ABA似乎没有什么危害。但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。

对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。

2、高竞争下的开销问题 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。

3、功能限制 CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;(2)当涉及到多个变量(内存值)时,CAS也无能为力。

除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

senlinxuefeng avatar Jan 07 '22 07:01 senlinxuefeng