🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 乐观锁与悲观锁概述

        1.1 悲观锁(Pessimistic Locking)

        1.2 乐观锁(Optimistic Locking)

        1.3 区别与适用场景

        2.0 轻量级锁与重量级锁概述

        2.1 真正加锁的底层逻辑顺序

        2.2 轻量级锁

        2.3 重量级锁

        2.4 区别于适用场景

        2.5 轻量级锁与乐观锁的区别、重量级锁与悲观锁的区别

        3.0 自旋锁与挂起等待锁概述

        3.1 自旋锁(Spin Lock)

        3.2 挂起等待锁(Suspend-Resume Lock)

        3.3 自旋锁是一种典型的轻量级锁的实现方式

        4.0 公平锁与非公平锁概述

        4.1 公平锁(Fair Lock)

        4.2 非公平锁(Unfair Lock)

        5.0 可重入锁与不可重入锁概述

        6.0 读写锁与互斥锁概述

        6.1 读写锁(Read-Write Lock)

        6.2 互斥锁(Mutex Lock)

        7.0 CAS 概述

        7.1 CAS 的实际应用

        7.1.1 CAS 的实际应用 - 实现原子类

        7.1.2 CAS 的实际应用 - 自旋锁


        1.0 乐观锁与悲观锁概述

        乐观锁和悲观锁是两种并发控制的策略,用于处理多线程环境下的数据访问和更新。它们的主要区别在于对并发情况的预期和处理方式。

        synchronized 是乐观锁也悲观锁。

        1.1 悲观锁(Pessimistic Locking)

        悲观锁的基本思想是在操作数据之前先获取锁,假定会并发访问,因此在整个操作过程中都持有锁,以防其他线程对数据进行修改。悲观锁通常会导致其他线程在访问数据时被阻塞,以确保数据的一致性。

        简单来说,在多线程中对共享变量进行操作时,会认为其他线程都会对这个共享变量进行操作,因此,从悲观锁的角度来说,先加锁成功后,才能对共享变量进行操作;否则,只能阻塞等待锁释放。从而可以确保线程安全。

        常见的悲观锁:synchronized 关键字、ReentrantLock 等锁机制。

        1.2 乐观锁(Optimistic Locking)

        乐观锁的基本思想是假设在数据操作过程中不会发生并发冲突,因此不会立即加锁,而是在更新数据时检查是否有其他线程已经对数据进行修改了。如果没有发现数据被修改,那么继续操作;如果发现数据已经被修改了,会进行回滚或者重试。

        简单来说,在多线程中对共享变量进行操作时,一开始不会认为有其他线程会对该共享变量进行操作,认为当前线程可以安心的对该变量进行操作。执行到后面,如果发现已经有其他线程对该共享变量进行了操作了,那么当前的线程执行回滚或者重试。

        1.3 区别与适用场景

        synchronized 一开始是使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换悲观锁策略。

        悲观锁:真正加上了锁处理,适用于并发写入较多、冲突概率较高的场景,适合长事务处理。

        乐观锁:没有真正的加锁处理,适用于并发读取较多,冲突概率较低的场景,适合短事务处理。

        2.0 轻量级锁与重量级锁概述

        轻量级锁和重量级锁是 Java 中用于实现同步的两种锁机制,用于保护共享资源在多线程环境下的访问。它们的设计目的是为了在不同情况下提供更高效的并发控制。

        synchronized 是轻量级锁也是重量级锁。

        2.1 真正加锁的底层逻辑顺序

        CPU 提供了原子操作指令给操作系统,操作系统提供了 mutex 互斥锁给 JVM ,接着 JVM 将锁封装成了 synchronized 中的悲观锁、ReenTrantLock 等。

        2.2 轻量级锁

        加锁机制尽可能不适用 mutex ,而是尽量在用户态代码中完成,实在锁竞争太大、锁冲突太大了,再转换为重量级,即使用 mutex 。

        轻量级锁是一种乐观锁机制,用于优先低竞争情况下的同步操作。当一个线程尝试获取锁是,如果锁没有被其他线程占用,会将对象的 Mark Word 指向当前线程,将对象状态标记为“偏向锁”。

        2.3 重量级锁

        加锁机制重度依赖了 OS 提供了 mutex ,大量的内核态用户态切换,很容易引发线程的调度。两个操作成本比较高,一旦涉及到用户态和内核态的切换,就意味着“沧海桑田”。

        重量级锁是一种悲观锁机制,用于处理高竞争情况下的同步操作。当多个线程竞争同一把锁时,会将锁升级为重量级锁,线程会被阻塞,进入阻塞状态。

        2.4 区别于适用场景

        synchronized 开始是一个轻量级锁。如果锁冲突比较严重,就会变成重量级锁。

        轻量级锁:适用于低竞争情况下的同步操作,提高了并发性能,但在高竞争情况下会升级为重量级锁。

        重量级锁:适用于高竞争情况下的同步操作,保证了数据的一致性,但在低竞争情况下会带来额外的开销。

