本篇文章将介绍常见的锁策略以及CAS中的ABA问题,前面介绍使用synchronized关键字来保证线程的安全性,本质上就是对对象进行加锁操作,synchronized所加的锁到底是什么类型的锁呢?本文带你一探究竟。

🍀1.常见的锁策略

🍂1.1乐观锁与悲观锁

乐观锁与悲观锁是从处理锁冲突的态度方面来进行考量分类的。

  • 乐观锁预期锁冲突的概率很低,所以做的准备工作更少,付出更少,效率较高。
  • 悲观锁预期锁冲突的概率很高,所以做的准备工作更多,付出更多,效率较低。

🍂1.2读写锁与普通互斥锁

对于普通的互斥锁只有两个操作:

  • 加锁
  • 解锁

而对于读写锁来说有三个操作:

  • 加读锁,如果代码仅进行读操作,就加读锁。
  • 加写锁,如果代码含有写操作,就加写锁。
  • 解锁。

针对读锁与读锁之间,是没有互斥关系的,因为多线程中同时读一个变量是线程安全的,针对读锁与写锁之间以及写锁与写锁之间,是存在互斥关系的。

在java中有读写锁的标准类,位于java.util.concurrent.locks.ReentrantReadWriteLock,其中ReentrantReadWriteLock.ReadLock为读锁,ReentrantReadWriteLock.WriteLock为写锁。

🍂1.3重量级锁与轻量级锁

这两种类型的锁与悲观锁乐观锁有一定的重叠,重量级锁做的事情更多,开销更大,轻量级锁做的事情较少,开销也就较少,在大部分情况下,可以将重量级锁视为悲观锁,轻量级锁视为乐观锁。

如果锁的底层是基于内核态实现的(比如调用了操作系统提供的mutex接口)此时一般认为是重量级锁,如果是纯用户态实现的,一般认为是轻量级锁。

🍂1.4挂起等待锁与自旋锁

挂起等待锁表示当获取锁失败之后,对应的线程就要在内核中挂起等待(放弃CPU,进入等待队列),需要在锁被释放之后由操作系统唤醒,该类型的锁是重量级锁的典型实现。 自旋锁表示在获取锁失败后,不会立刻放弃CPU,而是快速频繁的再次询问锁的持有状态一旦锁被释放了,就能立刻获取到锁,该类型的锁是轻量级锁的典型实现。

🍄挂起等待锁与自旋锁的区别

  • 最明显的区别就是,挂起等待锁开销比自旋锁要大,且挂起等待锁效率不如自旋锁。
  • 挂起等待锁会放弃CPU资源,自旋锁不会放弃CPU资源,会一直等到锁释放为止。
  • 自旋锁相较于挂起等待锁更能及时获取到刚释放的锁。
  • 自旋锁相较于挂起等待锁的劣势就是当自旋的时间长了,会持续地销耗CPU资源,因此自旋锁也可以说是乐观锁。

🍂1.5公平锁与非公平锁

公平锁遵循先来后到的原则,多个线程在等待一把锁的时候,谁先来尝试拿锁,那这把锁就是谁的。 非公平锁遵循随机的原则,多个线程正在等待一把锁时,当锁释放时,每个线程获取到这把锁的概率是均等的。

🍂1.6可重入锁与不可重入锁

一个线程连续加锁两次,不会造成死锁,那么这个锁就是可重入锁。 反之,一个线程连续加锁两次,会造成死锁现象,那么这个锁就是不可重入锁。

关于死锁是什么,稍等片刻,后面就会介绍到。

🍄综合上述的几种锁策略,synchronized加的所到底是什么锁?

  • 它既是乐观锁也是悲观锁,当锁竞争较小时它就是乐观锁,锁竞争较大时它就是悲观锁。
  • 它是普通互斥锁。
  • 它既是轻量级锁也是重量级锁,根据锁竞争激烈程度自适应。
  • 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。
  • 它是非公平锁。
  • 它是可重入锁。

🍂1.7死锁问题

🍁1.7.1常见死锁的情况

死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

🍄情况1:一个线程一把锁 比如下面这种情况

