Java锁和分布式锁的用法及解读

 更新时间:2025年07月21日 09:55:36   作者:坏猫警长  
文章阐述了线程安全问题、Java同步锁的使用及Redis分布式锁的原理,核心内容包括:多线程并发修改共享数据导致不一致,Java通过synchronized锁保证原子性;Redis利用单线程特性和setnx命令实现跨进程锁,解决多节点数据同步问题

一、并发下的线程安全问题

所谓线程安全问题,是指多个线程“同时”对同一个数据进行修改时,出现数据不一致等现象。

考虑如下这个线程不安全的情况:

public static int num = 0;
public static void main(String[] args) throws InterruptedException {
   Thread t1 = new Thread(() -> {
   for (int i = 0; i < 10000; i++) {
   num++;
   }
   });
   //
   Thread t2 = new Thread(() -> {
   for (int i = 0; i < 10000; i++) {
   num--;
   }
   });
   //
   t1.start();
   t2.start();
   Thread.sleep(5000);
   System.out.println(num);
}

以上代码中,num变量的初值为0,两个线程并发执行,分别对num自增1万次和自减1万次,按常理执行完成后,num的值应该仍然是0。然而实际的情况是,最终的值是多少并不确定,多运行几次,有时得个正数,有时得个负数。

出现这种现象的原因是两个线程对同一个数据num的操作会互相干扰,一个线程正用着num运行到一半时,另一个线程修改了num的值。这里的num自增自减虽然在java层面只有一行代码,但是在cpu执行时其实是多个步骤,至少包括读取数值、计算加1、写回内存,而这中间便可能发生线程切换,例如:线程一读取了num的值为0,然后线程二也读取了num的值为0,线程一计算+1再写回内存值为1,线程二计算-1然后写回内存值为-1,所以明明应该得到结果为0,但是这里得到的值却是-1,这里最本质的问题就是线程对共享数据num的操作不是原子性的

这个示例中对共享数据num的操作只有一行代码,实际项目中往往涉及到多行代码的复杂处理,于是高并发情况下就很容易出现诡异的数据不一致问题。

二、Java同步锁的用法

如上所说,解决并发安全问题的关键就是保证共享数据操作的原子性,在某个线程正在使用共享数据的过程中,避免其他线程操作这个共享数据。Java代码层面最简单的方法就是加锁,针对某个共享数据,在所有准备使用它的代码段开头获得一个锁,用完了再把锁释放即可,如果拿不到锁就说明被其他线程拿去了,则代码暂停执行,等待其他线程释放锁,直到拿到了锁再继续执行

需要知道,获得锁和释放锁的动作会由操作系统和硬件来保证其原子性,试想如果加解锁动作本身也不原子,那么线程安全问题就无解了。Java加解锁的synchronized方式和ReentrantLock方式原理相似,这里只介绍synchronized的用法。

Java中的任意Object对象都拥有一个锁,包括Class对象,以下代码演示Class对象锁的使用:

要特别注意,不同类加载器加载的同一个class文件,产生的Class对象并不是同一个,当然锁也不是同一个。

以下代码演示使用一个普通对象的锁:

要特别注意,如果是同一个class类型的不同对象执行这个方法,在这里锁当然是不同的,各自是各自的锁。

如下代码不能保证线程安全,因为这100个线程中的obj对象不是同一个,即各线程执行methodSync方法时获得的锁不是同一个:

下面简单演示开篇代码的synchronized处理办法,只要保证给不安全的代码块加上同一个锁即可,不论是同一个对象、同一个Class对象、同一个字符串对象。

public static int num = 0;
public static void main(String[] args) throws InterruptedException {
   Thread t1 = new Thread(() -> {
   synchronized ("锁") {
   for (int i = 0; i < 10000; i++) {
   num++;
   }
   }
   });
   //
   Thread t2 = new Thread(() -> {
   synchronized ("锁") {
   for (int i = 0; i < 10000; i++) {
   num--;
   }
   }
   });
   //
   t1.start();
   t2.start();
   Thread.sleep(5000);
   System.out.println(num);
}

真实项目中使用时,锁的选择很重要,不要随便找个对象来当锁,最好是和代码逻辑以及共享数据有所关联,随便找一个字符串来当锁是不负责任的。

这里应该能感觉到,很多情况下用那个被共享的数据来当锁可能正合适

三、Redis分布式锁的原理

Java的锁只能管到自己的进程,如果“共享数据”是多进程共享的比如数据库里的一个数据,那么Java锁就无效了,因为进程之间无法找到同一个java对象来当锁,这时就需要分布式锁来控制。当然其原理本身,也是想办法在进程之间找到“同一个对象”(类似二值信号量)来当锁

