秋招Java后端开发冲刺——并发篇2(JMM与锁机制)

发布于:2024-07-07 ⋅ 阅读:(37) ⋅ 点赞:(0)

本文对Java的内存管理模型、volatile关键字和锁机制进行详细阐述,包括synchronized关键字、Lock接口及其实现类ReentrantLock、AQS等的实现原理和常见方法。

一、JMM(Java内存模型)

1. 介绍
JMM定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
2. 模型
在这里插入图片描述

  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

二、Volatile关键字

1. 介绍

  • Volatile关键字主要用于修饰成员变量(实例变量或类变量),使得对该变量的读写操作直接与主内存交互,而不是通过线程本地的工作内存进行缓存。
  • Volatile关键字提供了一种轻量级的同步机制,用于保证多线程环境下数据的可见性(但不能保证数据的原子性,Synchronized关键字既保证可见性,又保证原子性)

2. Volatile禁止指令重排
(1)指令重排

指令重排是一种编译器和处理器优化技术,它改变指令执行的顺序,以提高性能。在单线程环境下,这种优化不会影响程序的正确性。但是在多线程环境下,指令重排可能导致意想不到的结果,破坏程序的正确性。

(2)Volatile如何禁止指令重排

  • 通过在读写 volatile 变量时插入内存屏障(Memory Barriers)来实现
    • 写屏障(Write Barrier):确保在写 volatile 变量之前的所有写操作在内存中完成。
    • 读屏障(Read Barrier):确保在读 volatile 变量之后的所有读操作在内存中完成
  • volatile关键字一般加在写变量最后一个,读变量第一个。
class Example {
    private volatile boolean flag = false;
    private int a = 0;

    public void writer() {
        a = 1;           // 1
        flag = true;     // 2
    }

    public void reader() {
        if (flag) {      // 3
            int i = a;   // 4
        }
    }
}

上述示例中:

  • 在writer方法中,写入a = 1(1)操作不能被重排序到写入flag = true(2)之后。因为flag是volatile,在写flag的时候会插入一个StoreStore屏障,确保在flag被写入之前,所有之前的写操作都完成了。
  • 在reader方法中,读取flag(3)操作不能被重排序到读取a(4)之前。因为flag是volatile,在读flag的时候会插入一个LoadLoad屏障,确保在flag被读取之后,所有之前的读操作都完成了。

三、锁

1. 悲观锁和乐观锁
(1)悲观锁:认为访问共享资源时一定会发生线程安全问题,因此在访问时必须加锁。
(2)乐观锁:认为访问共享资源时不一定会发生线程安全问题,因此不会加锁,当提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改即可。
(3)二者区别

特性 悲观锁 乐观锁
锁定机制 操作数据前先加锁,其他线程无法操作 操作数据时不加锁,提交时检查冲突
使用场景 适用于写多读少,冲突概率高的场景 适用于读多写少,冲突概率低的场景
性能 由于频繁加锁和释放锁,性能较低 不加锁,性能较高
实现方式 使用数据库锁机制(如行锁、表锁) 使用版本号或时间戳进行冲突检测
数据一致性 一致性强,通过锁机制防止并发修改 一致性依赖于冲突检测和重试机制
并发性 并发性差,锁定会阻塞其他线程 并发性好,线程可以同时进行操作
死锁风险 存在死锁风险 不存在死锁风险
典型应用 行锁、表锁、页锁和意向锁(数据库)、ReentrantLock等 版本控制系统、CAS(Compare and Swap)操作

2. synchronized 关键字
(1)介绍
synchronized 关键字是一种对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁。
(2)底层实现原理
① Monitor(对象监视器)

  • Monitor(监视器)是一种同步机制,用于管理多线程之间的互斥访问和并发控制。Monitor是JVM提供的,但是是基于c++实现的。
  • Monitor的结构
    • Owner:存储当前获取锁的线程的,只能有一个线程可以获取
    • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
    • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

在这里插入图片描述
② synchronized关键字实现原理
线程获得锁需要使用对象(锁)关联monitor监视器,即获得monitor的持有权。

(3)使用方法

  • 同步方法:当一个线程执行同步方法时,它会自动获得该方法所属对象的锁。其他线程在获得该锁之前无法执行该方法。
public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}
  • 同步块:同步块用于在一个方法内同步部分代码,提供了比同步方法更细粒度的控制。
public class SynchronizedBlockExample {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}
  • 静态同步方法:静态同步方法锁定的是类对象,对于所有该类的实例都有效。
public class StaticSynchronizedExample {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static synchronized int getCount() {
        return count;
    }
}

3. synchronized锁升级

Monitor实现的锁属于重量级锁,因为monitor是使用c++实现的,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。在JDK 1.6引入了两种新型锁机制:偏向锁轻量级锁

(1)对象的内存结构
在这里插入图片描述
:填充为8的整数倍是方便字节对齐提高程序运行效率 ,而且对于压缩指针也更加的方便。

(2)轻量级锁
在无竞争的情况下,轻量级锁使用CAS操作来实现锁的获取和释放,提高运行效率。
① 加锁流程

  • 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
  • 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
  • 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。(仍需CAS操作)
  • 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。

② 解锁过程

  • 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
  • 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
  • 如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

(3)偏向锁
与轻量级锁不同的是,偏向锁只第一次获得锁时使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后该线程再获取锁时,只需要判断锁对象mark word中是否是自己的线程id即可。

