高并发是在做项目中不可避免的问题,因此也成了大厂面试中高频的面试问题,因此对高并发的一些问题进行总结整理。
文章目录
一、Java中提供了synchronized,为什么还要提供Lock呢?
既然JVM中提供了synchronized关键字来保证只有一个线程能够访问同步代码块,为何还要提供Lock接口呢?这是在重复造轮子吗?Java的设计者们为何要这样做呢?
1.1 为何提供Lock接口?
在Java 1.5
版本中,synchronized的性能不如Lock,但在Java 1.6
版本之后,synchronized做了很多优化,性能提升了不少。那既然synchronized关键字的性能已经提升了,那为何还要使用Lock呢?
原因是:我们使用synchronized加锁是无法主动释放锁
的,这就会涉及到死锁
的问题。
1.2 死锁问题
如果要发生死锁,则必须存在以下四个必要条件,四者缺一不可。
- 互斥条件
在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
- 不可剥夺条件
线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
- 请求与保持条件
线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件
在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
1.3 synchronized的局限性
如果我们的程序使用synchronized关键字发生了死锁时,synchronized关键是是无法破坏“不可剥夺
”这个死锁的条件的。这是因为synchronized申请资源的时候, 如果申请不到, 线程直接进入阻塞状态了, 而线程进入阻塞状态, 啥都干不了, 也释放不了线程已经占有的资源
。
然而,在大部分场景下,我们都是希望“不可剥夺”这个条件能够被破坏。也就是说对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时, 如果申请不到, 可以主动释放它占有的资源, 这样不可剥夺这个条件就破坏掉了。
如果我们自己重新设计锁来解决synchronized的问题,我们该如何设计呢?
1.4 解决synchronized的问题
我们在设计锁的时候,要如何解决synchronized的局限性问题呢?这里,可以从三个方面来思考这个问题。
能够响应中断
。synchronized的问题是, 持有锁A后, 如果尝试获取锁B失败, 那么线程就进入阻塞状态, 一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号, 也就是说当我们给阻塞的线程发送中断信号的时候, 能够唤醒它,那它就有机会释放曾经持有的锁A。这样就破坏了不可剥夺条件了。支持超时
。如果线程在一段时间之内没有获取到锁, 不是进入阻塞状态, 而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可剥夺条件。非阻塞地获取锁
。如果尝试获取锁失败, 并不进入阻塞状态, 而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可剥夺条件。
体现在Lock接口上,就是Lock接口提供的三个方法,如下所示。
// 支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException