Java锁到底是个什么东西

一、java锁存在的必要性

要认识java锁,就必须对2个前置概念有一个深刻的理解:多线程共享资源

对于程序来说,数据就是资源。

在单个线程操作数据时,或快或慢不存在什么问题,一个人你爱干什么干什么。

多个线程操作各自操作不同的数据,各干各的,也不存在什么问题。

多个线程对共享数据进行读取操作,我就四处看看,什么也不动,也不存在什么问题。

但如果多个线程共享数据进行操作,问题就来了。

经典库存问题:

mysql 记录剩余:1,redis 缓存记录剩余:1。

小明上网下单,后台程序检查 redis 记录存货剩 1 台,数据库执行 -1,但小明网太卡了,数据库刚执行完 -1,redis 没来得及更新成0,小红的华为5G直接下单,redis 剩1台,数据库-1,redis -1,下单成功一气呵成。结果就是2个人买了同一台手机。

这种业务场景可以说比比皆是,所以要解决这种数据同步问题就要有对应的办法,所以发明了java锁这个工具来保证数据的一致性,举个例子:

在一个不分男女的公共厕所中上一把锁,有人进去,把门锁住,上完出来,把锁打开,以此类推。

二、2个重要的java锁

synchronized关键字

synchronized关键字是java开发人员最常用的给共享资源上锁的方式,也基本可以满足一般的进程同步要求,使用 synchronized 无需手动执行加锁和释放锁的操作,只需在需要同步的代码块、普通方法、静态方法上加入该关键字即可,JVM 层面会帮我们自动的进行加锁和释放锁的操作。

修饰普通方法

/*** synchronized 修饰普通方法*/
public synchronized void method() {// ....
}

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

修饰静态方法

/*** synchronized 修饰静态方法*/
public static synchronized void staticMethod() {// .......
}

当 synchronized 修饰静态方法时,其作用范围是整个程序,这个锁对于所有调用这个锁的对象都是互斥的。

修饰普通方法 VS 修饰静态方法

创建一个类,其中有synchronized修饰的普通方法和synchronized修饰的静态方法。

public class SynchronizedUsage {/*** synchronized 修饰普通方法*/public synchronized void method() {System.out.println("普通方法执行时间:" + LocalDateTime.now());try {// 休眠 3sTimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}}/*** synchronized 修饰静态方法*/public static synchronized void staticMethod() {System.out.println("静态方法执行时间:" + LocalDateTime.now());try {// 休眠 3sTimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}}
}

测试

    public class Test01 {/*** 创建线程池同时执行任务*/static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static void main(String[] args) {// 执行两次静态方法threadPool.execute(() -> {SynchronizedUsage.staticMethod();});threadPool.execute(() -> {SynchronizedUsage.staticMethod();});// 执行两次普通方法threadPool.execute(() -> {SynchronizedUsage usage = new SynchronizedUsage();usage.method();});threadPool.execute(() -> {SynchronizedUsage usage2 = new SynchronizedUsage();usage2.method();});}}

结果

图片

说明:

普通方法的2次调用归属于不同的对象,也就是不同的锁,所以执行的时候互不影响。

静态方法的2次调用归属于同一个类,也就是相同的锁,所以分先后执行,间隔3s。

修饰代码块

我们在日常开发中,最常用的是给代码块加锁,而不是给方法加锁,因为给方法加锁,相当于给整个方法全部加锁,这样的话锁的粒度就太大了,程序的执行性能就会受到影响,所以通常情况下,我们会使用 synchronized 给代码块加锁,它的实现语法如下:

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

从上述代码我们可以看出,相比于修饰方法,修饰代码块需要自己手动指定加锁对象,加锁的对象通常使用 this 或 xxx.class 这样的形式来表示,比如以下代码:

// 加锁某个类
synchronized (SynchronizedUsage.class) {// ......
}// 加锁当前类对象
synchronized (this) {// ......
}

以上2中加锁方式类似于上文中普通方法与静态方法的区别,加锁当前类对象this只作用于当前对象,对象不同则锁不同,加锁某个类则作用于该类,同属于一个类的对象使用同一把锁。

创建一个类

    public class SynchronizedUsageBlock {/*** synchronized(this) 加锁*/public void thisMethod() {synchronized (this) {System.out.println("synchronized(this) 加锁:" + LocalDateTime.now());try {// 休眠 3sTimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}}}/*** synchronized(xxx.class) 加锁*/public void classMethod() {synchronized (SynchronizedUsageBlock.class) {System.out.println("synchronized(xxx.class) 加锁:" + LocalDateTime.now());try {// 休眠 3sTimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}}}}

测试

