当前位置: 首页 > news >正文

Java中三种重要的锁

1.Synchornized对象锁

一般我们要解决临界区中的静态条件发生,我们一般使用两种手段

  1. 阻塞式解决方案:synchronized lock
  2. 非阻塞式解决方案:  原子类

下面我们着重讲解阻塞式解决方案:synchornized

synchronized俗称对象锁,采用互斥的方案让同一时刻至多只有一个线程能持有对象锁,其他线程想要获取这个对象锁就会被阻塞住,这样就保证拥有锁的线程可以安全执行临界区内的代码,不用担心线程上下文切换导致的并发问题

synchronized(对象){//线程1执行,线程2阻塞//临界区}

synchronized(对象)中的对象,可以想象成一个房间(room),有唯一的入口(房门),房间只能一次进入一个人,而我们的线程t1和线程t2就是两个人

当线程1执行到synchronized(room)时就好比t1进入了房间,锁住了门拿走了钥匙,在门内执行临界区代码

如果t2也走到了synchronized时,会发现门被锁住了,就会在门外等待(阻塞),这中间即使t1的cpu时间片不幸被用完,被提出了门外,但是由于钥匙还在它身上,因此t2还是进不来,必须等到t1来执行完代码然后把锁打开放出钥匙才能执行。

语法:

class Test{public synchornized void test(){}
}
//等价于
class Test{public void test(){sychronized(this){} }
}
class Test{public synchornized statsic void test(){}
}
//等价于
class Test{public static void test(){sychronized(Test.class){}}}

sychornized底层

Monitor对象头

其中的状态会根据锁的升级而改变

Monitor工作原理

当一个线程进入到sychronized中时,Monitor内部会发生改变,如果发现Monitor中的Owner指向了别的线程,就会进入EntryList阻塞队列进行等待。当线程执行完了以后,WaitSet就会指向这个线程,然后Thread就会让出锁,Owner就会指向其他阻塞队列中的线程

如何判断是否获取锁:由Monitor中的Owner来判定,如果Owner指向的线程为进来的线程就说明拥有这把锁.

Sychornized优化

1.轻量级锁

如果一个对象虽然有多个线程访问,但是访问的时间是错开的,也就是没有竞争,那么可以使用轻量级锁来优化

轻量级锁对使用者是透明的,既语法仍然是synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1(){synchronized(obj){//同步块Amethod2()}}public static void method2(){synchronized(obj){//同步块B}}

此时由于一个锁有两个线程获取锁,但是没有发生竞争,因为第二个方法里的锁以及在方法1中获取了,所以此时的锁会升级为轻量级锁

2.锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁升级为重量级锁

static Object obj = new Object();
public static void method1(){synchronized(obj){//同步块}}

当Thread-1进行轻量级加锁时,Thread-0以及对该对象加了轻量级锁

这时Thread1加轻量级锁失败,进入锁膨胀流程

  • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
  • 然后自己进入Monitor的EntryList BLOCKED

当Thread0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败。这时就会进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

自旋优化

偏向锁

只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归改线程所有

重量级锁

重量级锁是 synchronized 在处理激烈线程竞争时使用的同步机制。它依赖于操作系统的互斥量(mutex) 实现,线程的阻塞与唤醒需要通过操作系统内核态的介入,因此开销较大,故称为 “重量级”。

当多个线程频繁竞争同一把锁,且轻量级锁的自旋优化(见下文)无法解决竞争时,锁会膨胀为重量级锁,此时线程会进入阻塞状态,放弃 CPU 资源,等待被唤醒后重新竞争锁。

优点
  • 能稳定处理激烈竞争场景,确保多线程同步的正确性(原子性、可见性、有序性)。
缺点
  • 性能开销大:线程的阻塞与唤醒需要从用户态切换到内核态(上下文切换),而内核态操作的耗时远高于用户态(通常是微秒级 vs 纳秒级)。
  • 效率低:阻塞的线程无法参与 CPU 调度,可能导致资源利用率下降。

2.ReentrantLock可重入锁

ReentrantLock相对于synchronized有如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁(先进先出)
  • 支持多个条件变量(可以对变量细分,如notify可以细分唤醒变量,而synchronized就是直接全部唤醒)

与synchronized一样,都支持可重入

基本语法

和synchronized不一样,reentrantlock是对象而非方法,在使用前需要先new一个

//获取锁
reentrantlock.lock();
try{//临界区} finally{//释放锁reentrantlock.unlock();    }

可重入

如下图,我们两个方法都用Reentrantlock加锁,这样它们每次获取都是获取的同一把锁,相当于重入了同一把锁

public void outerMethod() {lock.lock();try {System.out.println("获取外层锁");innerMethod(); // 调用内层方法} finally {lock.unlock();System.out.println("释放外层锁");}}public void innerMethod() {lock.lock();try {System.out.println("获取内层锁");System.out.println("当前锁的持有次数: " + lock.getHoldCount());} finally {lock.unlock();System.out.println("释放内层锁");}}

可打断

当一个线程正在等待获取锁时,可以被其他线程通过interrupt()方法中断,从而避免无限等待。这是ReentrantLock相比synchronized的一个重要优势,后者在等待锁时无法被中断。

ReentrantLock提供了两种获取锁的方式:

  1. 不可打断模式:使用lock()方法获取锁。若锁被其他线程持有,当前线程会进入阻塞状态,且无法被中断,直到锁被释放。
  2. 可打断模式:使用lockInterruptibly()方法获取锁。若锁被其他线程持有,当前线程会进入阻塞状态,但可以被其他线程中断(通过调用Thread.interrupt())。

如下代码所示:

// 线程1:持有锁并休眠5秒Thread t1 = new Thread(() -> {lock.lock();try {System.out.println("线程1获取锁,开始执行...");Thread.sleep(5000); // 模拟长时间持有锁} catch (InterruptedException e) {System.out.println("线程1被中断");} finally {lock.unlock();System.out.println("线程1释放锁");}});// 线程2:使用lockInterruptibly()获取锁Thread t2 = new Thread(() -> {try {System.out.println("线程2尝试获取锁(可打断模式)");lock.lockInterruptibly();try {System.out.println("线程2获取锁,开始执行");} finally {lock.unlock();System.out.println("线程2释放锁");}} catch (InterruptedException e) {System.out.println("线程2被中断,放弃获取锁");Thread.currentThread().interrupt(); // 恢复中断状态}});

公平锁与非公平锁(Fairness)

  • 公平锁:线程按照请求锁的顺序获取锁(FIFO),避免 “线程饥饿”。
  • 非公平锁:允许插队,锁释放时任何线程都可能竞争获取锁,性能更高

创建方式

ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
  • 公平锁:适用于需要保证线程执行顺序的场景(如资源分配)。
  • 非公平锁:适用于高并发场景,减少线程切换开销。

 条件变量(Condition)

提供更灵活的线程等待 / 通知机制,替代synchronizedwait()/notify()

使用方式

  • lock.newCondition():创建与锁绑定的条件变量。
  • condition.await():线程等待,释放锁。
  • condition.signal()/signalAll():唤醒等待的线程
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();// 生产者线程
lock.lock();
try {while (queue.isFull()) {notFull.await(); // 队列满,等待}queue.add(item);notEmpty.signal(); // 通知消费者
} finally {lock.unlock();
}// 消费者线程
lock.lock();
try {while (queue.isEmpty()) {notEmpty.await(); // 队列空,等待}queue.take();notFull.signal(); // 通知生产者
} finally {lock.unlock();
}

Volatile

Volatile原理

volatile的底层实现原理是内存屏障,Memory Barrier

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

Java 内存模型规定了线程之间的变量访问规则:

  • 主内存:所有变量的原始值存储在此。
  • 工作内存:每个线程有自己的工作内存,保存了从主内存拷贝的变量副本。

可见性问题:当线程 A 修改了变量 X 的值,可能只更新了自己的工作内存,而未及时同步到主内存;此时线程 B 读取变量 X 时,仍使用自己工作内存中的旧值。

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public class VolatileExample {private volatile boolean flag = false;public void writer() {flag = true; // 写屏障:确保flag=true立即刷新到主内存}public void reader() {while (!flag) { // 读屏障:确保每次读取flag时都从主内存获取// 循环等待}System.out.println("flag is now true");}
}

读写屏障如何保证有序性

编译器和处理器为了优化性能,可能会对指令进行重排序。但volatile变量的读写操作不会被重排序:

  • 写操作前的指令不会被重排序到写操作之后。
  • 读操作后的指令不会被重排序到读操作之前。
int a = 0;
volatile boolean flag = false;public void init() {a = 1;         // 1flag = true;   // 2(写操作禁止重排序到1之前)
}public void use() {if (flag) {    // 3(读操作禁止重排序到4之后)int b = a; // 4}
}

http://www.dtcms.com/a/273620.html

相关文章:

  • spring boot 详解以及原理
  • 界面设计工具——MasterGo莫高设计
  • 【UE教程/进阶】Slate链式编辑原理
  • 解决echarts数据更新了图表不更新
  • GPU 降成本免运维,睿观 AI 助手选择函数计算
  • 打破信息壁垒!可视化如何让交通数据 “开口说话”
  • C#元组:从基础到实战的全方位解析
  • 人脸图像生成(DCGAN)
  • Qt数据库编程详解:SQLite实战指南
  • Vue 3 中父组件内两个子组件相互传参的几种方法
  • Vue 3 入门——自学习版本
  • DOM编程实例(不重要,可忽略)
  • 分享|2025年机器学习工程师职业技术证书报考指南
  • 论容器化 | 分析Go和Rust做医疗的后端服务
  • 在vue中使用Three.js渲染FBX模型
  • arcgis api for js 设置地图服务请求带有请求头信息
  • 录音实时上传
  • uniapp
  • Claude Code是什么?国内如何使用到Claude Code?附国内最新使用教程
  • 基于定制开发开源AI智能名片与S2B2C商城小程序的旅游日志创新应用研究
  • uniapp小程序tabbar跳转拦截与弹窗控制
  • Elasticsearch混合搜索深度解析(上):问题发现与源码探索
  • Excel 转 JSON by WTSolutions API 文档
  • 较为深入的了解c++中的string类(2)
  • MyBatis 从入门到实战:代理 Dao 模式下的 CRUD 全解析
  • Netplan 配置网桥(Bridge)的模板笔记250711
  • excel如何只保留前几行
  • 提示工程:解锁大模型潜力的核心密码
  • 基于redis的分布式session共享管理之销毁事件不生效问题
  • 这个方法的目的是检查一个给定的项目ID(projectId)是否在当前数据库中被使用(搜索全库)