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

java锁

简介

java多线程中可能出现数据不一致问题,而锁在对于多线程执行是必不可少的,且单体项目(部署到一台机器)和部署多台机器上锁的用法是不一样的

什么是锁:

锁用于控制多线程对共享资源的访问,保证同一时刻只有一个线程能够访问共享资源。它的基本作用是:

**保证线程安全:**避免多个线程同时对共享数据进行修改,导致数据不一致或程序错误。
**数据一致性:**加锁确保了在加锁范围内的代码只能被一个线程执行,从而保证数据的一致性。

为什么要加锁

**多线程竞争:**在多线程环境下,多个线程可能同时尝试访问或修改共享资源。如果不加锁,就可能会出现线程之间的竞态条件(race condition),导致数据不一致或者错误的执行结果。
**保证原子性:**锁确保一段代码在执行时,其他线程无法中断或同时执行。

单体项目:

例如一个Springboot项目部署到一台服务器上

加锁方式

一般加锁方式
1.synchronzied关键字
synchronized 是一个 关键字,使用C++实现的,没办法控制锁的开始、锁结束,也没办法中断线程的执行
2.lock接口
是 java层面的实现,可以获取锁的状态,开启锁,释放锁,通过设置可以中断线程的执行,更加灵活
是否自动是否锁:synchronized 会自动是否锁,而 lock 需要手动调用unlock 方法释放,否则会死循环,我们平时加的锁是ReentrantLock实现lock接口

在这里插入图片描述

synchronzied(内置关键字加锁)

synchronzied可以用于一下场景:

加在实例方法上

实例方法上的 synchronized 锁的是 当前实例对象。也就是说,只有在同一个实例上调用该方法时,多个线程会争夺锁。如果有多个不同的实例,每个实例的锁是独立的,不会互相影响。多个线程在操作不同实例时,互不干扰。

public synchronized void instanceMethod() {
    // 同步代码
}

加在静态方法上

静态方法上的 synchronized 锁的是 类对象(Class 对象)。也就是说,同一个类的所有实例在访问该静态方法时会争夺同一把锁,不管你创建多少个实例,静态方法的同步机制总是锁住 类的 Class 对象。

public static synchronized void staticMethod() {
    // 同步代码
}

加在代码块

实际上是对代码块中实例加锁

public class SynchronizedBlockExample {
    private int counter;
    private Object lock = new Object();
 
    public void increment() {
        synchronized(lock) {
            counter++;
        }
    }
 
    public int getCount() {
        synchronized(lock) {
            return counter;
        }
    }
}

这里是对lock加锁,lock是属于SynchronizedBlockExample 这个类的,所有当多个线程操作同一个SynchronizedBlockExample 类实例,执行increment操作时候会对lock加锁,其他线程执行代码块里面的方法的时候需要看lock锁释放没有

this

代码块中this和在类中定义一个对象实例的区别

public class SynchronizedBlockExample {
    private int counter;
	private Object lock = new Object();
	
    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public int getCount() {
        synchronized (this) {
            return counter;
        }
    }
}

使用 this 会锁住 当前对象实例,确保同一个实例的同步代码块在同一时刻只有一个线程可以访问,多个线程访问同一实例时会发生锁竞争。
这种方式和 synchronized(lock) 在功能上是 等价的,也就是说,它们会表现得一样,锁住的是当前对象实例。

那如果创建一个不是Object对象的实例呢(Integer lock2=new Integer())
public class SynchronizedBlockExample {
    private int counter;
	private Integer lock2 = new Integer();
	
    public void increment() {
        synchronized (lock2 ) {
            counter++;
        }
    }

    public int getCount() {
        synchronized (lock2 ) {
            return counter;
        }
    }
}

使用 new Integer(0) 创建的 lock2 对象作为锁是 有效的,并且它与 this 或 lock 在逻辑上没有本质区别——它仅仅是一个替代的 锁对象。
lock2 作为一个锁对象,会确保同步代码块在执行时只有一个线程能够访问。

潜在的问题:

性能问题:使用 new Integer(0) 每次创建一个新的对象作为锁,这在性能上会有所浪费,尤其是在多线程情况下,频繁创建新的对象可能会引入不必要的开销。

