字节面试:说说Java中的锁机制?

java · 浏览次数 : 0

小编点评

**Java 中的锁机制** 锁是并发编程中用于资源共享的机制,它可以将多个线程同步访问共享资源,提高应用程序的性能。 **主要类型:** * **ReentrantLock:** Java 中常用的并发锁,它支持多线程并发操作。 * **ReentrantReadWriteLock:** 支持读写操作的并发锁,它可以并行读写操作。 * **StampedLock:** 支持乐观读写操作的并发锁,它可以优化读写操作的性能。 **使用示例:** ```java // 使用 ReentrantLock 示例 ReentrantLock lock = new ReentrantLock(); lock.lock(); // 执行需要同步的代码块 lock.unlock(); // 使用 ReentrantReadWriteLock 示例 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock(); // 读取共享资源 lock.unlockRead(); // 使用 StampedLock 示例 StampedLock lock = new StampedLock(); lock.tryOptimisticRead(); // 读取共享资源 lock.unlockRead(); lock.tryOptimisticWrite(); // 写入共享资源 lock.unlockWrite(); ``` **底层实现:** StampedLock 的底层实现通常使用乐观锁或悲观锁机制。乐观锁是一种在读操作中进行自旋的锁,它可以优化读操作的性能,但它可能导致读操作期间出现并发读操作。悲观锁是一种在写操作中进行自旋的锁,它可以确保写操作的原子性,但它可能导致写操作期间出现并发写操作。 ** AQS (原子共享锁):** AQS 是 StampedLock 的升级版本,它使用一种称为 CAS 的算法来优化读写操作的性能。 CAS 是原子操作的组合,它可以确保读操作和写操作都是原子执行的。当读操作使用 CAS 时,它可以并行读取共享资源,而当写操作使用 CAS 时,它可以并行写入共享资源。

正文

Java 中的锁(Locking)机制主要是为了解决多线程环境下,对共享资源并发访问时的同步和互斥控制,以确保共享资源的安全访问。

锁的作用主要体现在以下几个方面:

  1. 互斥访问:确保在任何时刻,只有一个线程能够访问特定的资源或执行特定的代码段。这防止了多个线程同时修改同一资源导致的数据不一致问题。
  2. 内存可见性:通过锁的获取和释放,可以确保在锁保护的代码块中对共享变量的修改对其他线程可见。这是因为 Java 内存模型(JMM)规定,对锁的释放会把修改过的共享变量从线程的工作内存刷新到主内存中,而获取锁时会从主内存中读取最新的共享变量值。
  3. 保证原子性:锁能够保证在其保护的代码块内,一系列操作是不可分割的整体,即原子操作。这意味着在多线程环境下,这些操作不会被线程调度机制打断,从而避免了数据的不完整修改。
  4. 同步:协调线程间的执行顺序,使得某些操作在另一些操作完成之后再执行,保证程序的逻辑正确性。例如,一个线程在写入数据之后,另一个线程才能读取该数据,以确保读取到的数据是最新的。

1.锁策略

在 Java 中有很多锁策略,用于对锁进行分类和指导锁的(具体)实现,这些锁策略包括以下内容:

  1. 乐观锁:它基于一种乐观的思想,即认为数据一般情况下不会造成冲突,所以不会立即加上锁,而是在数据进行更新提交的时候再进行检查。如果发生冲突,则返回错误信息,让用户决定如何去做。
  2. 悲观锁:它总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  3. 自旋锁:如果持有锁的线程能在很短时间内释放锁,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋就是空循环),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
  4. 可重入锁(递归锁):指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获得该锁的代码。即,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
  5. 读写锁:在读写场景中,读操作可以并发进行,但写操作需要互斥进行。通过读写锁可以实现读写分离,提高系统的并发性能。
  6. 公平锁/非公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先到先得。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
  7. 共享锁/独占锁:共享锁允许多个线程同时读取一个资源,而独占锁则只允许一个线程访问资源。
  8. 轻量级锁/重量级锁:这些是 Java 在 JVM 层面对 synchronized 锁的优化,以减少线程之间的竞争和提高程序的性能。
  9. 分段锁:将一把锁分成多段,允许不同的线程同时访问不同的段,从而提高了并发访问的性能。
  10. 同步锁:Java 内建的一种同步机制,例如 synchronized,它可以修饰方法或代码块,用于保护共享资源的访问。

2.锁实现

