Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Moosphan opened this issue Aug 30, 2019 · 8 comments
Open

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

Moosphan opened this issue Aug 30, 2019 · 8 comments
Labels

Comments

@Moosphan
Copy link
Owner

No description provided.

@Moosphan Moosphan added the Java label Aug 30, 2019
@fogcoding
Copy link

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

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

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

@canyie
Copy link

canyie commented Aug 30, 2019

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

总结:

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

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

@suagger
Copy link

suagger commented Nov 14, 2019

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

@syx103
Copy link

syx103 commented Nov 14, 2019

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

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

@chunlinchulv
Copy link

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

@manondidi
Copy link

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

@zhengjiong
Copy link

zhengjiong commented Apr 13, 2020

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

@senlinxuefeng
Copy link

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

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

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

#实现方式
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如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包下的原子类使用,灵活性受到限制。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants