0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

多线程常见锁策略+CAS介绍

Android编程精选 来源:CSDN 2023-03-14 16:30 次阅读

一、乐观锁 & 悲观锁

1.1 乐观锁的定义

乐观锁,顾名思义,他比较乐观,他认为一般情况下不会出现冲突,所以只会在更新数据的时候才会对冲突进行检测。如果没有发生冲突直接进行修改,如果发生了冲突则不进行任何修改,然后把结果返回给用户,让用户自行处理。

1.1.1乐观锁的实现-CAS

乐观锁的实现并不是给数据加锁 ,而是通过CAS(Compare And Swap)比较并替换,来实现乐观锁的效果。

CAS比较并替换的流程是这样子的:CAS中包含了三个操作,单位:V(内存值)、A(预期的旧址)、B(新值),比较V值和A值是否相等,,如果相等的话则将V的值更换成B,否则就提示用户修改失败,从而实现了CAS机制。

这只是定义的流程,但是在实际执行过程中,并不会当V值和A值不相等时,就立即把结果返回给用户,而是将A(预期的旧值)改为内存中最新的值,然后再进行比较,直到V值也A值相等,修改内存中的值为B结束。

可能你还是觉得有些晦涩,那我们举个栗子:

41ee6676-bf62-11ed-bfe3-dac502259ad0.png

看完这个图相信你一定能理解了CAS的执行流程了。

1.1.2 CAS的应用

CAS的底层实现是靠Unsafe类实现的,Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。因此不推荐使用Unsafe类,如果用不好会对底层资源造成影响。

为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类,我们来看看AtomicInteger的源码:

41f6b146-bf62-11ed-bfe3-dac502259ad0.png

在getAndIncrement方法中还调用了unsafe的方法,因此这也就是为什么它能够保证原子性的原因。

因此我们可以利用Atomic+包装类实现线程安全的问题。

importjava.util.concurrent.atomic.AtomicInteger;

