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

Java中的锁

一、乐观锁和悲观锁

        1、悲观锁

        悲观锁: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源-----狼性锁

        2、乐观锁

        乐观锁: 认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我

🌟概念区别
特性乐观锁(Optimistic Lock)悲观锁(Pessimistic Lock)
思想认为并发冲突很少,操作时先不加锁,提交时再检测是否有冲突。认为并发冲突很常见,操作前先加锁,确保别人无法同时修改。
实现方式版本号机制时间戳机制:更新前先比较版本,不一致则更新失败。数据库锁机制:如 SELECT...FOR UPDATE,或者 synchronized / ReentrantLock
性能影响冲突少时,性能很好;冲突多时,频繁重试会影响性能。并发高时,线程等待多,吞吐量较低。
应用场景读多写少,冲突概率低的场景。写多,冲突概率高的场景。

🔥实际案例

乐观锁:

  1. 数据库表有一个 version 字段。

  2. 取数据时,连同 version 一起读出。

  3. 更新时,使用:
     

    UPDATE table_name SET value = ?, version = version + 1 WHERE id = ? AND version = ?;
  4. 如果 version 不一致,说明有别的线程修改了,更新失败,可以重试。


悲观锁:

  1. 数据库级别:

    SELECT * FROM table_name WHERE id = ? FOR UPDATE;

    查询时就加锁,其他事务不能修改,等当前事务提交/回滚后才能解锁。

  2. Java并发:

    synchronized (obj) { // 临界区代码 }
    Lock lock = new ReentrantLock(); lock.lock();try   { // 临界区代码 } 
    finally { lock.unlock(); }


总结一句话:
  • 乐观锁:适合冲突很少的业务,提升并发性能,失败时重试。

  • 悲观锁:适合冲突很频繁的业务,直接锁住资源,确保安全但牺牲并发。

二、synchronized关键字分析

        1、阿里Java规范:

        高并发时,同步调用应该去考置锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体﹔能用对象锁,就不要用类锁。
        说明︰尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

        2、案例分析
/*** @author Guanghao Wei* @create 2023-04-10 14:57*/class Phone {public synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("------sendEmail");}public synchronized void sendSMS() {System.out.println("------sendSMS");}public void hello() {System.out.println("------hello");}
}/*** 现象描述:* 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信  共用一个对象锁* 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信  共用一个对象锁* 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件* 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件  资源没有争抢,不是同一个对象锁* 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信  共用一个类锁* 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁* 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件   一个用类锁一个用对象锁* 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁*/public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}}

         第1 第2中案例说明,只要我的一个类中有方法加了synchronized 关键字,这个synchronized 锁的并不是当前该方法,而是整个资源类,也就是说可能该类有多个方法都加了synchronized 关键字,但是多线程的环境中,只有一个线程能够进入众多加了synchronized 方法中的一个方法。然后依次排队。换句话说,某一个时间内,只能有唯一的一个线程去访问这些synchronized 方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized 方法。

        第3个案例说明,普通方法不需要竞争锁,直接执行。

        第4个案例说明,对于普通的方法加上了synchronized关键字,我们锁的是当前实例对象(this),如果有两个不同的实例对象,等于说不同的资源,所以也不会竞争锁。

        第5 第6种案例说明,如果在静态方法上加锁,那么锁的是类,也就是说不管你有多少个实例,但其实都是同一个类,所以不同的实例也会被锁住。

        第7 第8种案列说明,静态方法上加synchronized代表类锁,普通方法加锁是实例锁,这两种锁不产生冲突,不会互相竞争。

        3、synchronized的三种应用方式:

        1)作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
        2)作用于代码块,对括号里配置的对象加锁。
        3)作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

        第一 第三种跟上述案例分析的是一样的,着重说一下第二中静态代码块:

        对于代码块:
        代码块的锁也有两种,一种是synchronized中加一个实例对象,一种是加类.class.这两种锁也分别代表实例锁跟类锁。

        ⚙️重点区别:

场景锁对象影响范围
synchronized(obj)普通对象实例 obj同一个对象的线程互斥,不同对象互不影响。
synchronized(类名.class)类的Class对象(全局唯一)所有线程,不管用哪个对象实例,都会互斥。

          📌形象解释:

        1️⃣ synchronized(obj):
        假如你 new 出很多个对象,每个对象自己用synchronized(this),那每个对象都像是自己的小屋,互不干扰。

        2️⃣ synchronized(类名.class):
        锁住的是整个类,哪怕不同的对象实例,线程也必须抢同一个锁,类似一个“工厂总门”,只有拿到钥匙的人才能进去,别人都得等。

           🌰举个例子:

class MyClass {public void instanceMethod() {synchronized(this) {System.out.println("对象锁:锁住的是当前实例");}}public static void staticMethod() {synchronized(MyClass.class) {System.out.println("类锁:锁住的是整个类");}}
}
  • synchronized(this):只会锁住这个对象实例,不同实例之间互不干扰。

  • synchronized(MyClass.class):锁住整个类,无论用哪个对象调用这个代码,只要一个线程进了,其他线程必须等!

        4、解释分析

         在java虚拟机种,class loader类加载器把 Car.class文件读进来,Car class就是类锁,这个就是模板,由一份模板可以生成 car1、car2、car3 三个实例对象,这是三个不同的对象但是均来自于一个模板。所以类锁对应的就是Car Class,在方法区中有且仅有一份,但是对于我们的对象锁,new出来的实例对象,在jvm的堆中。所以类锁跟对象锁,加锁的对象跟地方都不一样,自然就会产生不同的效果。

        5、从字节码角度分析synchronized实现

        从字节码角度分析,需要借助两个命令:
        javap -c ***.class  对代码进行反编译
        javap -v ***.calss  对文件进行反编译,但是会输出更多附加信息(包括行号、本地变量表、反汇编等详细信息)

        1)使用javap -c 反编译一个同步代码块的class文件: 


http://www.mrgr.cn/news/99155.html

相关文章:

  • Matplotlib的应用
  • (一)mac中Grafana监控Linux上的CPU等(Node_exporter 安装使用)
  • STM32的三种启动方式
  • 安卓学习24 -- 网络
  • AnimateCC基础教学:制作一个打地鼠简化版
  • 06 GE Modifier
  • 使用注解方式整合ssm时,启动tomcat扫描不到resource下面的xxxmapper.xml问题,解决方法
  • dawgctf 2025 writeup
  • 从零起步的Kaggle竞赛 - BirdCLEF2025
  • Python多任务编程:进程全面详解与实战指南
  • 实现AWS Lambda函数安全地请求企业内部API返回数据
  • 【C++详解】C++入门(一)命名空间、缺省参数、函数重载
  • Linux安装mysql_exporter
  • 第 28 场 蓝桥月赛
  • 线性DP:最长上升子序列(子序列可不连续,子数组必须连续)
  • C++ 模块化编程(Modules)在大规模系统中的实践难点
  • acwing--动态规划【线性dp】4/20、4/21
  • 大数据应用开发——大数据平台集群部署(四)
  • 机器学习专栏(4):从数据饥荒到模型失控,破解AI训练的七大生死劫
  • 分布类相关的可视化图像