Java锁和分布式锁用法浅析

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

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

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

public static int num 0;
public static void main(String[] args) throws InterruptedException {
   Thread t1 new Thread(() -> {
       for (int 010000i++) {
           num++;
       }
   });
   //
   Thread t2 new Thread(() -> {
       for (int 010000i++) {
           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 010000i++) {
               num++;
           }
       }
   });
   //
   Thread t2 new Thread(() -> {
       synchronized ("") {
           for (int 010000i++) {
               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命令就成了一个很好的选择,单条命令就可以设置数据同时返回是否设置成功。

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

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坏猫警长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值