使用 Integer 作为锁不推荐:虽然可以使用任意对象作为锁,但 Java 提倡使用 Object 或 私有的、专门用于锁定的对象(如 lock)。Integer 是一个不可变对象,使用它作为锁并不直观,容易导致不必要的误解。特别是对于 常量 或 基础数据类型的包装类,如果有不小心的地方(如对 Integer 对象进行了不可预料的共享),可能会引发错误。

Integer 是不可变对象:虽然在实际场景中,Integer 作为锁对象通常是安全的,但在某些特殊情况下,Integer 作为锁的行为不如 Object 直观,因为它是不可变的。通过 new Integer(0) 创建的新对象可以作为锁对象,但是它没有额外的语义,难以表明它的真正目的(即作为锁对象)。

synchronzied是可重入锁吗

可重入锁(Reentrant Lock) 的含义是,如果一个线程已经获得了锁,它可以再次进入该锁而不会被阻塞。这种锁的特性允许同一个线程多次获取同一个锁。

为什么 synchronized 是可重入锁?

当一个线程进入一个 synchronized 方法或代码块时,如果它再次请求进入同一个锁(例如,调用同一个对象的另一个 synchronized 方法),它可以成功获得该锁,而不会导致死锁。
synchronized 锁内部会维护一个锁的计数器(重入计数器),每当同一线程再次请求锁时,计数器会增加,直到线程释放锁时,计数器减少。当计数器减到零时,锁被完全释放。

例如:

class ReentrantLockExample {
    synchronized void methodA() {
        System.out.println("Entering methodA");
        methodB();  // 线程可以再次进入 methodB,因为它已经持有锁
    }
    
    synchronized void methodB() {
        System.out.println("Entering methodB");
    }
}

synchronized 是非公平锁吗?

是的
非公平锁 指的是线程在竞争锁时,不保证按照请求的顺序(即先来先得)获取锁。当一个线程尝试获取锁时,即使队列中其他线程排队等待,它也可能直接获取锁,而不管它在队列中的顺序。

为什么 synchronized 是非公平锁?

synchronized 的实现机制是基于 偏向锁、轻量级锁 和 重量级锁 的,这些机制并没有显式地保证线程的公平性。
在高并发情况下,当多个线程竞争同一锁时,后到的线程有可能直接获取到锁,而不需要按照先到先得的顺序排队。
锁的竞争过程是“抢占式”的,即线程尝试获得锁时,如果锁空闲,它会立刻成功获取,而不等待前面的线程。

lock(ReentrantLock)加锁

Lock 接口的主要实现类是 ReentrantLock,它提供了与 synchronized 类似的基本功能,但具有更多的可控制性,如可中断、可公平性、定时锁等。

关键方法:
lock():获取锁。如果锁不可用,调用线程会被阻塞,直到获得锁。
unlock():释放锁。释放后,其他等待锁的线程可以获取锁。
tryLock():尝试获取锁。如果锁不可用,立即返回 false,而不阻塞。
lockInterruptibly():获取锁,但如果线程在等待过程中被中断,它会抛出 InterruptedException。
newCondition():创建一个 Condition 对象,用于线程间通信,类似于 Object.wait() 和 Object.notify()。

实现方式

底层实现原理

Lock的底层是 AQS+CAS机制 实现。
默认是非公平锁,可设置为公平锁

CAS

这里的CAS机制,主要是指一个线程获取到锁之后,会将state变量+1,这里的+1操作底层就是用CAS实现的。

不太了解CAS的可以看看这篇文章

java CAS

AQS

AQS是AbstractQueuedSynchronizer的缩写,是一个抽象类,属于JUC的一个基类,JUC下的很多内容都是通过AQS实现的,例ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。

AQS在内部维护了2个数据结果
1.由volatile修饰的state成员变量,保证了可见性和有序性(禁止指令重排)
2.双向链表Node头节点和尾节点

AQS参考下面这篇文章
java AQS

state

当一个线程

双向链表

这里双向链表存储的是每个线程

为什么是双向链表

主要原因是它可以高效地管理线程的排队和唤醒

1. 线程的前后依赖关系:
当一个线程阻塞在获取锁时,它的状态和位置会被记录在队列中。双向链表可以轻松地维护线程之间的前后关系。