于是redis就成了一个选择,分布式系统中,各个应用大家共用同一个redis,那不妨就模仿进程级锁来做一个分布式锁,具体实现上考虑以下两个方面:

1、在redis中找一个公共的数据来充当锁的角色,所有并发安全相关的代码块开头加锁,结尾解锁,加不上锁就等待。

2、想办法保证加锁动作的原子性,没有了操作系统和硬件来保证,则必须自行设计一番。

使用redis来实现分布式锁的代码网上有很多,可以结合以上两方面来分析其写法。这里只解释一下为什么reids容易保证加锁动作的原子性:

如上图,redis对数据的操作是单线程的,即由一个线程来执行从外部传进来的诸多命令,一个命令一个命令排着执行,先到的先执行,后到的后执行。所以只要加锁的动作能由一个命令来完成,则天然就能保证加锁动作的原子性,解锁也是一样。于是setnx命令就成了一个很好的选择,单条命令就可以设置数据同时返回是否设置成功。

所以也就很好理解,如果要给锁加失效时间或锁持有者信息的话,为什么实现上就变得复杂了,根本原因就是需要保证加解锁动作的原子性,避免高并发时加解锁动作执行到一半时被别的进程给钻了空子。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java中获取类加载路径和项目根路径的5种方式分析

    java中获取类加载路径和项目根路径的5种方式分析

    本篇文章介绍了,java中获取类加载路径和项目根路径的5种方式分析。需要的朋友参考下
    2013-05-05
  • Java实现本地缓存的四种方法实现与对比

    Java实现本地缓存的四种方法实现与对比

    本地缓存的优点就是速度非常快,没有网络消耗,本地缓存比如 caffine,guava cache 这些都是比较常用的,下面我们来看看这四种缓存的具体实现吧
    2025-08-08
  • Docker与Kubernetes部署Java应用容器化实践指南

    Docker与Kubernetes部署Java应用容器化实践指南

    容器化是一种将应用程序及其依赖打包成一个可移植的单元的方法,容器能够在任何环境中运行,并且具有隔离性、可扩展性和高效性,这篇文章主要介绍了Docker与Kubernetes部署Java应用容器化实践指南的相关资料
    2026-05-05
  • Spring中的@ModelAttribute模型属性绑定详解

    Spring中的@ModelAttribute模型属性绑定详解

    这篇文章主要介绍了Spring中的@ModelAttribute模型属性绑定详解,@ModelAttribute用于将方法参数或返回值绑定到Model属性上,并公开给Web视图,支持使用@RequestMapping注释的Controller类,需要的朋友可以参考下
    2024-02-02
  • IDE 搭配 Lombok 注解的配置方法

    IDE 搭配 Lombok 注解的配置方法

    Lombok是一个Java库,通过注解简化代码编写,减少样板代码,在使用IDE如IntelliJ IDEA或Eclipse结合Lombok时,需要先添加Lombok依赖,然后在IDE中配置,以启用注解处理器,配置完成后,使用Lombok注解来简化Java类的编写,本文介绍IDE搭配Lombok注解的配置,感兴趣的朋友一起看看吧
    2025-03-03
  • java新人基础入门之递归调用

    java新人基础入门之递归调用

    这篇文章主要给大家介绍了关于java新人基础入门之递归调用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02
  • SpringBoot实现任意文件在线预览功能

    SpringBoot实现任意文件在线预览功能

    在现代工作与生活环境中,文件共享与查看已经成为日常不可或缺的一部分,不过传统的文件处理方式要求用户下载文件到本地后才能查看,不仅占用了设备的存储空间,还可能导致文件传播效率低下,所以本文给大家介绍了SpringBoot实现任意文件在线预览功能,需要的朋友可以参考下
    2025-07-07
  • 浅谈Java中的集合存储数据后,输出数据的有序和无序问题

    浅谈Java中的集合存储数据后,输出数据的有序和无序问题

    这篇文章主要介绍了浅谈Java中的集合存储数据后,输出数据的有序和无序问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Java 8日期处理LocalDateTime时区转换的3大误区与解决方案

    Java 8日期处理LocalDateTime时区转换的3大误区与解决方案

    文章介绍了Java 8中的日期时间API,包括`LocalDateTime`、`ZonedDateTime`和`Instant`等核心类,以及它们在不同场景下的应用,文章还探讨了常见误区,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • Springboot集成Hutool导出CSV的方法

    Springboot集成Hutool导出CSV的方法

    文章介绍了如何引入依赖、使用CSV工具类以及实现接口的相关内容,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2026-01-01

最新评论