在 Java 中也有一些具体的锁实现,用于代码层面的锁操作以此来保证线程安全的,这些常见的锁实现有以下几个:

  1. synchronized:内置锁(Monitor Lock),可以用于方法或代码块,提供互斥访问。当一个线程进入 synchronized 方法或块时,它会自动获取对象的锁,其他线程则需等待锁释放后才能进入。
  2. ReentrantLock:是一个重入锁,是 java.util.concurrent.locks 包中的接口 Lock 的实现,提供了比 synchronized 更灵活的锁操作,如尝试获取锁、可中断的获取锁、超时获取锁等。它也支持公平锁和非公平锁策略。
  3. ReentrantReadWriteLock(读写锁):也是 java.util.concurrent.locks 包中的一部分,允许同时有多个读取者,但只允许一个写入者。它分为读锁和写锁,读锁之间不互斥,读锁与写锁互斥,写锁之间也互斥,适用于读多写少的场景。
  4. StampedLock(Java 8 引入):提供了三种锁模式:读锁、写锁和乐观读锁。相较于 ReentrantReadWriteLock,StampedLock 提供了更细粒度的控制,支持乐观读取操作,可以提高并发性能。

2.1 synchronized 使用

synchronized 可以用来修饰普通方法、静态方法和代码块

① 修饰普通方法

public synchronized void method() {
    // .......
}

当 synchronized 修饰普通方法时,被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象。

② 修饰静态方法

public static synchronized void staticMethod() {
    // .......
}

当 synchronized 修饰静态的方法时,其作用的范围是整个方法,作用对象是调用这个类的所有对象。

③ 修饰代码块

为了减少锁的粒度,我们可以选择在一个方法中的某个部分使用 synchronized 来修饰(一段代码块),从而实现对一个方法中的部分代码进行加锁,实现代码如下:

public void classMethod() throws InterruptedException {
    // 前置代码...
    
    // 加锁代码
    synchronized (SynchronizedExample.class) {
        // ......
    }
    
    // 后置代码...
}

以上代码在执行时,被修饰的代码块称为同步语句块,其作用范围是大括号“{}”括起来的代码块,作用的对象是调用这个代码块的对象。

2.2 ReentrantLock 使用

ReentrantLock 基本使用:

// 1. 创建ReentrantLock对象
ReentrantLock lock = new ReentrantLock();
// 2.获取锁
lock.lock(); 
try {
    // 3.得到锁,执行需要同步的代码块
} finally {
    // 4.释放锁
    lock.unlock(); 
}

进阶使用:尝试获取锁并设定超时时间(可选):

ReentrantLock lock = new ReentrantLock();
 // 尝试获取锁,等待2秒,超时返回false
boolean locked = lock.tryLock(2, TimeUnit.SECONDS);
if (locked) {
    try {
        // 执行需要同步的代码块
    } finally {
        lock.unlock();
    }
}

2.3 ReentrantReadWriteLock 使用

ReentrantReadWriteLock 特点如下:

  1. 多个线程可以同时获取读锁,实现读共享的并发访问。
  2. 写锁是排它的,一旦有一个线程获取写锁,其他线程无法获取读锁或写锁,直到写锁释放。
  3. 读锁与读锁之间可以共存,但写锁与读锁和写锁之间是互斥的。

也就是说:读读不互斥、读写互斥、写写互斥。

ReentrantReadWriteLock 基础使用如下:

// 创建 ReentrantReadWriteLock 对象
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 创建读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
// 获取读锁
readLock.lock(); 
try {
    // 读取共享资源的操作
} finally {
    // 释放读锁
    readLock.unlock(); 
}
// 创建写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 获取写锁
writeLock.lock();
try {
    // 写入共享资源的操作
} finally {
    // 释放写锁
    writeLock.unlock(); 
}

2.4 StampedLock 使用

StampedLock 有三种读写方法:

  • readLock:读锁,用于多线程并发读取共享资源。
  • writeLock:写锁,用于独占写入共享资源。
  • tryOptimisticRead:读乐观锁,用于在不阻塞其他线程的情况下尝试读取共享资源。

其中 readLock() 和 writeLock() 方法与 ReentrantReadWriteLock 的用法类似,而 tryOptimisticRead() 方法则是 StampedLock 引入的新方法,它用于非常短的读操作,它是使用如下:

// 创建 StampedLock 实例
StampedLock lock = new StampedLock();
// 获取乐观读锁
long stamp = lock.tryOptimisticRead(); 
// 读取共享变量
if (!lock.validate(stamp)) { // 检查乐观读锁是否有效
    stamp = lock.readLock(); // 如果乐观读锁无效,则获取悲观读锁
    try {
        // 重新读取共享变量
    } finally {
        lock.unlockRead(stamp); // 释放悲观读锁
    }
}