如果某个线程需要被唤醒,它通常会需要查看排队中的 前驱线程 或 后继线程,以决定唤醒顺序。双向链表通过 前驱指针 和 后继指针 使得这种操作变得容易。
线程可能会因 条件变量 或 信号量 等条件被唤醒,双向链表能够灵活地管理线程的等待和唤醒。
2. 线程的公平性控制:
在公平锁模式下,AQS 使用同步队列来确保线程按顺序获取锁(先来先服务)。双向链表支持 FIFO(先进先出) 的排队机制,便于实现公平性策略。

前驱和后继 指针帮助 AQS 在唤醒线程时遵循队列中的顺序,避免出现饥饿现象(即某些线程始终无法获取锁)。
如果一个线程被唤醒后,它会检查自己的 后继线程,以便唤醒队列中排队最久的线程。
3. 线程的插入与移除效率:
线程在等待队列中的 插入 和 移除 操作是非常频繁的,双向链表在这方面比单向链表更具优势:

当一个线程进入队列时,AQS 会将它插入到队列的尾部,操作相对简单且高效。
线程退出队列时,它可能是 队头线程 或者 队尾线程,双向链表可以迅速移除头尾节点。
双向链表不需要像单向链表那样遍历整个链表来找到节点,能更高效地进行节点的插入和删除操作。
4. 支持中断、等待和超时操作:
双向链表能帮助 AQS 实现线程的中断、等待和超时控制:

中断线程:当线程在等待队列中被中断时,可以快速找到线程在队列中的位置并将其从队列中移除。
等待超时:当线程的等待时间到达最大限制时,双向链表能够帮助它快速检查并从队列中移除。

ReentranLock是非公平锁吗

默认非公平锁,可以创建对象的时候设置为公平锁
在这里插入图片描述

非公平性体现在那里

当线程尝试获取锁时,并不是 严格按照队列的顺序 来获取锁。非公平锁 允许一个 正在等待的线程 在它的前面排队的线程还没有尝试重新获取锁时,直接抢占锁。
也就是说,如果 队列头的线程 还没有尝试获取锁(它可能正在等待某个条件),其他排队的线程 可以绕过它,直接尝试获取锁。

集群项目

有时候咱们项目的扩大,导致单台服务器CPU压力过大,这时候就会采用集群的方法将同一个springboot项目部署到多台机器,利用负载均衡分担压力

集群项目多线程问题

如果这时候前端用户有同时调用了一个接口,其中这个接口有个共享变量


http://www.kler.cn/a/460710.html

相关文章:

  • 小程序学习07—— uniapp组件通信props和$emit和插槽语法
  • WebSocket 的封装使用
  • Golang的代码质量分析工具
  • h5页面在安卓手机被软键盘弹起引起的bug
  • 【微软,模型规模】模型参数规模泄露:理解大型语言模型的参数量级
  • springboot集成qq邮箱服务
  • FFmpeg 编码和解码
  • 《Java编程入门官方教程》第十六章练习答案
  • 【Spring MVC 核心概念】揭秘概念和整体架构
  • 图表控件Aspose.Diagram入门教程(7):在 C# 中删除 Visio 形状保护
  • OpenStack系列第三篇:CentOS7 上部署 OpenStack(Train版)集群教程 Ⅳ Dashboard Cinder 服务部署
  • 低代码/无代码开发平台下的电商API接口创新应用
  • Microsoft 365 Copilot模型多元化,降低对OpenAI依赖并降低成本
  • gitlab 还原合并请求
  • JVM调优(内存、GC、JVM参数)
  • 《庐山派从熟悉到...》Sensor 模块(摄像头)基础设置
  • html+css+js网页设计 美食 美食杰8个页面
  • AviatorScript
  • 数据结构-排序思想
  • 1月第一讲:WxPython跨平台开发框架之前后端结合实现附件信息的上传及管理
  • 【MyBatis】MyBatis项目的创建、配置和启动
  • 异步请求在TypeScript网络爬虫中的应用
  • docker Oracle设置rman自动备份步骤
  • Linux jupyter notebook Matplotlib 无法显示汉字
  • 企业储能电站 储能配电柜监测管理系统
  • 基于微信小程序的校园点餐平台的设计与实现(源码+SQL+LW+部署讲解)