目录
本文来源:极客时间vip课程笔记
一、引入前提
- 在消息队列中,第一个是,“锁”是一个必须要使用的技术。第二个是,使用锁其实会降低系统的性能。
- 使用异步和并发的设计可以大幅提升程序的性能,但我们为此付出的代价是,程序比原来更加复杂了,多线程在并行执行的时候,带来了很多不确定性。特别是对于一些需要多个线程并发读写的共享数据,如果处理不好,很可能会产出不可预期的结果,这肯定不是我们想要的。
- 我给你举个例子来说明一下,大家应该都参与过微信群投票吧?比如,群主说:“今晚儿咱们聚餐,能来的都回消息报一下名,顺便统计一下人数。都按我这个格式来报名。”然后,群主发了一条消息:“群主,1 人”。
- 这时候小六和无双都要报名,过一会儿,他俩几乎同时各发了一条消息,“小六,2 人”“无双,2 人”,每个人发的消息都只统计了群主和他们自己,一共 2 人,而这时候,其实已经有 3 个人报名了,并且,在最后发消息的无双的名单中,小六的报名被覆盖了。
- 这就是一个非常典型的由于并发读写导致的数据错误。使用锁可以非常有效地解决这个问题。锁的原理是这样的:任何时间都只能有一个线程持有锁,只有持有锁的线程才能访问被锁保护的资源。
- 在上面微信群报名的例子中,如果说我们的微信群中有一把锁,想要报名的人必须先拿到锁,然后才能更新报名名单。这样,就避免了多个人同时更新消息,报名名单也就不会出错了。
二、如何正确使用锁
2.1、避免滥用锁
2.1.1、使用锁原则
- 那是不是遇到这种情况都要用锁解决呢?我分享一下我个人使用锁的第一条原则:如果能不用锁,就不用锁;如果你不确定是不是应该用锁,那也不要用锁。为什么这么说呢?因为,虽然说使用锁可以保护共享资源,但是代价还是不小的。
- 只有在并发环境中,共享资源不支持并发访问,或者说并发访问共享资源会导致系统错误的情况下,才需要使用锁。
2.1.2、使用锁的代价
- 第一,加锁和解锁过程都是需要 CPU 时间的,这是一个性能的损失。另外,使用锁就有可能导致线程等待锁,等待锁过程中线程是阻塞的状态,过多的锁等待会显著降低程序的性能。
- 第二,如果对锁使用不当,很容易造成死锁,导致整个程序“卡死”,这是非常严重的问题。本来多线程的程序就非常难于调试,如果再加上锁,出现并发问题或者死锁问题,你的程序将更加难调试。
- 所以,你在使用锁以前,一定要非常清楚明确地知道,这个问题必须要用一把锁来解决。切忌看到一个共享数据,也搞不清它在并发环境中会不会出现争用问题,就“为了保险,给它加个锁吧。”千万不能有这种不负责任的想法,否则你将会付出惨痛的代价!我曾经遇到过的严重线上事故,其中有几次就是由于不当地使用锁导致的。
2.1.3、锁的用法
- 首先,在访问共享资源之前,先获取锁。
- 然后,如果获取锁成功,就可以访问共享资源了。
- 最后,需要释放锁,以便其他线程继续访问共享资源。比较容易出现状况的地方是,很多语言都有异常机制,当抛出异常的时候,不再执行后面的代码。如果在访问共享资源时抛出异常,那后面释放锁的代码就不会被执行,这样,锁就一直无法释放,形成死锁。所以,你要考虑到代码可能走到的所有正常和异常的分支,确保所有情况下,锁都能被释放
2.1.4、使用锁的例子
-
在 Java 语言中,使用锁的例子:
private Lock lock = new ReentrantLock(); public void visitShareResWithLock() { lock.lock(); try { // 在这里安全的访问共享资源 } finally { lock.unlock(); } }
-
也可以使用 synchronized 关键字,它的效果和锁是一样的:
private Object lock = new Object(); public void visitShareResWithLock()