记录一些多线程中和锁有关的内容,想到什么写什么,学到什么写什么
互斥锁
互斥锁的出现是为了解决线程原子性问题的。
原子性问题出现的原因是因为cpu分配使得线程1还没执行完就轮到线程2执行了,线程2还偏偏需要操作与线程1相同的对象,这就导致了两线程都执行完了,但执行的结果与顺序执行的结果不一样的情况出现。 为了解决这种情况我们可以不让cpu切换,从而实现单线程操作。但这种情况只适用于单核cpu的情况,一旦是多核cpu就还是会出现问题。所以为了安全,我们就需要使线程独占操作的对象。所以就有了加互斥锁的操作。
synchronized(java提供的锁技术)
锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块。
synchronized会自动进行加锁和解锁操作,无需我们手动使用,所以使用时只管使用synchronized即可。
锁与保护资源的关系应该是N:1。即一把锁可以保护多个资源对象,而一个资源对象仅能同时被一把锁保护。因为不同锁之间不是互斥的。这就会导致锁A中在对资源1的操作修改对锁B中的操作是不可见的,这就会引起并发问题。
那互斥锁如何保护多个资源?
- 多个不相关的资源:如一个银行账户有余额和用户密码两个资源,这两个资源在业务的关联性没那么大,这样就可以各自持有一把锁各管各的。可以提供一把锁来同时管理这两种资源但结果就会是线程的并发度降低。所以,各管各的低粒度锁效果好
- 相关的资源:还是上一个例子,有转账的业务,这种功能的实现涉及到了两个对象的同一个资源:余额。线程A要将用户1的钱打到用户2上,那么线程A就要对转账操作加锁,这样问题也就来了:线程A只能保护自己的余额而不能管另一个用户的,这样用户2的线程B也执行操作就会引起混乱。所以这种情况下我们要加一个锁可以同时保护住所有的相同资源才行,比较好的解决方法是对class类加锁,所有的用户都是同一个class类,所以用class类加锁的效果就很好。
死锁
上面的情况很好的解决了资源被乱用导致的并发问题,但所有的相同资源都被锁住了又会引发效率的问题,因为用户1要给用户2转账,就把所有的用户都锁住,那用户3想给用户4转账这时候就会被锁住无法操作,只能等用户1操作完。这就使得所有操作都必须串行化才行,这在今天显然是不可以的。
解决:既然不能都锁住又要保护操作的双方的安全,那我只对操作双方进行加锁不就可以了嘛。
但这样还是带来了问题就是死锁。比如两个线程先分别拿到用户1和用户2的锁,然后又想拿对方的锁,这时候就尴尬了,双方都想拿到对方的锁来完成操作,自己又不能释放自己持有的锁,这样俩线程就只能一直干等,等到死,这种情况就是死锁。
死锁一旦产生最好的方法就是重启,所以为了解决死锁我们要避免死锁的产生。
死锁产生的条件如下:
- 互斥,共享资源×和Y只能被一个线程占用;
- 占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;
- 不可抢占,其他线程不能强行抢占线程T1占有的资源;
- 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。
所以为了避免死锁,只要破坏上面的条件就可以了。
- 用锁就是为了互斥,所以不能从这方面来入手。
- 占有且等待,可以让线程直接申请好所有的资源后再进行操作。实际中可以写一个获取资源和释放资源的加锁函数方法,在调用真正的业务逻辑之前先使用获取资源的方法来获取所有用到的资源,一直请求直到请求成功,之后就执行业务逻辑,执行完成后再释放掉资源。
- 不可抢占,破坏该点的核心是能主动释放资源,但synchronize本身是无法完成这种操作的。但java.util.concurrent 这个包下面提供的 Lock是可以轻松解决这个问题。
- 循环等待,破坏这个条件,需要对资源进行排序,然后按序申请资源。