    public class Test02 {public static void main(String[] args) {// 创建线程池同时执行任务ExecutorService threadPool = Executors.newFixedThreadPool(10);// 执行两次 synchronized(this)threadPool.execute(() -> {SynchronizedUsageBlock usage = new SynchronizedUsageBlock();usage.thisMethod();});threadPool.execute(() -> {SynchronizedUsageBlock usage2 = new SynchronizedUsageBlock();usage2.thisMethod();});// 执行两次 synchronized(xxx.class)threadPool.execute(() -> {SynchronizedUsageBlock usage3 = new SynchronizedUsageBlock();usage3.classMethod();});threadPool.execute(() -> {SynchronizedUsageBlock usage4 = new SynchronizedUsageBlock();usage4.classMethod();});}}

结果

图片

Lock接口

Lock接口及其相关的实现类是在JDK 1.8之后在并发包中新增的,最常用且常见的就是ReentrantLock。与synchronized不同的是,ReentrantLock在使用时需要显式的获取和释放锁。

虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

Lock接口提供的synchronized不具备的特性

图片

Lock接口中定义的方法

图片

尽管java实现的锁机制有很多种,并且有些锁机制性能也比synchronized高,但还是强烈推荐在多线程应用程序中使用该关键字,因为实现方便,后续工作由jvm来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。

三、java锁的核心分类

悲观锁

悲观锁总是假设最坏的情况,每次取数据时都认为其他线程会对数据进行修改,所以都会加锁,当其他线程想要访问数据时,都需要阻塞挂起。所以悲观锁总结为悲观加锁阻塞线程

  • • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

MySQL数据库中的表锁、行锁、读锁、写锁等,Java中,synchronized关键字和Lock的实现类都是悲观锁。

图片

乐观锁

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。总结为乐观无锁回滚重试

  • • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

  • • 乐观锁天生免疫死锁。

图片

乐观锁一般有2种实现方式:

CAS算法

即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

优点:

效率比较高,无阻塞,无等待,重试。

缺点:

ABA问题: 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS检查时发现它的值没有发生变化,但实际上发生了变化:A->B->A的过程。

循环时间长,开销大: 自旋CAS如果长时间不成功,会给CPU带来很大的执行开销。

只能保证一个共享变量的原子操作: 当对一个共享变量操作时,我们可以采用CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。

版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    update table set x = x + 1, version = version + 1 where id = #{id} and version = #{version};

MybaisPlus对乐观锁的实现

1)在数据库中添加 version 字段,作为乐观锁的版本号

    --在数据库中的user表中添加一个version字段,用于实现乐观锁ALTER TABLE `user` ADD COLUMN `version` INT

2)在对应的实体类中添加 version 属性,并且在这个属性上面添加 @Version 注解

    @Datapublic class User {@TableId(type = IdType.AUTO)//主键自动增长private Long id;private String name;private Integer age;private String email;@TableField(fill = FieldFill.INSERT)//INSERT的含义就是添加,也就是说在做添加操作时,下面一行中的createTime会有值private Date createTime;@TableField(fill = FieldFill.INSERT_UPDATE)//INSERT_UPDATE的含义就是在做添加和修改时下面一行中的updateTime都会有值,因为是第一次添加,还没有做修改(一般都使用这个)private Date updateTime;@Version//版本号,用于实现乐观锁(这个一定要加)@TableField(fill = FieldFill.INSERT)//添加这个注解是为了在后面设置初始值,不加也可以private Integer version;}

3)写一个配置类,配置乐观锁插件

