Java多线程-常见锁的概述

悲观锁和乐观锁

简介

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

悲观锁,在操作数据时比较悲观,认为别人会同时修改数据。所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

乐观锁,就是思想很乐观,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。注意:Innodb默认使用的是行锁。而行锁是基于索引的,因此要想加上行锁,在加锁时必须命中索引,否则将使用表锁。

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

注意:乐观锁事实上并没有使用锁机制,只是在更新时判断一下数据是否被其他线程更新了;

乐观锁和悲观锁的优缺点和适用场景

  • 功能限制

CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。

再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

  • 竞争激烈程度

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

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

当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

悲观锁的缺点:
悲观锁实际上是“先取锁再访问”的保守策略,虽然为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

总结

读多写少的场景优先用乐观锁,写多读少的场景优先用悲观锁.

公平锁和非公平锁

定义:

  • 公平锁:是指多个线程按照锁申请的顺序来获取锁,类似排队打饭,先来后到.
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁.在高并发场景下,有可能会造成优先级反转或者线程饥饿现象.

两者区别

  • 公平锁:在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否者就会加入到等待队列中,以后会按照 FIFO 的规则获取锁
  • 非公平锁:一上来就尝试占有锁,如果失败再进行排队

ReentrantLock通过构造参数指定该锁是否是公平锁,默认是非公平锁.Synchronized是非公平锁.

在没有公平性的需求的前提下尽量使用非公平锁,因为非公平锁的吞吐量比公平锁高.

可重入锁和不可重入锁

定义

可重入锁也叫作递归锁.最大的作用是防止死锁.

定义:

  • 可重入锁:指的是同一个线程外层函数获得锁之后,内层仍然能获取到该锁,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁.即:线程可以进入任何一个它已经拥有的锁所同步着的代码块.
  • 不可重入锁: 所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞

原理

synchronized内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。

但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1。当计数器值为0 时,锁里面的线程标示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁。

synchronized 和 ReentrantLock 都是可重入锁.

自旋锁

由于Java中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程 。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。

自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是 10,可以使用-XX :PreBlockSpinsh 参数设置该值),很有可能在后面几次尝试中其他线程己经释
放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。

由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了 。

指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU.

独占锁和共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。

独占锁保证任何时候都只有一个线程能得到锁,比如ReentrantLock。

共享锁则可以同时被多个线程持有,比如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。

独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

发布了24 篇原创文章 · 获赞 8 · 访问量 926

猜你喜欢

转载自blog.csdn.net/kaihuishang666/article/details/103920902