:轻量级锁和偏向锁都是在没有竞争的情况下使用,一旦发生了竞争会立刻升级为重量级锁。

4. Lock接口:ReentrantLock

Lock接口提供了一种比传统的同步块和方法更灵活的锁机制。Lock 接口有多种实现,其中最常用的是 ReentrantLock。

(1)Lock接口常见实现类

实现类 描述
ReentrantLock 可重入的互斥锁,具有与使用 synchronized 方法和语句相同的基本行为和语义,但功能更强大。
ReentrantReadWriteLock.ReadLock 读锁,可被多个读线程同时持有,只要没有写线程持有写锁。
ReentrantReadWriteLock.WriteLock 写锁,独占锁,只有写线程可以持有,读线程和其他写线程都被阻塞。
StampedLock 一种新的读写锁实现,支持乐观读锁和写锁的互斥锁操作,提高并发性和性能。
LockSupport 提供了基本的线程阻塞原语,用于创建锁和其他同步工具。

(2)ReentrantLock
① ReentrantLock是一个可重入且独占式的锁,还支持轮询、超时、中断、公平锁和非公平锁等。
② 特点

  • 可重入性:一个线程可以多次获得该锁,而不会导致死锁。这意味着同一个线程可以进入锁保护的代码块多次,必须确保在离开代码块时相应的次数解锁。
  • 公平性:可以选择公平锁或者非公平锁。公平锁按照线程请求的顺序获取锁,非公平锁则没有这种顺序,可能导致某些线程长时间等待。
  • 灵活性:提供了比 synchronized 块更灵活的锁获取方式,如可中断的锁获取、超时的锁获取。

③ 实现原理

  • ReentrantLock使用CAS+AQS来实现,使用用一个整数state表示锁的状态,使用一个先进先出的队列(CLH)管理被阻塞的线程。
  • 当线程需要获得锁时,使用CAS操作修改锁的状态,修改成功则获得锁;修改失败则加入CLH队列管理。

④ 常用方法

方法 描述
lock() 获取锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获得锁。
unlock() 释放锁。
tryLock() 尝试获取锁。如果锁在调用时未被另一个线程持有,则获取锁并立即返回 true;否则返回 false
tryLock(long time, TimeUnit unit) 尝试在给定的等待时间内获取锁。如果锁在给定时间内可用,则获取锁并返回 true;否则返回 false
lockInterruptibly() 如果当前线程未被中断,则获取锁。如果锁不可用,则当前线程出于线程调度目的将被禁用并处于休眠状态,直到发生以下两种情况之一:锁由当前线程获得;或者其他某个线程中断当前线程。
newCondition() 返回一个绑定到此 Lock 实例的新 Condition 实例。

5. synchronized与ReentrantLock的比较

特性 synchronized ReentrantLock
锁类型 隐式锁 显式锁
获取锁的方式 自动 需要显式调用 lock() 方法
释放锁的方式 自动 需要显式调用 unlock() 方法
可重入性
公平锁支持 是(通过构造函数可以指定)
条件变量支持 是(通过 newCondition() 方法)
获取锁中断 是(通过 lockInterruptibly() 方法)
超时锁获取 是(通过 tryLock(long time, TimeUnit unit) 方法)
性能开销 较低(JVM 内部优化) 较高
锁的范围 代码块或方法 灵活,可锁代码块
代码复杂度 较高
推荐使用场景 简单的同步需求 复杂的同步需求,如需要公平锁、超时等

6. AQS(Abstract Queued Synchronizer,抽象队列同步器)
(1)介绍
AQS是Java 并发包 (java.util.concurrent.locks) 中的一个核心框架,是构建各种锁和同步组件的基础框架。
(2)实现原理

  • AQS 使用一个 int 类型的变量 state 表示同步状态。
  • AQS 内部维护了一个 FIFO 队列,用于管理获取锁失败的线程。线程在争夺锁失败后会被封装成队列节点(Node)并加入到等待队列中。
  • 队列中每个节点包含线程引用和状态标志

(3)常用方法

方法 描述
acquire(int arg) 独占模式获取锁,如果获取失败则进入等待队列。
release(int arg) 独占模式释放锁,如果释放成功则唤醒等待队列中的下一个节点。
acquireShared(int arg) 共享模式获取锁,如果获取失败则进入等待队列。
releaseShared(int arg) 共享模式释放锁,如果释放成功则唤醒等待队列中的下一个节点。
addWaiter(Node mode) 将当前线程封装成节点并加入等待队列的尾部。
unparkSuccessor(Node node) 唤醒等待队列中的下一个节点。

(4)常见实现类

实现类 描述
ReentrantLock 可重入独占锁。允许线程重复获取同一把锁。
ReentrantReadWriteLock 可重入读写锁。支持多个线程同时读取数据,但写操作是互斥的。
Semaphore 信号量。控制同时访问特定资源的线程数。
CountDownLatch 倒计时闩。一个或多个线程等待其他线程完成操作后再继续执行。
CyclicBarrier 循环栅栏。一组线程相互等待,达到同步点后再继续执行。
Phaser 阶段器。管理多个参与者线程的同步,支持动态注册和注销。
StampedLock 读写锁的改进版本,支持乐观读取和写锁。
Condition 条件变量。与锁配合使用,允许线程等待某个条件变为真。

网站公告

今日签到

点亮在社区的每一天
去签到