JMQ为提升性能,使用近乎无锁的设计:
- MQ中的锁是个必须使用的技术
- 使用锁会降低系统性能
如何正确使用锁?
异步和并发设计可大幅提升性能,但程序更复杂:多线程执行时,充斥不确定性。对一些需并发读写的共享数据,一着不慎满盘皆输。
案例:团建
老板说:“部门准备团建,愿意参加的回消息报名,统计下人数。都按我规定格式报名。”
老板发了:“A,1人”。这时候B和C都要报名,过一会儿,他俩几乎同时各发了一条消息,“B,2人”“C,2人”,每个人发的消息都只统计了老板和他们自己,一共2人,而这时,其实已经有3个人报名了,并且,在最后发消息的C的名单中,B的报名被覆盖。
典型并发读写导致的数据错误。使用锁可有效解决:任何时间都只能有一个线程持锁,持锁线程才能访问被锁保护的资源。
团建案例中,可认为群中有把锁,想要报名的人必须先拿到锁,然后才能更新名单。这就避免了多人同时更新消息,报名名单也就不会出错了。
避免滥用锁
难道遇到这种情况都用锁?
如果能不用锁,就不用锁;
如果你不确定是不是应该用锁,那也不要用锁。
因为使用锁虽然可以保护共享资源,但代价不小。
- 加锁和解锁都要CPU时间,这是性能损失。另外,使用锁就有可能导致线程等待锁,等待锁过程中线程是阻塞的状态,过多的锁等待会显著降低程序的性能
- 如果锁使用不当,很容易死锁,导致程序卡死。多线程本就难以调试,再加锁,出现并发问题或者死锁问题,程序更难调试。
所以,你在使用锁以前,一定要非常清楚明确地知道,这个问题必须要用一把锁来解决。切忌看到一个共享数据,也搞不清它在并发环境中会不会出现争用问题,就“为了保险,给它加个锁吧。”千万不能有这种不负责任的想法,否则你将会付出惨痛的代价!我曾经遇到过的严重线上事故,其中有几次就是由于不当地使用锁导致的。
只有并发下的共享资源不支持并发访问,或者并发访问共享资源会导致系统错误的情况下,才需使用锁。
锁的用法
在访问共享资源之前,先获取锁。
如果获取锁成功,就可以访问共享资源了。
最后,需要释放锁,以便其他线程继续访问共享资源。
Java使用锁
private Lock lock = new ReentrantLock(); public void visitShareResWithLock() { lock.lock(); try { // 在这里安全的访问共享资源 } finally { lock.unlock(); } }
也可以使用synchronized关键字,它的效果和锁是一样的:
private Object lock = new Object(); public void visitShareResWithLock() { synchronized (lock) { // 在这里安全的访问共享资源 } }
使用锁要注意:
使用完锁一定要释放。若在访问共享资源时抛异常,后面释放锁代码就不会再执行,导致死锁。所以要考虑代码可能走的所有分支,确保所有情况下的锁都能释放。
接下来我们说一下,使用锁的时候,遇到的最常见的问题:死锁。