并发编程-锁的那些事儿【四:理解原子性,议Java如何保证原子产生的问题】

前言

在前面章节,全面概括了并发三大特性,其中可见、有序性还是较为容易理解,并在前面章节都有对其做过场景理解说明,此篇单独对原子性做场景理解;

原子性特性

把一个或者多个操作在 CPU 执行的过程中不被中断的特性;Java内存模型中,直接保证了原子性变量操作【read,load,use,assign,store,wirte】,在应用中,可以大致认定基本类型操作读写具备原子性的,除了【long,double】,如果应用场景需要一个更大范围的原子操作,那么就有lock,unlock来保证。 但这并不是唯一的,比如还提供了,隐式的字节符码指令monitorenter和monitorexit,对应关键字就是synchronized。

原子性问题

从特性中明显分析的出来,保证原子性的根本:就是禁止CPU的中断,换句话来讲就是线程切换

以long基本类型操作读写为例说明: 总所周知,pc机器分32位和64位,long的字符节是64位的,如果在32位机器上执行写操作,那么得分俩次写操作执行,第一段32位,第二段32位。 那么就有可能出现 明明已经进行的写入操作,但读出来并不是预期值; 为什么会产生此bug呢? 在多核情况下,如有线程A在cpu1,B在cpu2上同时在运行,此时cpu中断了,只能保证cpu上的线程连续执行,不能让保证同一时刻,只有一个线程执行。如果线程a和b同时写入第一段32位字符,就可能产生问题了。

同一时刻下保证只有一个线程执行 就可以保证原子性,称为互斥

原子性解决思路

在上述的问题中,已经把问题和答案公布,那么解决的思路就是如何来建立互斥,实现上述的论点;

对,你猜到了,就是用Lock【锁】 :
对于一段有风险的代码块,可在 代码块进行 lock()---->代码块执行[互斥区]----->unlock()

不论任意线程需要执行时,先尝试进行加锁,获取到锁后,方可进行互斥区的操作,最后解锁。 于此同时,其他线程在在还未解锁时。都需要在互斥区钱等着获取锁,否者一直等着;

用个生活中的例子理解下: 每天早上上班后,在上厕所高峰期时,坑位 就是咱这互斥区,进坑为为加锁,出坑开门为解锁。 咋一看,没毛病就是这样的,但细细一想不对,咱们锁的到底是啥? 所以得在进一步细化这个互斥的步骤;

互斥锁的模型

接着上面的问题细化,lock到底是锁什么东西呢? 总得有个事物,比如我家里的门锁,锁的自己的家,总不能去锁其他人家的吧。 那么锁和资源就有一种关系存在了,改进锁的方式:

create资源保护锁LR---->lock()---->代码块执行[互斥区]------>互斥区中圈出受保护资源----->unlock()

那么这个流程中 LR锁 对应资源关系 就是 互斥区中的受保护资源,不然又得出现自家门锁,锁其他人家了。

解决原子的技术

在Java中提供了synchronized 关键字 和 Lock锁的实现,俩种方式各有特点。后面例子先用synchronized讲述,可以用来修饰方法,代码块,对象;例如:

class LockTest {

    Object obj = new Object();
    
    //锁非静态方法------当修饰非静态方法的时候,锁定的是当前实例对象 this。
    synchronized void test1() {}
    
    //锁静态方法------当修饰静态方法的时候,锁定的是当前类的 Class 对象,即LockTest
    synchronized static void test2() {}
        
    //锁对象,锁代码块
    void test3() {
            synchronized(obj) {}
    }
}

不用奇怪,没找到lock和unlock是因为在编译时,隐时帮助自动加上,切记lock和unlock必定是成对出现,不然肯定会造成很大问题。

在看a +=1场景

class Calc{
    long a = 0L;
    
    long read(){
        return a;
    }

    synchronized void calc(){
        a += 1;
    }
}

calc()是符合 Happens-Before规则中的 “管程锁定规则”,因此是具备可见性,在则被synchronized修饰后,能保证在同一时刻下只有一个线程执行,也可以保证原子操作。如果在多线程的情况下循环执行calc()100次,不出意外 a已经等于100;

在看read方法,经过calc方法后,其a变量对read是可见的么? 答案是否定,套用下BF规则,是否没有一个满足的。 那么解决的办法也有挺多的, 最简单的对read方法进行加锁,用synchronize修饰即满足管程锁定规则:对一个锁解锁后,后续对这个锁加锁的是可见的。

class Calc{
    long a = 0L;
    
    synchronized long read(){
        return a;
    }

    synchronized void calc(){
        a += 1;
    }
}

这就回到上述说的互斥锁的模型上了, 这里锁的资源都是this,不论哪个线程执行read和calc方法时,都是用this锁进行保护的,那么这样一来 a 也对read具备了可见。

锁与受保护资源的关系

这个单独拿出来说,是因为这个误区大多从这不经意间开始,否者锁是有了,但我家锁,锁到别人家,这不茬批了。 对于这个关系正解应该是:受保护资源和锁之间的关联关系是 N:1 的关系 ,也就是说,家里的门锁,可以保护电视,冰箱,洗衣机等等,对于家里面的物件不是单独上锁了。

基于上述例子做下小改动,将calc编程静态方法,那此时并发还会安全么? 各自锁的资源又是啥?

class Calc{
    long a = 0L;
    
    synchronized long read(){
        return a;
    }

    synchronized static void calc(){
        a += 1;
    }
}

首先并发肯定不是安全的,在“解决原子的技术”篇幅,有说明,对于静态和非静态方法加锁后的所资源实惠变化的, read锁住的是this,calc锁的是Calc.class这个对象,那么此时俩个方法并有互斥了,肯定是存在并发安全问题。由此可见虽然表面上都是加了锁,但是使用方式不对,是很可能在此造成安全问题。

总结

通过原子特性,引出来互斥锁的特性。 但同时也告诉我们,盲目的加锁有可能还是会造成并发不安全,如果想要真正正确使用锁,那么就得有这种锁与资源的风险意识。在平时开发过程中,这个点往往是最容易搞混淆的点。深入分析锁 定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互 斥锁。

Java中还有其他的锁实现,此篇是用synchronize为例,但是使用的原理本质都是一致的。只是锁的实现方式不同,功能不同罢了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值