加锁 方法 () {
	加锁 (this) {
		//代码块
	}
}

首先,首次加锁,可以成功,因为当前对象并没有被加锁,然后进去方法里面,再次进行加锁,此时由于当前对象已经被锁占用,所以会加锁失败然后尝试再次加锁,此时就会陷入一个加锁的死循环当中,造成死锁。

🍄情况2:两个线程两把锁 不妨将两个线程称为A,B,两把锁称为S1,S2,当线程A已经占用了锁S1,线程B已经占用了锁S2,当线程A运行到加锁S2时,由于锁S2被线程B占用,线程A会陷入阻塞状态,当线程B运行到加锁S1时,由于锁S1被线程A占用,会导致线程B陷入阻塞状态,两个线程都陷入了阻塞状态,而且自然条件下无法被唤醒,造成了死锁。

🍄情况3:多个线程多把锁 最典型的栗子就是哲学家就餐问题,下面我们来分析哲学家就餐问题。

🍁1.7.2哲学家就餐问题

哲学家就餐问题是迪杰斯特拉这位大佬提出并解决的问题,具体问题如下:

有五位非常固执的科学家每天围在一张圆桌上面吃饭,这个圆桌上一共有5份食物和5 筷子,哲学家们成天都坐在桌前思考,当饿了的时候就会拿起距离自己最近的2根筷子就餐,但是如果发现离得最近的筷子被其他哲学家占用了,这个哲学家就会一直等,直到旁边的哲学家就餐完毕,这位科学家才会拿起左右的筷子进行就餐,就餐完毕后哲学家们又开始进行思考状态,饿了就再次就餐。

哲学家就餐问题

当哲学家们每个人都拿起了左边的筷子或者右边的筷子,由于哲学家们非常地顽固,拿起一只筷子后发现另一只筷子被占用就会一直等待,所以所有的哲学家都会互相地等待,这样就会造成所有哲学家都在等待,即死锁。

哲学家环路等待

🍄从上述的几种造成死锁的情况,可以总结发生死锁的条件:

  • 互斥使用,一个锁被一个线程占用后,其他线程使用不了(锁本质,保证原子性)。
  • 不可抢占,一个锁被一个线程占用后,其他线程不能将锁抢占。
  • 请求和保持,当一个线程占据多把锁后,除非显式释放锁,否则锁一直被该线程锁占用。
  • 环路等待,多个线程等待关系闭环了,比如A等B,B等C,C等A。

🍄如何避免环路等待? 只需约定好,线程针对多把锁加锁时有固定的顺序即可,当所有的线程都按照约定的顺序加锁就不会出现环路等待。

比如对于上述的哲学家就餐问题,我们可以对筷子进行编号,每次哲学家优先拿编号小的筷子就可以避免死锁。

哲学家问题解决

🍀2.CAS指令与ABA问题

🍂2.1CAS指令

CAS即compare and awap,即比较加交换,具体说就是将寄存器或者某个内存上的值v1与另一个内存上的值v2进行比较,如果相同就将v1与需要修改的值swapV进行交换,并返回交换是否成功的结果。

伪代码如下:

boolean CAS(v1, v2, swapV) {
	if (v1 == v2) {
		v1=swapV;
		return true;
	}
	return false;
}

上面的这一段伪代码很明显就是线程不安全的,CPU中提供了一条指令能够一步到位实现上述伪代码的功能,即CAS指令。该指令是具有原子性的,是线程安全的。

java标准库中提供了基于CAS所实现的“原子类”,这些类的类名以Atomic开头,针对常用的int,long等进行了封装,它们可以基于CAS的方式进行修改,是线程安全的。

CAS标准类

就比如上次使用多个线程对同一个变量进行自增操作的那个程序,它是线程不安全的,但是如果使用CAS原子类来实现,那就是线程安全的。

其中的getAndIncrement方法相当于i 操作。 现在我们来使用原子类中的“getAndIncrement方法(基于CAS实现)来实现该程序。

import java.util.concurrent.atomic.AtomicInteger;
public class Main {
    private static final int CNT = 50000;
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < CNT; i  ) {
                count.getAndIncrement();
            }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < CNT; i  ) {
                count.getAndIncrement();
            }
        });
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