/**
*使用AtomicInteger保证线程安全问题
*/
publicclassAtomicIntegerDemo{
staticclassCounter{
privatestaticAtomicIntegernum=newAtomicInteger(0);
privateintMAX_COUNT=100000;
publicCounter(intMAX_COUNT){
this.MAX_COUNT=MAX_COUNT;
}
//++方法
publicvoidincrement(){
for(inti=0;i< MAX_COUNT; i++) {
                    num.getAndIncrement();
               }
          }
          //--方法
          public void  decrement(){
               int temp=0;
               for (int i = 0; i < MAX_COUNT; i++) {
                    num.getAndDecrement();
               }
          }
          public  int getNum(){
               return num.get();
          }
     }


     public static void main(String[] args) throws InterruptedException {
          Counter counter=new Counter(100000);
          Thread  thread1=new Thread(()->{
counter.increment();
});
Threadthread2=newThread(()->{
counter.decrement();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终结果:"+counter.getNum());
}
}
4207dbe2-bf62-11ed-bfe3-dac502259ad0.png

1.1.3 CAS存在的问题

循环时间长,开销大

只能保证一个共享变量的原子性操作(可以通过循环CAS的方式实现)

存在ABA问题

1.1.4 ABA问题

什么时ABA问题呢?

比如说两个线程t1和t2,t1的执行时间为10s,t2的执行时间为2s,刚开始都从主内存中获取到A值,t2先开始执行,他执行的比较快,于是他将A的值先改为B,再改为A,这时t1执行,判断内存中的值为A,与自己预期的值一样,以为这个值没有修改过,于是将内存中的值修改为B,但是实际上中间可能已经经历了许多:A->B->A。

所以ABA问题就是,在我们进行CAS中的比较时,预期的值与内存中的值一样,并不能说明这个值没有被改过,而是可能已经被修改了,但是又被改回了预期的值。

importjava.util.concurrent.atomic.AtomicInteger;

/**
*ABA问题演示
*/
publicclassABADemo1{
privatestaticAtomicIntegermoney=newAtomicInteger(100);
publicstaticvoidmain(String[]args)throwsInterruptedException{
//第一次点转账按钮(-50)
Threadt1=newThread(()->{
intold_money=money.get();//先得到余额
try{//执行花费2s
Thread.sleep(2000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
money.compareAndSet(old_money,old_money-50);
});
t1.start();

//第二次点击转账按钮(-50)不小心点击的,因为第一次点击之后没反应,所以不小心又点了一次
Threadt2=newThread(()->{
intold_money=money.get();//先得到余额
money.compareAndSet(old_money,old_money-50);
});
t2.start();

//给账户加50
Threadt3=newThread(()->{
//执行花费1s
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
intold_money=money.get();
money.compareAndSet(old_money,old_money+50);

});
t3.start();

t1.join();
t2.join();
t3.join();
System.out.println("最终的钱数:"+money.get());
}
}
42189b62-bf62-11ed-bfe3-dac502259ad0.png

这个例子演示了ABA问题,A有100元,A向B转钱,第一次转了50元,但是点完转账按钮没有反应,于是又点击了一次。第一次转账成功后A还剩50元,而这时C给A转了50元,A的余额变为100元,第二次的CAS判断(100,100,50),A的余额与预期的值一样,于是将A的余额修改为50元。

1.1.5 ABA问题的解决方案

由于CAS是只管头和尾是否相等,若相等,就认为这个过程没问题,因此我们就引出了AtomicStampedReference,时间戳原子引用,在这里应用于版本号的更新。也就是我们新增了一种机制,在每次更新的时候,需要比较当前值和期望值以及当前版本号和期望版本号,若值或版本号有一个不相同,这个过程都是有问题的。

我们来看上面的例子怎么用AtomicStampedReference解决呢?

importjava.util.concurrent.atomic.AtomicInteger;
importjava.util.concurrent.atomic.AtomicStampedReference;

/**
*ABA问题解决添加版本号
*/
publicclassABADemo2{
privatestaticAtomicStampedReferencemoney=
newAtomicStampedReference<>(100,0);
publicstaticvoidmain(String[]args)throwsInterruptedException{
//第一次点转账按钮(-50)
Threadt1=newThread(()->{
intold_money=money.getReference();//先得到余额100
intoldStamp=money.getStamp();//得到旧的版本号
try{//执行花费2s
Thread.sleep(2000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1);
System.out.println(Thread.currentThread().getName()+"转账:"+result);
},"线程1");
t1.start();

//第二次点击转账按钮(-50)不小心点击的,因为第一次点击之后没反应,所以不小心又点了一次
Threadt2=newThread(()->{
intold_money=money.getReference();//先得到余额100
intoldStamp=money.getStamp();//得到旧的版本号
booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1);
System.out.println(Thread.currentThread().getName()+"转账:"+result);
},"线程2");
t2.start();

//给账户+50
Threadt3=newThread(()->{
//执行花费1s
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
intold_money=money.getReference();//先得到余额100
intoldStamp=money.getStamp();//得到旧的版本号
booleanresult=money.compareAndSet(old_money,old_money+50,oldStamp,oldStamp+1);
System.out.println(Thread.currentThread().getName()+"发工资:"+result);

},"线程3");
t3.start();

t1.join();
t2.join();
t3.join();
System.out.println("最终的钱数:"+money.getReference());
}
}
422cd712-bf62-11ed-bfe3-dac502259ad0.png

AtommicStampedReference解决了ABA问题,在每次更新值之前,比较值和版本号。

1.2 悲观锁

什么是悲观锁?

悲观锁就是比较悲观,总是假设最坏的情况,每次去拿数据的时候都会认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿数据就会阻塞直到它拿到锁。

比如我们之前提到的synchronized和Lock都是悲观锁。

二、公平锁和非公平锁

公平锁: 按照线程来的先后顺序获取锁,当一个线程释放锁之后,那么就唤醒阻塞队列中第一个线程获取锁。

4235bb02-bf62-11ed-bfe3-dac502259ad0.png

非公平锁: 不是按照线程来的先后顺序唤醒锁,而是当有一个线程释放锁之后,唤醒阻塞队列中的所有线程,随机获取锁。

424aa724-bf62-11ed-bfe3-dac502259ad0.png

之前在讲synchronized和Lock这两个锁解决线程安全问题线程安全问题的解决的时候,我们提过:

synchronized的锁只能是非公平锁;

Lock的锁默认情况下是非公平锁,而挡在构造 函数中传入参数时,则是公平锁;

公平锁:Lock lock=new ReentrantLock(true);

非公平锁:Lock lock=new ReentrantLock();

由于公平锁只能按照线程来的线程顺序获取锁,因此性能较低,推荐使用非公平锁。

三、读写锁

3.1 读写锁

读写锁顾名思义是一把锁分为两部分:读锁和写锁。

读写锁的规则是:允许多个线程获取读锁,而写锁是互斥锁,不允许多个线程同时获得,并且读操作和写操作也是 互斥的,总的来说就是读读不互斥,读写互斥,写写互斥。

为什么要这样设置呢?

让整个读写的操作到设置为互斥不是更方便吗?

其实只要涉及到“互斥”,就会产生线程挂起等待,一旦挂起等待,,再次被唤醒就不知道什么时候了,因此尽可能的减少“互斥"的机会,就是提高效率的重要途径。

Java标准库提供了ReentrantReadWriteLock类实现了读写锁。

ReentrantReadWriteLock.ReadLock类表示一个读锁,提供了lock和unlock进行加锁和解锁。

ReentrantReadWriteLock.WriteLock类表示一个写锁,提供了lock和unlock进行加锁和解锁。

下面我们来看下读写锁的使用演示~

importjava.time.LocalDateTime;
importjava.util.concurrent.LinkedBlockingDeque;
importjava.util.concurrent.ThreadPoolExecutor;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.locks.ReentrantReadWriteLock;

/**
*演示读写锁的使用
*/
publicclassReadWriteLockDemo1{
publicstaticvoidmain(String[]args){
//创建读写锁
finalReentrantReadWriteLockreentrantReadWriteLock=newReentrantReadWriteLock();
//创建读锁
finalReentrantReadWriteLock.ReadLockreadLock=reentrantReadWriteLock.readLock();
//创建写锁
finalReentrantReadWriteLock.WriteLockwriteLock=reentrantReadWriteLock.writeLock();
//线程池
ThreadPoolExecutorexecutor=newThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,newLinkedBlockingDeque<>(100));
//启动线程执行任务【读操作1】
executor.submit(()->{
//加锁操作
readLock.lock();
try{
//执行业务逻辑
System.out.println("执行读锁1:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
readLock.unlock();
}
});

//启动线程执行任务【读操作2】
executor.submit(()->{
//加锁操作
readLock.lock();
try{
//执行业务逻辑
System.out.println("执行读锁2:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
//释放锁
readLock.unlock();
}
});

//启动线程执行【写操作1】
executor.submit(()->{
//加锁
writeLock.lock();
try{
System.out.println("执行写锁1:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
writeLock.unlock();
}
});

//启动线程执行【写操作2】
executor.submit(()->{
//加锁
writeLock.lock();
try{
System.out.println("执行写锁2:"+LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}finally{
writeLock.unlock();
}
});
}
}
426afbbe-bf62-11ed-bfe3-dac502259ad0.png

根据运行结果我们看到,读锁操作是一起执行的,而写锁操作是互斥执行的。

3.2 独占锁

独占锁就是指任何时候只能有一个线程能执行资源操作,是互斥的。

比如写锁,就是一个独占锁,任何时候只能有一个线程执行写操作,synchronized、Lock都是独占锁。

3.3 共享锁

共享锁是指可以同时被多个线程获取,但是只能被一个线程修改。读写锁就是一个典型的共享锁,它允许多个线程进行读操作 ,但是只允许一个线程进行写操作。

四、可重入锁 & 自旋锁

4.1 可重入锁

可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁。

因为在对象头存储了拥有当前锁的id,进入锁之前验证对象头的id是否与当前线程id一致,若一致就可进入,因此实现可重入锁 。

4.2 自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采取循环的方式尝试获取锁,这样的好处是减少线程上下文切换的消耗。线程上下文切换就是从用户态—>内核态。

synchronized就是一种自适应自旋锁(自旋的次数不固定),hotSpot虚拟机的自旋机制是这一次的自旋次数由上一次自旋获取锁的次数来决定,如果上次自旋了很多次才获取到锁,那么这次自旋的次数就会降低,因为虚拟机认为这一次大概率还是要自旋很多次才能获取到锁,比较浪费系统资源。




审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 虚拟机
    +关注

    关注

    1

    文章

    919

    浏览量

    28268
  • CAS
    CAS
    +关注

    关注

    0

    文章

    35

    浏览量

    15213
  • ABAT
    +关注

    关注

    0

    文章

    2

    浏览量

    6286

原文标题:一篇文章搞定,多线程常见锁策略+CAS

文章出处:【微信号:AndroidPush,微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Java多线程的用法

    本文将介绍一下Java多线程的用法。 基础介绍 什么是多线程 指的是在一个进程中同时运行多个线程,每个线
    的头像 发表于 09-30 17:07 965次阅读

    C++面向对象多线程编程 (pdf电子版)

    C++面向对象多线程编程共分13章,全面讲解构建多线程架构与增量多线程编程技术。第1章介绍
    发表于 09-25 09:39 0次下载

    QNX环境下多线程编程

    介绍了QNX 实时操作系统和多线程编程技术,包括线程间同步的方法、多线程程序的分析步骤、线程基本程序结构以及实用编译方法。QNX 是由加拿大
    发表于 08-12 17:37 30次下载

    多线程技术在串口通信中的应用

            首先介绍多线程技术的基本原理,然后讨论了多线程技术在串口通信中的应用,并给出了实现的方法和步骤。关键词:
    发表于 09-04 09:10 18次下载

    多线程细节问题学习笔记

    这一次我们要说下关于final在多线程的作用,原子性的使用,死锁以及Java中的应对方案,线程的局部变量 和 读写介绍 。关于final变量在
    发表于 11-28 15:34 1145次阅读
    <b class='flag-5'>多线程</b>细节问题学习笔记

    多线程好还是单线程好?单线程多线程的区别 优缺点分析

    摘要:如今单线程多线程已经得到普遍运用,那么到底多线程好还是单线程好呢?单线程多线程的区别又
    发表于 12-08 09:33 8.1w次阅读

    mfc多线程编程实例及代码,mfc多线程间通信介绍

    摘要:本文主要以MFC多线程为中心,分别对MFC多线程的实例、MFC多线程之间的通信展开的一系列研究,下面我们来看看原文。
    发表于 12-08 15:23 1.8w次阅读
    mfc<b class='flag-5'>多线程</b>编程实例及代码,mfc<b class='flag-5'>多线程</b>间通信<b class='flag-5'>介绍</b>

    什么是多线程编程?多线程编程基础知识

    摘要:多线程编程是现代软件技术中很重要的一个环节。要弄懂多线程,这就要牵涉到多进程。本文主要以多线程编程以及多线程编程相关知识而做出的一些结论。
    发表于 12-08 16:30 1.3w次阅读

    java学习——java面试【事务、多线程】资料整理

    本文档内容介绍了基于java学习java面试【事务、多线程】资料整理,供参考
    发表于 03-13 13:53 0次下载

    多线程编程指南的PDF电子书免费下载

    多线程编程指南》介绍了 SolarisTM 操作系统 (Solaris Operating System, Solaris OS)中 POSIX®线程和 Solaris 线程
    发表于 06-11 08:00 4次下载
    <b class='flag-5'>多线程</b>编程指南的PDF电子书免费下载

    CAS如何实现各种无的数据结构

    ,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某⼀数据时由于执行顺序不确定性以及中断的不可预知性产⽣的数据不一致问题 有了CAS,我们就可以用它来实现各种无
    的头像 发表于 11-13 15:38 832次阅读
    无<b class='flag-5'>锁</b><b class='flag-5'>CAS</b>如何实现各种无<b class='flag-5'>锁</b>的数据结构

    多线程同步的几种方法

    多线程同步是指在多个线程并发执行的情况下,为了保证线程执行的正确性和一致性,需要采用特定的方法来协调线程之间的执行顺序和共享资源的访问。下面将介绍
    的头像 发表于 11-17 14:16 1197次阅读

    多线程如何保证数据的同步

    。本文将详细介绍多线程数据同步的概念、问题、以及常见的解决方案。 一、多线程数据同步概念 在多线程编程中,数据同步指的是通过某种机制来确保多
    的头像 发表于 11-17 14:22 1263次阅读

    mfc多线程编程实例

    (图形用户界面)应用程序的开发。在这篇文章中,我们将重点介绍MFC中的多线程编程。 多线程编程在软件开发中非常重要,它可以实现程序的并发执行,提高程序的效率和响应速度。MFC提供了丰富的多线程
    的头像 发表于 12-01 14:29 1521次阅读

    java实现多线程的几种方式

    了多种实现多线程的方式,本文将详细介绍以下几种方式: 1.继承Thread类 2.实现Runnable接口 3.Callable和Future 4.线程池 5.Java 8中
    的头像 发表于 03-14 16:55 737次阅读