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

Java 中的锁(Locking)机制主要是为了解决多线程环境下,对共享资源并发访问时的同步和互斥控制,以确保共享资源的安全访问。
锁的作用主要体现在以下几个方面:
- 互斥访问:确保在任何时刻,只有一个线程能够访问特定的资源或执行特定的代码段。这防止了多个线程同时修改同一资源导致的数据不一致问题。
- 内存可见性:通过锁的获取和释放,可以确保在锁保护的代码块中对共享变量的修改对其他线程可见。这是因为 Java 内存模型(JMM)规定,对锁的释放会把修改过的共享变量从线程的工作内存刷新到主内存中,而获取锁时会从主内存中读取最新的共享变量值。
- 保证原子性:锁能够保证在其保护的代码块内,一系列操作是不可分割的整体,即原子操作。这意味着在多线程环境下,这些操作不会被线程调度机制打断,从而避免了数据的不完整修改。
- 同步:协调线程间的执行顺序,使得某些操作在另一些操作完成之后再执行,保证程序的逻辑正确性。例如,一个线程在写入数据之后,另一个线程才能读取该数据,以确保读取到的数据是最新的。
1.锁策略
在 Java 中有很多锁策略,用于对锁进行分类和指导锁的(具体)实现,这些锁策略包括以下内容:
- 乐观锁:它基于一种乐观的思想,即认为数据一般情况下不会造成冲突,所以不会立即加上锁,而是在数据进行更新提交的时候再进行检查。如果发生冲突,则返回错误信息,让用户决定如何去做。
- 悲观锁:它总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- 自旋锁:如果持有锁的线程能在很短时间内释放锁,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋就是空循环),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
- 可重入锁(递归锁):指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获得该锁的代码。即,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
- 读写锁:在读写场景中,读操作可以并发进行,但写操作需要互斥进行。通过读写锁可以实现读写分离,提高系统的并发性能。
- 公平锁/非公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先到先得。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
- 共享锁/独占锁:共享锁允许多个线程同时读取一个资源,而独占锁则只允许一个线程访问资源。
- 轻量级锁/重量级锁:这些是 Java 在 JVM 层面对 synchronized 锁的优化,以减少线程之间的竞争和提高程序的性能。
- 分段锁:将一把锁分成多段,允许不同的线程同时访问不同的段,从而提高了并发访问的性能。
- 同步锁:Java 内建的一种同步机制,例如 synchronized,它可以修饰方法或代码块,用于保护共享资源的访问。
2.锁实现
在 Java 中也有一些具体的锁实现,用于代码层面的锁操作以此来保证线程安全的,这些常见的锁实现有以下几个:
- synchronized:内置锁(Monitor Lock),可以用于方法或代码块,提供互斥访问。当一个线程进入 synchronized 方法或块时,它会自动获取对象的锁,其他线程则需等待锁释放后才能进入。
- ReentrantLock:是一个重入锁,是 java.util.concurrent.locks 包中的接口 Lock 的实现,提供了比 synchronized 更灵活的锁操作,如尝试获取锁、可中断的获取锁、超时获取锁等。它也支持公平锁和非公平锁策略。
- ReentrantReadWriteLock(读写锁):也是 java.util.concurrent.locks 包中的一部分,允许同时有多个读取者,但只允许一个写入者。它分为读锁和写锁,读锁之间不互斥,读锁与写锁互斥,写锁之间也互斥,适用于读多写少的场景。
- 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 特点如下:
- 多个线程可以同时获取读锁,实现读共享的并发访问。
- 写锁是排它的,一旦有一个线程获取写锁,其他线程无法获取读锁或写锁,直到写锁释放。
- 读锁与读锁之间可以共存,但写锁与读锁和写锁之间是互斥的。
也就是说:读读不互斥、读写互斥、写写互斥。
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中的锁机制 synchronized & 偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景 & AtomicReference
参考文章: http://blog.csdn.net/chen77716/article/details/6618779 目前在Java中存在两种锁机制:synchronized和Lock,Lock接 ...
- JAVA中关于锁机制
本文转自 http://blog.csdn.net/yangzhijun_cau/article/details/6432216 一段synchronized的代码被一个线程执行之前,他要先拿到执行这 ...
- Java 中的锁机制
多个进程或线程同时(或着说在同一段时间内)访问同一资源会产生并发(线程安全)问题.解决并发问题可以用锁. java的内置锁: 每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁.线程进入同步 ...
- Java中的锁机制,你真的了解吗?
学到锁说明你已经学过多线程了,只有在多线程并发的情况下才会涉及到锁,相信大家用的最多的要数synchronized了,因为这个也是最简单的,直接加在方法上就可以使一个方法同步.那么除了synchron ...
- Java中的锁机制
1.在Java中锁的分类 其实就是按照锁的特性分类的 公平锁,非公平锁 可重入锁 独享锁,共享锁 互斥锁,读写锁 乐观锁,悲观锁 分段锁 偏向锁,轻量级锁,重量级锁 自旋锁 相关资料:思维导图 使用场 ...
- 【Todo】【转载】Java中的锁机制2 - Lock
参考这篇文章 http://blog.csdn.net/chen77716/article/details/6641477 上一篇 (http://www.cnblogs.com/charlesblc ...
- Java并发指南4:Java中的锁 Lock和synchronized
Java中的锁机制及Lock类 锁的释放-获取建立的happens before 关系 锁是java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消 ...
- 深入浅出Java并发包—锁机制(一)
前面我们看到了Lock和synchronized都能正常的保证数据的一致性(上文例子中执行的结果都是20000000),也看到了Lock的优势,那究竟他们是什么原理来保障的呢?今天我们就来探讨下Jav ...
- Java并发编程:Java中的锁和线程同步机制
锁的基础知识 锁的类型 锁从宏观上分类,只分为两种:悲观锁与乐观锁. 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新 ...
- AQS:Java 中悲观锁的底层实现机制
介绍 AQS AQS(AbstractQueuedSynchronizer)是 Java 并发包中,实现各种同步组件的基础.比如 各种锁:ReentrantLock.ReadWriteLock.Sta ...
随机推荐
- ArrayList,LinkedList,Vector三者的区别
List 中元素是有序的,元素可以重复,因为该集合体有索引 ArrayList: 底层数据结构是数组,查询快,增删慢. 线程不安全,效率高. 当元素放满了后,默认以原长度的 50%+1 的长度加长集合 ...
- #线段树、树状数组#D 筹备计划
分析 首先这个位置应该是带权中位数\((\geq \frac{sum+1}{2}(奇数要加一,WA了几次了))\),但是既然有这个选择的限制, 那么要用线段树求出可选择的前驱和后继,然后用树状数组计算 ...
- Docker学习路线5:在 Docker 中实现数据持久化
Docker 可以运行隔离的容器,包括应用程序和其依赖项,与主机操作系统分离.默认情况下,容器是临时的,这意味着容器中存储的任何数据在终止后都将丢失.为了解决这个问题并在容器生命周期内保留数据,Doc ...
- C++调用Python-2:调用Python函数,返回数字
# mytest.py def hello(): print("this is test python print hello world") return 123 #includ ...
- 动态库 DLL 封装三:对dll二次封装,并将回调函数放出去,供别人调用
背景: 我需要对一个dll进行二次封装,其中有一个接口,里面的参数需要传回调函数. 需求: 这个回调函数,我需要开放出去,让别人调用我的dll时,自己写这个回调函数 示例: // 回调原型 VOID ...
- 决策树模型(4)Cart算法
Cart算法 Cart是Classification and regression tree的缩写,即分类回归树.它和前面的ID3, C4.5等算法思想一致都是通过对输入空间进行递归划分并确定每个单元 ...
- 安装pnpm 和报错解决,亲测可行
安装pnpm 和报错解决,亲测可行 pnpm 是一款磁盘空间高效的软件包管理器. 当使用 npm 或 Yarn 时,如果你有 1000个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要 ...
- mysql 必知必会整理—sql 排序与过滤[三]
前言 简单整理一下MySQL的排序与过滤. 正文 我们查询出来的结果有时候是希望进行排序的,比如说: select product_name from products order by prod_n ...
- 精通中间件测试:Asp.Net Core实战指南,提升应用稳定性和可靠性
引言 在上一章节我们实战了在Asp.Net Core中的项目实战,这一章节讲解一下如何测试Asp.Net Core的中间件. TestServer 还记得我们在集成测试中提供的TestServer吗? ...
- KubeVela 插件指南:轻松扩展你的平台专属能力
简介: 本文将会全方位介绍 KubeVela 插件的核心机制,教你如何编写一个自定义插件.在最后,我们将展示最终用户使用插件的体验,以及插件将如何融入到 KubeVela 平台,为用户提供一致的体验. ...