🍄运行结果:

原子类

从结果我们也能看出来,该程序是线程安全的。

上面所使用的AtomicInteger类方法getAndIncrement实现的伪代码如下:

class AtomicInteger {
    private int value;//保存的值
    //自增操作
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue 1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

首先,对于CAS指令,它的执行逻辑就是先判断value的值是否与oldValue的值相同,如果相同就将原来value的值与value 1的值进行交换,相当于将value的值加1,其中oldValue的值为提前获取的value值,在单线程中oldValue的值一定与value的值相同,但是多线程就不一定了,因为每时每刻都有可能被其他线程修改。

然后,我们再来看看下面的while循环,该循环使用CAS指令是否成功为判断条件,如果CAS成功了则退出循环,此时value的值已经加1,最终返回oldValue,因为后置 先使用后
如果CAS指令失败了,这就说明有新线程提前对当前的value进行了 value的值发生了改变,这时候需要重新保存value的值给oldValue,然后尝试重新进行CAS操作,这样就能保证有几个线程操作,那就自增几次,从而也就保证了线程安全,总的来说相当于传统的 操作,基于CAS的自增操作只有两个指令,一个是将目标值加载到寄存器,然后在寄存器上进行CAS操作,前面使用传统 操作导致出现线程安全问题是指令交错的情况,现在我们来画一条时间轴,描述CAS实现的自增操作在多个线程指令交错时的运行情况。

CAS时间图

发现尽管指令交错了,但是运行得到的结果预期也是相同的,也就说明基于CAS指令实现的多线程自增操作是线程安全的。

此外,基于CAS也能够实现自旋锁,伪代码如下:

//这是一个自旋锁对象,里面有一个线程引用,如果该引用不为null,说明当前锁对象被线程占用,反之亦然。
public class SpinLock {
    private Thread owner;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

根据CAS与自旋锁的逻辑,如果当前锁对象被线程占用,则lock方法会反复地取获取该锁是否释放,如果释放了即owner==null,就会利用CAS操作将占用该锁对象的线程设置为当前线程,并退出加锁lock方法。

解锁方法非常简单,就将占用锁对象的线程置为null即可。

🍂2.2ABA问题

根据上面的介绍我们知道CAS指令操作的本质是先比较,满足条件后再进行交换,在大部分情况下都能保证线程安全,但是有一种非常极端的情况,那就是一个值被修改后又被改回到原来的值,此时CAS操作也能成功执行,这种情况在大多数的情况是没有影响的,但是也存在问题。

像上述一个值被修改后又被改回来这种情况就是CAS中的ABA问题,虽说对于大部分场景都不会有问题,但是也存在bug,比如有以下一个场景就说明了ABA问题所产生的bug:

有一天。滑稽老铁到ATM机去取款,使用ATM查询之后,滑稽老铁发现它银行卡的余额还有200,于是滑稽老铁想去100块给女朋友买小礼物,但是滑稽老铁取款时,在点击取款按钮后机器卡了一下,滑稽老铁下意识又点了一下,假设这两部取款操作执行图如下:

滑稽老铁取钱

如果没有出现意外,即使按下两次取款按钮也是正常的,但是在这两次CAS操作之间,如图滑稽老铁的朋友给它转账了100块,导致第一次CAS扣款100后的余额从100变回到了200,这时第二次CAS操作也会执行成功,导致又被扣款100块,最终余额是100块,这种情况是不合理的,滑稽老铁会组织滑稽大军讨伐银行的,合理的情况应该是第二次CAS仍然失败,最终余额为200元。

滑稽老铁被多扣钱

为了解决ABA问题造成的bug,可以引入应该版本号,版本号只能增加不能减少,加载数据的时候,版本号也要一并加载,每一次修改余额都要将版本号加1, 在进行CAS操作之前,都要对版本号进行验证,如果版本号与之前加载的版本号不同,则放弃此次CAS指令操作。

滑稽老铁的救赎

上面的这张图是引入版本号之后,滑稽老铁账户余额变化图,我们不难发现余额的变化是合理的。

总结一下,本篇文章介绍了常见的锁策略,并说明了synchronized关键字加的锁类型不是单一一种锁类型的,根据可重入锁与非可重入锁引出了死锁的概念与死锁条件,最后介绍了CAS指令以及CAS锁产生的ABA问题及其解决方案。

到此这篇关于Java多线程之常见锁策略与CAS中的ABA问题的文章就介绍到这了,更多相关Java多线程常见锁策略内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

解析Java多线程之常见锁策略与CAS中的ABA问题的更多相关文章

