java高并发程序设计:JUC
前言
本文为作者学习笔记,内容包括了自己的一些理解,所以可够准确,希望路过的小伙伴可以指出来。
一、多线程的团队协作:同步控制
1.1重入锁
- 重温一下造成死锁的条件:
1.互斥,共享资源 X 和 Y 只能被一个线程占用;
2.占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
3.不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
4.循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 - 前面在介绍死锁问题的时候,提出了一个破坏不可抢占条件方案,但是这个方案 synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
问:如果我们重新设计一把互斥锁去解决这个问题,那该怎么设计呢?
答:三种方案。1.能够响应中断。**synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。**但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。2.支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。3.非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这三个方案就是“重复造轮子”的主要原因,**体现在 API 上,就是 Lock 接口的三个方法。**详情如下:
// 支持中断的API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
-
由以上结论可以知道,重入锁由JDK实现,主要应用于解决死锁问题。与synchroized由jvm实现的方式不一样。有一个概念先说清楚,不然后面混淆,重入锁和可重入锁是两个概念,重入锁指的是由jdk实现的reentrantLock锁,可重入锁指的是一种可重入概念,是一种现象,synchroized也是可重入锁,与可重入相对的就是不可重入锁,指的是当拿到过一次锁后再次拿锁会被阻塞。重入锁的写法参考如下:
可以明显的看到重入锁的加锁释放锁的过程是显示的,这也意味着重入锁的逻辑控制灵活性相对synchroized较高。 -
重入锁允许一个线程连续重复的获得同一把锁,只需要也释放同样数量的锁就可以了,如果释放的锁数量不够,会造成死锁。
1.1.1 中断响应(重入锁的特点)
与synchroized不同,重入锁ReentrantLock提供中断功能,不同于synchroized锁在线程等待锁时只有两种可能:1.获得锁继续执行,2.保持等待。ReentrantLock可以根据需要取消对锁的等待去做其他的事。比如当发生死锁的时候就需要有人先释放出琐资源才能解决,这时候利用中断就非常的重要了。
注意:重入锁的加锁过程是由开发人员操作的,除了lock()方法申请锁外还有其他的,比如这里要申请可以中断的锁,就需要用lockInterruptibly()方法申请可中断锁。
1.1.2 锁申请等待限时(重入锁的特点)
- 使用重入锁的tryLock()方法设置申请锁的等待时间,如果超过了等待的时间就会放弃申请锁。显然,这又是一个解决死锁的好方法。
- 当tryLock()方法不带参数时(也就是没设置等待时间)如果没获取到锁该线程会立即放弃等待锁并返回false(tryLock()是布尔类型)。
1.1.3 公平锁(重入锁的特点)
- 公平锁会按照时间的先后顺序,保证先到的先得到锁资源,最大的特点就是公平锁不会产生饥饿现象。
- 重入锁设置参数可以控制锁是否为公平锁,而synchroized锁只能是非公平的。使用方法如下:
1.1.4 重入锁实现的三大要素
- 原子状态:使用CAS技术来存储当前锁的状态,判断锁是否被别的线程占用。
- 等待队列:没请求到锁的线程会被放到等待队列中进行等待。
- 阻塞原语parl()和unpark()