互斥锁 Synchronized

目录

一、java语言提供的所技术:synchronized

       用synchronized 解决 count += 1的问题:

二、对象级别的锁和类级别的锁的区别

三、用一个锁来保护多个资源

四、死锁问题:


使用锁来保护资源,首先需要确定锁和被保护资源的关系。为被保护资源R创建一把锁LR,然后再操作被保护资源R的时候对R进行加锁和解锁处理。

一、java语言提供的所技术:synchronized

synchronized关键字,是锁的一种实现,可以用来修饰方法,代码块。为了确保lock() 和 unlock() 的成对出现。synchronized中内置了lock() 和 unlock() 方法。


对于修改代码块的时候,我们可以明确的看出,synchronized锁定的对象是obj这个对象,但在我们使用synchronized修饰方法(静态和非静态)的时候,并没有显示的指出锁定的是什么(也就是未指定被保护的资源)。
是因为java中存在一条隐式规则(该规则 与 静态和非静态 的 实现机制有关):
synchronized修饰的是静态方法:锁定的是当前类的class,也就是代码中的Class X;
synchronized修饰的是非静态方法:锁定的是当前的实例对象 this;

以上规则应该着重注意


用synchronized 解决 count += 1的问题:

在多核场景下,多线程共同处理count += 1时候,会由于缓存的可见性导致 count += 1 的结果与预期不符合(并发编程中缓存导致的可见性问题
现在我们用synchronized来解决这个问题,我们需要确保同一个时刻只有一个线程进行 count += 1;所以用synchronized来修饰addOne()方法。

这种情况首先确保了同一个时刻只有一个线程执行addOne方法,确保了原子性,当前线程执行完成后,count的值根据happen-before中的管程中锁规则,对于后续进行加锁执行addOne方法的线程是可见的。无论由多少个线程来一共执行1000次addOne方法。count的值都会是预期的1000;

当我们想获取count的值的时候,我们需要借助get()方法。那么count的值对于get方法的可见性却无法保证。为了保证get方法的可见性,根据管程中锁的规则,需要是get和addOne使用同一把锁。这里面 addOne 使用的锁 是 this对象(java隐式规则),那么直接用synchronized修改get方法,从而get和addOne方法使用了同一把锁,从而确保了count对于两个方法的可见性。代码如下:

将以上代码转换为锁的模型大概是:


 

如果我们将addOne()方法活成static

这时候 根据 java的隐式规则 get 加锁的是 this对象,而 addOne 加锁的就是 Class x。这个时候两个方法的锁是不一样的。从而 管程中的锁的规则 就无法保证这两个方法间 对于共享变量 count的可见性了。

以上代码的锁的模型就是

 

综上:同一个资源不可以使用多个锁来进行锁定,但可以用一个锁来保护多个资源

二、对象级别的锁和类级别的锁的区别

  • 对象级别的锁 : 不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行加锁方法,锁并不会起作用
  • 类级别的锁:所有的对象都共享同一把锁

 

三、用一个锁来保护多个资源

保护多个资源的时候,首先我们需要确定多个资源是否存在关联

保护没有关联的多个资源

多个资源没有关联关系,就例如银行账户的取款操作和账户的密码修改,密码和余额就是需要保护的没有关联的资源。那么不存在关联的不同的资源就使用不同的锁保护,各自管各自的。这样可以使不同的资源可以并行被使用,提升了性能。如果用通一把锁将没有关联的资源加锁,这些资源就只能串行使用用不同的锁对受保护的资源精细化管理。这种锁叫做 细粒度锁。


保护有关联的多个资源
提供一把可以覆盖所有受保护资源就可以。主要需要注意的是 synchronized 锁的是this对象还是Class。

例如:转账业务,从账户A中转出100元到账户B中,那么账户A和B就是存在关联的两个不同资源。

                 

为了保证transfer方法不存在并发问题。我们可以给他加上synchronized关键字修饰一下。那么可以看出来改锁 锁定的是 Account 的 this对象,那么对于 target 对象就保护不了。因而简单的加上synchronized并不能实现一把锁保护多个资源。

根据上面提到过的java隐式规则,如果transfer声明成静态方法。那么就可以达到,用一把Class的锁锁住了所有被保护的资源。但是需要考虑一下如果声明成静态方法,会比较浪费资源。参考 静态和非静态 方法的区别。
但我们可以通过代码块的方式 锁住 CLASS。

锁相当于保证了面向高级语言的原子性。

四、死锁问题:

在程序中通常使用细粒度锁来提高并发量,进行性能的提升,但是细粒度锁的代价是可能会造成死锁问题。死锁的定义:一组相互竞争资源的线程因互相等待,导致“永久”阻塞的现象。程序一旦发生了死锁,一般没有什么好办法,很多时候只能通过重启应用来解决。因此我们解决死锁问题的最好办法是 规避死锁。

想要规避死锁,那么必须知道产生死锁的条件。产生死锁的条件(同时发生以下场景):

  • 互斥:共享资源X和Y只能被一个线程占有
  • 占有且等待:线程T1已经取得了共享资源X,在等待线程Y的时候,不释放共享资源X。
  • 不可抢占:去ITA线程不可以强行获取线程T1获取到资源
  • 循环等待:线程T1等待线程T2的资源,线程T2等待线程T1的资源,就是循环等待

因此我们可以破坏其中某一个或者多个条件,则可以避免死锁的出现。其中,互斥条件是无法破坏掉的,因为锁的本质就是互斥,其他三个方法都是有办法破坏掉的。

  • 占有且等待:可以使得线程一次性申请 所有需要的资源,就不存在等待问题了。体现在代码中:将所有操作放在一个临界区,及同时用synchronized锁定所需要的资源。
  • 不可抢占:可以使得线程 在占有部分资源同时进一步申请资源时,如果为成功申请到资源,就将已有资源释放掉。体现在代码中:synchronized无法实现主动释放资源,如果申请不到资源,线程就会进入阻塞状态,阻塞状态的线程啥也做不了,因此无法在Java语言层面解决,不会是可以通过SDK层面解决的,通过Java并发包中提供的lock是可以解决这个问题的。
  • 循环等待:指定线程申请资源的顺序。体现在代码中:对不同的资源设定不同的属性id,对id进行排序。

在选择对那种条件进行破坏的时候,需要结合具体情况进行判断,破坏那种条件的成本最低。

参考文章-1
参考文章-2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值