  1. iOS:核心图像和多线程应用程序

    我试图以最有效的方式运行一些核心图像过滤器.试图避免内存警告和崩溃,这是我在渲染大图像时得到的.我正在看Apple的核心图像编程指南.关于多线程,它说:“每个线程必须创建自己的CIFilter对象.否则,你的应用程序可能会出现意外行为.”这是什么意思?我实际上是试图在后台线程上运行我的过滤器,所以我可以在主线程上运行HUD(见下文).这在coreImage的上下文中是否有意义?

  2. ios – 意外的核心数据多线程违规

    我正在使用苹果的并发核心数据调试器.-com.apple.CoreData.ConcurrencyDebug1有时候我得到__Multithreading_Violation_AllThatIsLeftToUsIsHonor__,即使我几乎肯定线程没有被违反.这是发生异常的代码的一部分(代码是扩展NSManagedobject的协议的一部分):代码在上下文的执行:块中执行.这里是线程信息:和调试器

  3. ios – UIGraphicsBeginImageContextWithOptions和多线程

    我对UIGraphicsBeginImageContextWithOptions和线程有点困惑,因为根据UIKitFunctionReferenceUIGraphicsBeginImageContextWithOptions应该只在主线程上调用.当被调用时,它创建一个基于位图的上下文,可以使用CoreGraphics的函数或者像-drawInRect这样的方法来处理:对于UIImage,-draw

  4. Swift之dispatch_source实现多线程定时关闭功能

    由于在项目中需要用到定时关闭音频功能,本来打算用NSTimer的,可是写起来并不是那么精简好用,所以又在网上找到相关的实例,结合自己项目需要,就写出了如下代码,还请大家指教,废话不多说:

  5. swift 多线程实现

  6. swift_多线程基础_最简单用法GCD, NSOperationQueue, NSThread

    ////ViewController.swift//study1-1//Createdbyadminon15/12/28.//copyright2015年admin.Allrightsreserved.//importUIKitclassViewController:UIViewController{@IBOutletvarmyLable:UILabel?@IBActionfuncclickBut

  7. swift__多线程GCD详解

    有以下*-disPATCH_QUEUE_PRIORITY_HIGH:*-disPATCH_QUEUE_PRIORITY_DEFAULT:多用默认*-disPATCH_QUEUE_PRIORITY_LOW:*-disPATCH_QUEUE_PRIORITY_BACKGROUND:*第二个参数为预留参数,一般为0*/letmyQueue:dispatch_queue_t=dispatch_get_global_queue//用异步的方式运行队列里的任务dispatch_async//-------------

  8. Swift - 多线程实现方式3 - Grand Central DispatchGCD

    dispatchqueue可以是并发的或串行的。dispatch_suspend后,追加到DispatchQueue中尚未执行的任务在此之后停止执行。6//创建并行队列conQueue:dispatch_queue_t=dispatch_queue_create//暂停一个队列dispatch_suspend//继续队列dispatch_resume6,dispatch_once一次执行保证dispatch_once中的代码块在应用程序里面只执行一次,无论是不是多线程。注意,我们不能(直接)取消我们已经提

  9. 【Swift】三种多线程处理方式

    )Threadbtn.frame=CGRectMakeThreadbtn.setTitle//普通状态下的文字Threadbtn.setTitle//触摸状态下的文字letmethod:Selector=methodarr[index!]Threadbtn.addTargetself.view.addSubview;}}overridefuncdidReceiveMemoryWarning(){super.didReceiveMemoryWarning()}//1.NSThread线程functestNS

  10. Swift多线程之GCD

    学自:http://www.jianshu.com/p/2598a4e9c139

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部