在实际应用中,Java虚拟机会根据当前线程的竞争情况动态地选择轻量级锁或重量级锁来进行同步操作,以提高系统的并发性能和数据一致性。

        2.5 轻量级锁与乐观锁的区别、重量级锁与悲观锁的区别

轻量级锁:

        轻量级锁是一种乐观锁,它尝试使用 CAS(Compare and Swap)等原子操作来尝试获取锁,避免了线程阻塞和内核态操作,因此被称为轻量级。如果 CAS 操作成功,线程就成功获取了锁,如果失败,则会升级为重量级锁或其他适合的锁机制。轻量级锁并不是没有真正的加锁,而是通过乐观的方式尝试获取锁,避免了一些开销较大的操作。

重量级锁:

        重量级锁是一种悲观锁,它通常会涉及到线程的阻塞、唤醒和操作系统的调度等操作,因此被称为重量级。当多个线程竞争锁时,重量级锁会导致线程进入阻塞状态,等待其他线程释放锁后才能继续执行。重量级锁会涉及到真正的加锁操作,包括线程的阻塞和唤醒等。

        3.0 自旋锁与挂起等待锁概述

        自旋锁和挂起等待锁是两种不同的锁机制,它们在处理线程同步和互斥时有不同的实现方式和特点。

        synchronized 是自旋锁也是挂起锁。

        3.1 自旋锁(Spin Lock)

        自旋锁是一种基于忙等待的锁机制,当一个线程尝试获取锁时,如果发现锁已经被其他线程占用,它会一直循环检查锁的状态(自旋)直到锁可用。自旋锁适用于锁被占用时间较短的情况,因为它可用减少线程切换的开销。但是如果锁被长时间占用,自旋锁会导致线程长时间占用 CPU 资源而无法进展,造成性能问题。

        简单来说,自旋锁一直会占用 CPU 资源,所谓的“空转”、“忙等待”,只要锁被释放了,那么自旋锁就会立马获取锁,效率高。

        按照之前的方式,线程再抢锁失败后,进入阻塞状态,放弃 CPU ,需要过很久再次被调度,但实际上,大部分情况下,虽然当前线程抢锁失败,但过不了多久,锁就会被释放。这样就没有必要放弃 CPU 资源。这时候就可以适用自旋锁来处理这样的问题。

        3.2 挂起等待锁(Suspend-Resume Lock)

        挂起等待锁是一种基于线程阻塞和唤醒的锁机制,当一个线程尝试获取锁时,如果发现锁已经被占用,它会被挂起阻塞等待其他线程释放锁。当锁可用时,其他线程会唤醒被挂起的线程继续执行。

        挂起等待锁适用于锁被占用时间长的情况,因为它可以避免线程忙等待占用 CPU 资源,但是会引入线程切换的开销。

        3.3 自旋锁是一种典型的轻量级锁的实现方式

        synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的。

优点:没有放弃 CPU ,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。

缺点:如果锁被其他线程持有的时间比较长,那么就会持续的消耗 CPU 资源。(而挂起等待的时候是不会消耗 CPU 资源的)。

        相对应的,挂起等待锁是一种典型的重量级锁的实现方式。

        4.0 公平锁与非公平锁概述

        公平锁和非公平锁是两种不同的锁策略,它们主要影响了锁的获取顺序和公平性。

        synchronized 是非公平锁。ReenTrantLock 默认是非公平锁,可以转换成公平锁。