    @Configuration@MapperScan("cn.hb.mapper")public class MpConfig {//乐观锁插件@Beanpublic OptimisticLockerInterceptor optimisticLockerInterceptor() {return new OptimisticLockerInterceptor();}}

4)设置版本号 version 的初始值为1

5)向表中添加一条数据,看 version 的值是否为1

6)测试乐观锁,看 version 的值是否加1

四、java锁的其他分类

synchronized性能优化

锁膨胀/锁升级

JDK 6之前synchronized是一个独占式的悲观锁、重量级锁,效率偏低。JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。(注意无锁和偏向锁是同一级别,锁标志位都是01,二者之间不存在膨胀关系,可以理解为无锁状态是轻量锁的空闲状态)

偏向锁

在程序第一次执行到 synchronized 代码块的时候,锁对象变成 偏向锁 ,即偏向于第一个获得它的线程的锁。在程序第二次执行到改代码块时,线程会判断此时持有锁的线程是否就是它自己,如果是就继续往下面执行。值得注意的是,在第一次执行完同步代码块时,并不会释放这个偏向锁。从效率角度来看,如果第二次执行同步代码块的线程一直是一个,并不需要重新做加锁操作,没有额外开销,效率极高。

轻量级锁

当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

这里不同情况需值得注意:当第二个线程想要获取锁时,且这个锁是偏向锁时,会判断当前持有锁的线程是否仍然存活,如果该持有锁的线程没有存活,那么偏向锁并不会升级为轻量级锁 。

重量级锁

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

当其他线程再次尝试获取锁的时候,发现现在的锁是重量级锁,此时线程都会进入阻塞状态。

锁消除

锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是**被检测到不可能存在共享数据竞争情况”的锁进行消除。**根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。

锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

自适应自旋锁

自旋锁

现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。自旋锁优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

自适应自旋锁:

但是,如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。然而对于 synchronized 关键字来说,它的自旋锁更加的“智能”,synchronized 中的自旋锁是自适应自旋锁。

自适应自旋锁是指,**线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。**比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。

防止死锁

• 不要写嵌套锁,容易死锁

• 尽量少用同步代码块(Synchronized)

• 尽量使用ReentrantLock的tryLock方法设置超时时间,超时可以退出,防止死锁

• 尽量降低锁粒度,尽量不要几个功能一把锁

公平锁与非公平锁

当一个线程持有的锁释放时,其他线程按照先后顺序,先申请的先得到锁,那么这个锁就是公平锁。反之,如果后申请的线程有可能先获取到锁,就是非公平锁 。

Java 中的 ReentrantLock 可以通过其构造函数来指定是否是公平锁,默认是非公平锁。一般来说,使用非公平锁可以获得较大的吞吐量,所以推荐优先使用非公平锁

synchronized 是一种非公平锁。

可重入锁和非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

