设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间18 现在时间18 缓存数据 从头到尾 我要的只有感情 可没人能给我

从头到尾 我要的只有感情 可没人能给我 -- 情深深雨濛濛

查看: 936|回复: 0

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:204
  • 打卡月天数:0
  • 打卡总奖励:3098
  • 最近打卡:2023-08-27 10:43:43
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
407
主题
363
精华
0
金钱
4265
积分
798
注册时间
2022-12-25
最后登录
2025-8-27

发表于 2025-8-22 22:08:54 | 显示全部楼层 |阅读模式

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

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

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

  1. public static int num = 0;
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 = new Thread(() -> {
  4. for (int i = 0; i < 10000; i++) {
  5. num++;
  6. }
  7. });
  8. //
  9. Thread t2 = new Thread(() -> {
  10. for (int i = 0; i < 10000; i++) {
  11. num--;
  12. }
  13. });
  14. //
  15. t1.start();
  16. t2.start();
  17. Thread.sleep(5000);
  18. System.out.println(num);
  19. }
复制代码

以上代码中,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对象锁的使用:

1.jpeg

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

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

2.jpeg

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

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

3.jpeg

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

  1. public static int num = 0;
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 = new Thread(() -> {
  4. synchronized ("锁") {
  5. for (int i = 0; i < 10000; i++) {
  6. num++;
  7. }
  8. }
  9. });
  10. //
  11. Thread t2 = new Thread(() -> {
  12. synchronized ("锁") {
  13. for (int i = 0; i < 10000; i++) {
  14. num--;
  15. }
  16. }
  17. });
  18. //
  19. t1.start();
  20. t2.start();
  21. Thread.sleep(5000);
  22. System.out.println(num);
  23. }
复制代码

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

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

三、Redis分布式锁的原理

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

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

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

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

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

4.jpeg

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

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

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持晓枫资讯。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表