多线程常见锁策略—乐观悲观、自旋、读写、ABA问题及解决
发布日期:2021-06-29 04:42:05 浏览次数:2 分类:技术文章

本文共 8724 字,大约阅读时间需要 29 分钟。

目录

一、常见锁策略

1.乐观锁

定义

乐观锁认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正

式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

执行流程

在这里插入图片描述

实现

Atomic*家族

AtomicInteger count = new AtomicInteger(0);//int count = 0;count.getAndIncrement();//i++count.incrementAndGet();//++iSystem.out.println(count.getAndIncrement());
//AtomicInteger实现线程安全private static AtomicInteger count = new AtomicInteger(0);    private static final int MAXSIZE =100000;    public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override public void run() {
for (int i = 0; i

问题

并不总是能处理所有问题,所以会引入一定的系统复杂度

问题2

Integer高速缓存问题 (-128~127)超出范围的值会重新new对象,造成结果与预期不相符。

解决方案:设置应用程序的参数(-D),设置Integer高速缓存最大值。

2.悲观锁

定义

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,会出现并发冲突,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

例如:synchronized

问题

总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。

3.可重入锁

定义

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

/**Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。*/private static Object lock = new Object();    public static void main(String[] args) {
synchronized (lock){
System.out.println("第一次"); synchronized (lock){
System.out.println("第二次"); } } }

4.读写锁

定义

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而生。

读写锁(readers-writer lock),将一个锁分成两个,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
读锁和写锁互斥:防止读脏数据。
例如:ReentrantReadWriteLock

优点

粒度小,性能高

public static void main(String[] args) throws InterruptedException{
//创建读写锁 ReentrantReadWriteLock readwriteLock = new ReentrantReadWriteLock(true);//公平性 //读锁 ReentrantReadWriteLock.ReadLock readLock = readwriteLock.readLock(); //写锁 ReentrantReadWriteLock.WriteLock writeLock = readwriteLock.writeLock(); //线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(1000)); //任务1:执行读锁 executor.execute(new Runnable() {
@Override public void run() {
readLock.lock(); try {
//业务处理逻辑 System.out.println(Thread.currentThread().getName()+ "执行读操作:"+new Date()); Thread.sleep(3000); }catch (InterruptedException e){
e.printStackTrace(); }finally {
//释放锁 readLock.unlock(); } } }); //任务2:执行读锁 executor.execute(new Runnable() {
@Override public void run() {
readLock.lock(); try {
//业务处理逻辑 System.out.println(Thread.currentThread().getName()+ "执行读操作:"+new Date()); Thread.sleep(3000); }catch (InterruptedException e){
e.printStackTrace(); }finally {
//释放锁 readLock.unlock(); } } }); //任务3:执行写锁 executor.execute(new Runnable() {
@Override public void run() {
writeLock.lock(); try {
//业务处理逻辑 System.out.println(Thread.currentThread().getName()+ "执行写操作:"+new Date()); Thread.sleep(3000); }catch (InterruptedException e){
e.printStackTrace(); }finally {
//释放锁 writeLock.unlock(); } } }); }

在这里插入图片描述

5.共享锁

定义

一把锁可以被多个线程拥有,这就叫共享锁

例如:读写锁的读锁、
非共享锁:synchronized

6.自旋锁

定义

通过死循环一直尝试获取锁

问题

如果发生死锁则会一直自旋,所以会带来一定的额外开销

6.公平锁

定义

锁的获取顺序必须和线程方的获取顺序保持一致,就叫公平锁。执行时有序,结果可预测

new ReentrantLock(true)
非公平锁:默认锁策略,性能更高
new ReentrantLock()/ new ReentrantLock(false)/synchronized

7.常见面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
    1.乐观锁->CAS->Atomic*,CAS是由V、A、B组成,然后执行的时候使用V==A对比,结果为true表明没有冲突,可以直接修改否则不可以修改。CAS是通过调用C++实现的UnSafe中的本地方法(ComparaAndSwap)来实现,C++是通过调用操作系统Atomic::cmpxchg(原子指令)来实现
    2.悲观锁->synchronized在java中是将锁的ID存放到对象
  1. 是否了解什么读写锁么?
    读写锁(readers-writer lock),将一个锁分成两个,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
    读锁和写锁互斥:防止读脏数据。
    例如:ReentrantReadWriteLock
  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
    通过死循环一直尝试获取锁,如果发生死锁则会一直自旋,所以会带来一定的额外开销
  1. synchronized 是可重入锁么?

二、CAS

定义

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比 较)
  2. 如果比较相等,将 B 写入V。(交换)
  3. 返回操作是否成功。

多线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。 乐观锁。

原理

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

缺点

ABA问题:A:旧值;B:预期新值

银行转账为例

第一次转账(转出100):V(100) A(100) B(0)——>V(100)——>V(100) == A(100)——>true——>V(0)
第二次转账(转入100):V(0) A(0) B(100)——>V(100)——>V(0) == A(0)——>true——>V(100)
第三次转账(转出100):V(100) A(100) B(0)——>V(0)——>V(100) == A(100)——>true——>V(0)
当我第一次转出后,此时银行卡内转入100,加入我误操作点击两次,系统识别不出就会继续进行第二次转账,100元就会消失。

package thread0527;import java.util.concurrent.atomic.AtomicReference;public class ThreadDemo93 {
private static AtomicReference money = new AtomicReference(100);//初始金额100元 public static void main(String[] args) throws InterruptedException {
//转账一:(-100) Thread t1 = new Thread(new Runnable() {
@Override public void run() {
boolean result = money.compareAndSet(100,0);//转账操作 System.out.println("第一次转账(-100)"+result); } }); t1.start(); t1.join(); // Thread t3 = new Thread(new Runnable() {
@Override public void run() {
boolean result = money.compareAndSet(0,100); System.out.println("转入100元"+result); } }); t3.start(); t3.join(); //转账二:(-100) Thread t2 = new Thread(new Runnable() {
@Override public void run() {
boolean result = money.compareAndSet(100,0);//转账操作 System.out.println("第二次转账(-100)"+result); } }); t2.start(); }}

在这里插入图片描述

解决方案

增加版本号,每次修改后更新版本号

package thread0527;import java.util.concurrent.atomic.AtomicReference;import java.util.concurrent.atomic.AtomicStampedReference;public class ThreadDemo94 {
private static AtomicStampedReference money = new AtomicStampedReference(100,1); //private static AtomicReference money = new AtomicReference(100);//初始金额100元 public static void main(String[] args) throws InterruptedException {
//转账一:(-100) Thread t1 = new Thread(new Runnable() {
@Override public void run() {
boolean result = money.compareAndSet(100,0,1,2);//转账操作 System.out.println("第一次转账(-100)"+result); } }); t1.start(); t1.join(); // Thread t3 = new Thread(new Runnable() {
@Override public void run() {
boolean result = money.compareAndSet(0,100,2,3); System.out.println("转入100元"+result); } }); t3.start(); t3.join(); //转账二:(-100) Thread t2 = new Thread(new Runnable() {
@Override public void run() {
boolean result = money.compareAndSet(100,0,1,2);//转账操作 System.out.println("第二次转账(-100)"+result); } }); t2.start(); }}

程序执行结果

在这里插入图片描述
面试问题:CAS底层实现原理。

java层面CAS的实现的UNSafe类,UnSafe类调用C++的本地方法,通过调用操作系统的Atomic::cmpxchg(原子指令)来实现CAS操作。

在这里插入图片描述

三、synchronized 背后的原理

面试题:

  1. 什么是偏向锁?
    对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,
    降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁
    的,只有当其他线程尝试竞争偏向锁才会被释放。
  1. java 的 synchronized 是怎么实现的,有了解过么?
    无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
    偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
    偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态
    轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
    当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
    重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
    重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的
    Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

3.synchronized锁优化(重点)

JDK1.6锁升级的过程:无锁->偏向锁(第一个线程第一次访问,将线程ID存储在对象投中的偏行锁标识)->轻量级锁(自旋)->重量级锁

转载地址:https://blog.csdn.net/ZhangHahaaha/article/details/117335588 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:UITableView优化技巧
下一篇:(导航页博客)TCP/IP协议层层剖析,对整个TCP/IP协议有系统的理解

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月19日 14时34分50秒