    public class Widget {public synchronized void doSomething() {System.out.println("方法1执行...");doOthers();}public synchronized void doOthers() {System.out.println("方法2执行...");}}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

独享锁与共享锁

独占锁是一种思想:只能有一个线程获取锁,以独占的方式持有锁。

Java中用到的独占锁:synchronized,ReentrantLock

共享锁是一种思想:可以有多个线程获取读锁,以共享的方式持有锁。

Java中用到的共享锁:ReentrantReadWriteLock。


往期推荐:

● 师爷,翻译翻译什么叫AOP

● 终于搞懂动态代理了!

● 学会@ConfigurationProperties月薪过三千

● 0.o?让我看看怎么个事儿之SpringBoot自动配置

● 不是银趴~是@Import!

● Java反射,看完就会用

图片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/671194.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Go语言成长之路】创建Go模块

文章目录 创建Go模块一、包、模块、函数的关系二、创建模块2.1 创建目录2.2 跟踪包2.3 编写模块代码 三、其它模块调用函数3.1 修改hello.go代码3.2 修改go.mod文件3.3 运行程序 四、错误处理4.1 函数添加错误处理4.2 调用者获取函数返回值4.4 执行错误处理代码 五、单元测试5.…

LeetCode、198. 打家劫舍【中等,一维线性DP】

文章目录 前言LeetCode、198. 打家劫舍【中等,一维线性DP】题目及分类思路线性DP(一维) 资料获取 前言 博主介绍:✌目前全网粉丝2W,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注…

Python循环语句——for循环的基础语法

一、引言 在Python编程的世界中,for循环无疑是一个强大的工具。它为我们提供了一种简洁、高效的方式来重复执行某段代码,从而实现各种复杂的功能。无论你是初学者还是资深开发者,掌握for循环的用法都是必不可少的。在本文中,我们…

element ui表格手写拖动排序

效果图: 思路: 重点在于:拖动行到某一位置,拿到这一位置的标识,数据插入进这个位置 vueuse的拖拽hooks useDraggable 可以用;html5 drag能拖动行元素;mounsedown、mounsemove时间实现拖拽 页…

【Iceberg学习四】Evolution和Maintenance在Iceberg的实现

Evolution Iceberg 支持就底表演化。您可以像 SQL 一样演化表结构——即使是嵌套结构——或者当数据量变化时改变分区布局。Iceberg 不需要像重写表数据或迁移到新表这样耗费资源的操作。 例如,Hive 表的分区布局无法更改,因此从每日分区布局变更到每小…

2023年03月CCF-GESP编程能力等级认证C++编程二级真题解析

一、单选题(每题2分,共30分) 第1题 以下存储器中的数据不会受到附近强磁场干扰的是( )。 A.硬盘 B.U盘 C.内存 D.光盘 答案:D 第2题 下列流程图,属于计算机的哪种程序结构?( )。 A.顺序结构 B.循环结构 C.分支结构 D.数据结构 答案:C 第3题 下列关…

CTF-show WEB入门--web21

上一阶段的信息泄露已经全部完结了,下一阶段的爆破也由此开始啦~~~ 下面让我们看看web21,这题是个经典的爆破问题 老样子我们先打开题目,查看题目提示: 我们可以看到题目提示为: 爆破什么的,都是基操 还有这题题目…

【RPA】2分钟带你搞懂,这么火的RPA到底是什么?

2分钟带你搞懂,这么火的RPA到底是什么? 在当今数字化时代,机器人流程自动化(RPA)成为了企业数字化转型的重要组成部分。RPA是一种基于规则的软件技术,可以自动执行重复性、高度规范化的业务流程任务。 与传…

jsp教材管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 教材管理系统是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql5.0&…

Android应用程序的编译和打包

Android系统的APK应用程序可以有以下几种编译方式 借助系统编译:利用Android.mk 文件将众多小项目组织起来 借助IDE编译:AndroidStudio 命令行编译 : 比如利用gradle脚本编译APK应用。 一、 通过命令行编译和打包APK 编译命令(Window系…

没有联合和枚举 , C语言怎么能在江湖混 ?

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 我会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. 🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能…

探索C语言结构体:编程中的利器与艺术

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:C语言学习 贝蒂的主页:Betty‘s blog 1. 常量与变量 1. 什么是结构体 在C语言中本身就自带了一些数据类型&#x…

LLMs之miqu-1-70b:miqu-1-70b的简介、安装和使用方法、案例应用之详细攻略

LLMs之miqu-1-70b:miqu-1-70b的简介、安装和使用方法、案例应用之详细攻略 目录 miqu-1-70b的简介 miqu-1-70b的安装和使用方法 1、安装 2、使用方法 miqu-1-70b的案例应用 miqu-1-70b的简介 2024年1月28日,发布了miqu 70b,潜在系列中的…

Linux系统调试课:ftrace跟踪器介绍

文章目录 一、什么是frace跟踪器?二、Ftrace 配置三、Ftrace 文件系统四、Ftrace 初体验五、函数跟踪六、Ftrace function_graph七、函数 Profiler沉淀、分享、成长,让自己和他人都能有所收获!😄 一、什么是frace跟踪器? 操作系统内核对应用开发工程师来说就像一个黑盒,…

elementUI 表格中如何合并动态数据的单元格

elementUI 表格中如何合并动态数据的单元格 ui中提供的案例是固定写法无法满足 实际开发需求 下面进行改造如下 准备数据如下 //在表格中 设置单元格的方法 :span-method"spanMethodFun" <el-table :data"tableData" border :span-method"spa…

手撕spring bean的加载过程

这里我们采用手撕源码的方式&#xff0c;开始探索spring boot源码中最有意思的部分-bean的生命周期&#xff0c;也可以通过其中的原理理解很多面试以及工作中偶发遇到的问题。 springboot基于约定大于配置的思想对spring进行优化&#xff0c;使得这个框架变得更加轻量化&#…

Backtrader 文档学习- Observers

Backtrader 文档学习- Observers 1.概述 在backtrader中运行的策略主要处理数据源和指标。 数据源被加载到Cerebro实例中&#xff0c;并最终成为策略的一部分&#xff08;解析和提供实例的属性&#xff09;&#xff0c;而指标则由策略本身声明和管理。 到目前为止&#xff0c…

LabVIEW多功能接口卡驱动

LabVIEW多功能接口卡驱动 随着自动化测试系统的复杂性增加&#xff0c;对数据采集与处理的需求不断提高。研究基于LabVIEW开发平台&#xff0c;实现对一种通用多功能接口卡的驱动&#xff0c;以支持多通道数据采集及处理功能&#xff0c;展现LabVIEW在自动化和测量领域的强大能…

如何部署Docker Registry并实现无公网ip远程连接本地镜像仓库

文章目录 1. 部署Docker Registry2. 本地测试推送镜像3. Linux 安装cpolar4. 配置Docker Registry公网访问地址5. 公网远程推送Docker Registry6. 固定Docker Registry公网地址 Docker Registry 本地镜像仓库,简单几步结合cpolar内网穿透工具实现远程pull or push (拉取和推送)…

IPv4的公网地址不够?NAT机制可能是当下最好的解决方案

目录 1.前言 2.介绍 3.NAT机制详解 1.前言 我们都知道IPv4的地址范围是32个字节,这其中还有很多地址是不可用的.比如127.*,这些都是环回地址.那么在网路发展日新月异的今天,互联网设备越来越多,我们该如何解决IP地址不够用的问题呢?目前有一种主流的解决方案,也是大家都在用…