java多线程常见锁策略CAS机制(1)

简介: java多线程常见锁策略CAS机制(1)

本节要点

了解常见锁策略

了解synchronized使用的锁策略

理解CAS实现逻辑

了解CAS出现的ABA问题,并解决

synchronized锁的原理

常见锁策略

我们已经知道锁在我们的并发编程十分重要.那我们就需要了解,这些锁实现的策略!都有那些策略,便于我们更加深刻的理解锁!


下面介绍的几组锁策略,每一组里面都是相异的,每组策略之间又有相互关联的!


乐观锁 vs 悲观锁

这是程序员处理锁冲突的态度(原因),通过自己的预期而实现咋样的锁

就好比疫情:

乐观的人觉得过段时间就好了,就不会囤太多物资

悲观的人觉得紧张,就屯好多物资,做了好多工作


乐观锁


程序员在设计锁的时候,预期锁冲突概率很低


做的工作更少,付出的成本低,更高效


悲观锁


预期锁冲突概率很高


做的工作多,付出的成本高,更低效


互斥锁vs读写锁

互斥锁


只有多个线程对同一个对象加锁才会导致互斥


互斥锁就是普通的锁,只有加锁和解锁


读写锁


可以对读操作加锁(不存在互斥关系,可以多线程读)

对写操作加锁(只能进行写操作)

读写操作加锁(读时不能写,写时不能读)


轻量级锁vs重量级锁

轻量级锁


做的事情更少,开销比较小


重量级锁


做的事情更多,开销比较大


这里的轻量级锁和重量级锁和上面的悲观锁和乐观锁有所重叠

一个是设计锁的态度(原因),一个是处理锁冲突的结果!

通常情况下一般可以认为乐观锁一般都是轻量级锁,悲观锁都是重量级锁!但是不绝对!!!


是如何实现轻量和重量呢?

其实我们基于纯用户态实现的锁就是认为是轻量级锁,开销小,程序员可控!

如果是基于内核的一些功能实现(比如调用了操作系统内核的mutex接口)的锁就认为是重量级锁(操作系统的锁会在内核做好多事情,比如让线程等待…)


自旋锁vs挂起等待锁

这是上述轻量级锁和重量级锁的典型实现


自旋锁


往往通过纯用户态代码实现,较轻


挂起等待锁


通过内核的一些机制实现,往往较重


公平锁vs非公平锁

公平锁


多个线程等待一把锁时,遵循先来后到原则


非公平锁


多个线程等待一把锁时,每个线程拿到锁的机会均等!


这里就有人有疑惑了,咋的,机会均等还不公平了?

但是你换个场景想想,如果你在等待办理业务,先来不就应该先办业务嘛,就好比排队,你先来排在前面!所以这才公平嘛!!!


可重入锁vs不可重入锁

可重入锁


可重入锁就是对一个对象多次加锁时不会造成死锁


不可重入锁


一个对象多次加锁时会造成死锁!


synchronized使用的锁策略

我们了解了上述的多组锁策略,我们来分析一下,synchronized用了那些锁策略!


自适应锁,即是乐观锁又是悲观锁

不是读写锁只是普通的互斥锁

既是一个轻量级锁又是重量级锁(根据锁竞争程度自适应)

轻量级锁的部分基于自旋锁实现,重量级锁基于挂起等待宿实现

非公平锁(锁拿到的机会均等)

可重入锁(加锁多次,不会导致死锁)

CAS

什么是cas?


CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 – 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。


可能有点抽象,我们看下面案例!


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


比较 A 与 V 是否相等。(比较)

如果比较相等,将 B 写入 V。(交换)

返回操作是否成功。

image.png

可能看到这里你还是很懵!


boolean CAS(address, expectValue, swapValue) {
  if (&address == expectedValue) {
        &address = swapValue;
     return true;
    }
  return false;
}

我们看这个伪代码!

image.png

通俗点讲,就是CAS解决了多线程中多条指令进行的赋值问题!

我们之前已经了解过当我们需要对一个值进行++在cpu中其实要执行3条指令!

先拿到值放在寄存器,将寄存器中的值更改,然后在放回内存!

image.png

而我们知道在多线程执行写操作时,就会导致线程不安全问题!

因为++操作并不是原子性的!


而这里的CAS做的就是将多条指令封装成一条指令,达到原子性的效果!避免线程不安全问题!


我们的cpu提供了一个单独的指令cas来执行上诉代码!!!


CAS使用

CAS可以做什么呢?


基于CAS能够实现"原子类"

java标准库中给我们提供了一组原子类,就是将常用类(int long array …)进行了封装,可以基于CAS进行修改,并且线程安全!

//基于CAS多线程对一个数实现自加
import java.util.concurrent.atomic.AtomicInteger;
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        //原子类
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000 ; i++) {
                //这个方法相当于 ++num
                atomicInteger.incrementAndGet();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000 ; i++) {
                //这个方法相当于 ++num
                atomicInteger.incrementAndGet();
            }
        });
        t1.start();//启动线程
        t2.start();
        t1.join();
        t2.join();//等待2个线程执行结束
        System.out.println("多线程自加结果:"+atomicInteger.get());
    }
}