ReentrantLock fairLock = new ReentrantLock(true); // 创建公平锁

        通过将参数设置为 true,可以创建一个公平锁;而默认情况下参数为 false,创建的是非公平锁。

        4.1 公平锁(Fair Lock)

        公平锁是一种保证锁的获取按照请求的顺序进行的锁。当一个线程请求一个公平锁时,如果锁当前被其他线程占用,该线程会进入等待队列,按照先来先服务的原则等待获取锁。当锁释放时,等待时间最长的线程会被唤醒并获取锁。公平锁能够保证线程按照请求的顺序获取锁,避免了线程饥饿的问题。

        4.2 非公平锁(Unfair Lock)

        非公平锁是一种允许锁获取竞争策略,它允许新请求的线程直接尝试获取锁,而不考虑等待队列中的线程顺序。如果锁当前被其他线程占用,新请求的线程会直接尝试获取锁,而不会进入等待队列。这种策略可能会导致某些线程长时间无法获取锁,造成线程饥饿的问题。

        5.0 可重入锁与不可重入锁概述

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

        Java 里只要以 ReenTrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入锁。

        而 Linux 系统提供的 mutex 是不可重入锁。

        6.0 读写锁与互斥锁概述

        读写锁简单来说,读操作与读操作并发执行中不会加锁,读操作与写操作并发操作中会加锁,写操作与写操作并发操作中会加锁。

        互斥锁简单来说,无论进行哪一种操作,并发执行的操作都需要加上锁。

        synchronized 不是读写锁,是互斥锁。

        ReenTrantLock 不是读写锁,是互斥锁。

ReenTrantLock 使用代码演示:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int sharedData = 0;

    public void incrementData() {
        lock.lock();
        try {
            sharedData++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        // 创建多个线程并发执行 incrementData 方法
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementData();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementData();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final shared data value: " + example.sharedData);
    }
}

        6.1 读写锁(Read-Write Lock)

        读写锁允许多个线程同时读取共享资源,但在有写操作时需要互斥访问。

读操作:

        多个线程可以同时获取读锁,并发执行读操作,不会互斥。

写操作:

        写锁是互斥的,即写操作与任何其他操作或写操作都是互斥的。当有线程持有写锁时,其他线程无法获取读锁或者写锁,直到写操作释放写锁。

        ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个对象提供了 lock/unlock 方法进行加锁解锁。

        ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象提供了 lock/unlock 方法进行加锁解锁。

代码如下:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyReentrantLock {

    private int data = 100;
    private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    public int readData(){
        readLock.lock();
        try {
            return data;
        }finally {
            readLock.unlock();
        }
    }

    public void writeData(int data){
        writeLock.lock();
        try {
            this.data = data;
        }finally {
            writeLock.unlock();
        }
    }
}
public class demo1 {
    public static void main(String[] args) {
        MyReentrantLock myReentrantLock = new MyReentrantLock();
        //读取数据
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "-->读取数据为: " + myReentrantLock.readData());
            }).start();

        }

        //写数据
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
               myReentrantLock.writeData(1);
            }).start();
        }

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "-->读取数据为: " + myReentrantLock.readData());
            }).start();

        }
    }
}

        6.2 互斥锁(Mutex Lock)

        互斥锁是一种常见的锁机制,用于保护共享资源的互斥访问。无论是读操作还是写操作,都要获取互斥锁才能访问共享资源。

        7.0 CAS 概述

        CAS(Compare and Swap)是一种并发控制机制,通常用于实现无锁算法。它主要用于解决多线程并发访问共享数据时的原子性操作问题。

        CAS 的基本原理是利用 CPU 提供的原子性指令来实现无锁的原子操作。当多个线程同时尝试执行 CAS 操作时,只有一个线程会成功,其他线程会失败并重试。