// 获取悲观读锁
long stamp = lock.readLock(); 
try {
    // 读取共享变量
} finally {
    lock.unlockRead(stamp); // 释放悲观读锁
}

// 获取写锁
long stamp = lock.writeLock(); 
try {
    // 写入共享变量
} finally {
    lock.unlockWrite(stamp); // 释放写锁
}

使用乐观读锁的特性可以提高读操作的并发性能,适用于读多写少的场景。如果乐观读锁获取后,在读取共享变量前发生了写入操作,则 validate 方法会返回 false,此时需要转换为悲观读锁或写锁重新访问共享变量。

课后思考

StampedLock 底层是如何实现的?什么是 AQS?

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

与字节面试:说说Java中的锁机制?相似的内容:

字节面试:说说Java中的锁机制?

Java 中的锁(Locking)机制主要是为了解决多线程环境下,对共享资源并发访问时的同步和互斥控制,以确保共享资源的安全访问。 锁的作用主要体现在以下几个方面: 互斥访问:确保在任何时刻,只有一个线程能够访问特定的资源或执行特定的代码段。这防止了多个线程同时修改同一资源导致的数据不一致问题。 内

秋招还没Offer怎么办?

如果你是双非院线、没有实习经历、没有出众的技术(算法没刷一千道,也没做过 Spring Cloud 项目)、现在还没有面试(或只有少量的面试)、并且目前还没有 Offer,那么恭喜你,你和目前大部分同学的状态是一样的。 相信我,你并不孤单。 有人会说:“瞎扯,你去看牛客,别人都在为选阿里还是字节而发

[转帖]面渣逆袭:二十二图、八千字、二十问,彻底搞定MyBatis!

https://cdn.modb.pro/db/334793 大家好,我是老三,面渣逆袭系列继续,这节我们的主角是MyBatis,作为当前国内最流行的ORM框架,是我们这些crud选手最趁手的工具,赶紧来看看面试都会问哪些问题吧。 基础 1.说说什么是MyBatis? MyBatis logo 先吹

面试官:字节流可以处理一切文件为什么还需要字符流呢?

一、写在开头 在计算机领域中百分之九十以上的程序拥有着和外部设备交互的功能,这就是我们常说的IO(Input/Output:输入/输出),所谓输入就是外部数据导入计算机内存中的过程,输出则是将内存或者说程序中的数据导入到外部存储中,如数据库、文件以及其他本地磁盘等。 二、什么是IO流 这种输入输出往

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

自己理解的TCP三次握手

### TCP 三次握手过程是怎样的? TCP的建立连接是通过三次握手来进行的。三次握手的过程如下图: 说实话这个很好理解,我称之为N字型 首先我们理解到建立连接是一个虚的概念了对吧?那么我们来设计一个可靠的TCP,首先建立连接是必须的吧?相当于我们打电话,总要先说一句喂 wei?(面向连接正是这个

字节面试:MySQL自增ID用完会怎样?

在一些中小型项目开发中,我们通常会使用自增 ID 来作为主键的生成策略,但随着时间的推移,数据库的信息也会越来越多,尤其是使用自增 ID 作为日志表的主键生成策略时,可能很快就会遇到 ID 被用完的情况,那么如果发生了这种情况,MySQL 又会怎样执行呢? PS:当然,在分库分表的场景中,我们通常会

研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?

一、写在开头 今天和一个之前研二的学妹聊天,聊及她上周面试字节的情况,着实感受到了Java后端现在找工作的压力啊,记得在18,19年的时候,研究生计算机专业的学生,背背八股文找个Java开发工作毫无问题,但现在即便你是应届生,问的考题也非常的深入和细节了,只会背八股,没有一定的代码量和项目积累,根本

3年Java阿里跳字节的面试心得总结

中厂->阿里->字节,成都->杭州->成都 系列文章目录和关于我 0.前言 笔者在不足两年经验的时候从成都一家金融科技中厂跳槽到杭州阿里淘天集团,又于今年5月份从杭州淘天跳槽到成都字节。自认为自己在面试这方面有一点心得,处于记录和分享的目的便有了此文,此文纯主观,也许对3年社招的同学有所帮助。 本文

[转帖]图解:什么是红黑树?

https://zhuanlan.zhihu.com/p/273829162 注:本文比较硬核但是很值得大家花心思看完,看完你一定会有所收获的 红黑树是面试中一个很经典也很有难度的知识点,网传字节跳动面试官最喜欢问这个问题。很多人会觉得这个知识点太难,不想花太多功夫去了解,也有人会认为这个数据结构在