image.png

可以看到我们基于CAS多线程进行一个数的更改并不用加锁也能保证线程安全!!!


我们来学习一下java原子类中的一些方法!


原子类在java.util.concurrent.atomic包下!

image.png

构造方法,可以给初值!

image.png

实现+=操作

image.png

自加自减!

image.png

我们来看一下这里自加的实现逻辑!


//伪代码
class AtomicInteger {
  private int value;
  public int getAndIncrement() {
    int oldValue = value;
    while ( CAS(value, oldValue, oldValue+1) != true) {
     oldValue = value;
    }
    return oldValue;
  }
}

我们分析上述伪代码!

image.png


image.png

image.png


基于CAS能够实现"自旋锁"

//伪代码
public class SpinLock {
  private Thread owner = null;
  public void lock(){
  // 通过 CAS 看当前锁是否被某个线程持有.
  // 如果这个锁已经被别的线程持有, 那么就自旋等待.
  // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
  while(!CAS(this.owner, null, Thread.currentThread())){
  }
  }
  public void unlock (){
  this.owner = null;
  }
}

image.png

可以看到自旋锁的实现和原子类的实现类似!

我们来分析一下!


如果已经有线程持有了该锁对象,那么while循环就会一直自旋,直到该锁被释放,该线程才可以拿到该锁!


if(this.owner==null) 为真,Thread.currentThread当前线程就可以拿到该锁!否者自旋等待锁!


CAS的ABA面试问题

我们知道CAS的实现是通过对比当前CPU中的值和内存中的值是否相等,如果相等就采取计然后进行交换!


我们是否想过另外一种情况,就是该内存中的值改变了多次,又改回了原来那个值,显然这时内存中的值虽然和cpu中值相等,但是该值已经进行了多次改变,并没有保证此次CAS的原子性!


举个例子:

当有两个线程t1 和t2 这两个对象对同一块内存空间采取CAS修改操作!


我们已经知道CAS的原子性,t1和t2都能执行完成!

image.png

而如果这时有第3个线程在t1的load和t2的CAS之间将该值又更改回去,那么就出现bug了


我们假设一种现实场景:


某一天你去ATM取款

你的余额为1000元

然后你取款500元

你不小心多点了一次取款,但是你没有察觉到!

然后第一次你取款成功了,正常情况你第二次肯定无法取款成功!

因为我们知道CAS会比较寄存器和内存中的值!而此时的余额已经不是1000了,该CAS指令就无法成功执行!

但是如果在你执行第二个取款操作之前,你的朋友刚好给你转账500元!这样CAS在比较时发现相等就会再次执行取款操作,你取500居然取出了1000,余额还有500,这就是一个BUG!


我们用图来描述一下上述情况!

image.png



我们如何解决这个问题呢?


我们可以引入一个版本号

记录每次更改内存的次数,如果更改一次,版本号就加1,且版本号只能递增!!!

进行比较时只需要比较版本号即可,如果版本号相等就可以进行交换!


我们再进行上述的CAS就不会产生bug了!

image.png


当我们引入版本号时,每次只要比较版本号的值是否相等就可以判断内存中的值是否已经修改过,很好的解决了CAS中的ABA问题!


我们也可以用时间戳代替版本号,达到的效果一样,也可解决ABA问题!

目录
相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
5月前
|
安全 Java 程序员
Java并发编程中的锁机制与优化策略
【6月更文挑战第17天】在Java并发编程的世界中,锁是维护数据一致性和线程安全的关键。本文将深入探讨Java中的锁机制,包括内置锁、显式锁以及读写锁的原理和使用场景。我们将通过实际案例分析锁的优化策略,如减少锁粒度、使用并发容器以及避免死锁的技巧,旨在帮助开发者提升多线程程序的性能和可靠性。
|
4月前
|
存储 缓存 Java
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
51 0
|
6月前
|
安全 Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第30天】 在多线程环境下,确保数据的一致性和程序的正确性是至关重要的。Java提供了多种锁机制来管理并发,但不当使用可能导致性能瓶颈或死锁。本文将深入探讨Java中锁的优化策略,包括锁粗化、锁消除、锁降级以及读写锁的使用,以提升并发程序的性能和响应能力。通过实例分析,我们将了解如何在不同场景下选择和应用这些策略,从而在保证线程安全的同时,最小化锁带来的开销。
|
6月前
|
安全 Java 开发者
Java并发编程中的锁优化策略
【5月更文挑战第30天】 在Java并发编程领域,锁机制是实现线程同步的关键手段之一。随着JDK版本的发展,Java虚拟机(JVM)为提高性能和降低延迟,引入了多种锁优化技术。本文将深入探讨Java锁的优化策略,包括偏向锁、轻量级锁以及自旋锁等,旨在帮助开发者更好地理解和应用这些高级特性以提升应用程序的性能。