CAS 操作包括三个步骤:

        1)比较(Compare):首先, CAS 会比较当前内存中的值和预期值是否相等。

        2)交换(Swap):如果相等,CAS 就会将新值写入到主内存中;否则,不做任何操作。

        3)返回(Return):CAS 操作会返回操作是否成功的结果,通常时一个布尔值。

        这三个操作都是原子性的,因此不存在线程安全问题。

图解:

        7.1 CAS 的实际应用

        原子操作、非阻塞算法、自旋锁、ABA 问题的解决、乐观锁的实现等。

        7.1.1 CAS 的实际应用 - 实现原子类

        CAS 可以用于实现原子操作,比如 AtomicInteger、AtomicLong 等原子类都是基于 CAS 实现的。在多线程环境下,通过 CAS 可以确保对共享变量的操作是原子的,避免了使用锁带来的性能开销。

        比如说 AtomicInteger 类,是基于 CAS 的思想实现的,假如有一个 AtomicInteger 的实例对象,在多线程中,对该实例对象进行 +1 操作,一般来说,如果不加锁的话,会出现线程安全问题,但是对于当前对象来说,即使不用加锁,也不用出现线程安全问题。

代码如下:

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomicInteger {
    private AtomicInteger count = new AtomicInteger(0);

    public void add(){
        System.out.println(count.incrementAndGet());
    }
}
public class Text {
    public static void main(String[] args) {
        MyAtomicInteger myAtomicInteger = new MyAtomicInteger();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                for (int j = 0; j < 5000; j++) {
                    myAtomicInteger.add();
                }
            }).start();
        }
    }
}

运行结果如下:

        运行结果是正确的,没有出现线程安全问题。

        这是为什么即使没有加上锁也不会出现线程安全问题呢?

        答案就在用了 AtomicInteger 修饰的变量,且 +1 操作用到了 count.incrementAndGet() 实例方法。

详细对 count.incrementAndGet() 方法进行分析:

        该方法中还包含了 getAndAddInt() 方法,第一个参数是代表着当前对象,第二个参数可以认为是存放值的地址,第三个参数默认为 1 。

进入 getAndAddInt() 方法进行分析:

         参数 o 代表着当前对象,参数 offset 代表着值的地址,参数 delta 为 1 。该方法中内部定义了一个变量 v ,通过 getIntVolatile() 这个方法,用当前的对象还有值的地址获取到最新的数据赋值给 v 。再接着通过 weakCompareAndSetInt() 方法,来比较当前的 v 跟之前获取的 v 的值是否相同,如果相同,代表着没有线程访问这个数据,只有当前线程正在访问,那么就可以对这个数据进行修改,再返回到主内存中;如果不相同,代表有其他线程访问这个数据,此时不能直接将当前线程更新的值放到主内存中,会出现线程安全问题,因此重复循环,再来新一轮,先获取主内存中最新的数据,在来比较当前数据与之前获取到的 v 是否相同...一直循环往复。直到当前数据与之前的获取到的 v 相同,那么就可以将值放入到内存中。

进入 weakCompareAndSetInt() 方法进行分析:

        如果成功就返回 true,否则返回 false 。 

        最后,可以清楚的了解到以上这个思想跟 CAS 的机制是一致的。

        7.1.2 CAS 的实际应用 - 自旋锁

        自旋锁是基于 CAS 机制实现更灵活的锁,获取到更多的控制权。

伪代码:

public class MySpinLock {
    private  Thread ower = null;

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

        }
    }

    public void unlock(){
        this.ower = null;
    }
}

        结合自旋锁的特点和 CAS 机制来分析,线程只要没有获取的锁,就会一直占用 CPU 资源等待,直到锁释放为止,如何来判断锁是否被占用呢?

        就可以通过 CAS 机制来判断,大概流程是:判断当前的线程 ower 是否为 null ,如果是,则将 ower 修改为当前线程所持有,这样来看,其他线程也会通过 CAS 机制来判断当前的 ower 是否否为 null ,返回结果为 false ,则只能空转了,等待当前线程释放锁,此时释放锁会把 ower 赋值为 null 。交给其他线程来获取这把“锁”。 

更多推荐