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问题!

目录
相关文章
|
12天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
12天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
6天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
6天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
23 3
|
12天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
37 5
|
15天前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
30 6
|
12天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
46 1
|
13天前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
36 0
|
16天前
|
缓存 Java 开发者
Java中的多线程编程:从基础到进阶
在Java的世界中,多线程编程是一块不可或缺的拼图。它不仅提升了程序的效率和响应性,还让复杂任务变得井然有序。本文将带你领略多线程编程的魅力,从创建线程的基础操作到高级同步机制的应用,再到线程池的高效管理,我们将一步步揭开多线程编程的神秘面纱。无论你是初学者还是有一定经验的开发者,这篇文章都会为你提供新的视角和深入的理解。让我们一起探索Java多线程编程的世界,开启一段精彩的旅程吧!
31 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
51 1
C++ 多线程之初识多线程