本文主要是介绍互斥锁 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
这篇关于互斥锁 Synchronized的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!