天天看点

正确使用锁保护共享数据,协调异步线程(上)案例:团建避免滥用锁锁的用法

JMQ为提升性能,使用近乎无锁的设计:

  1. MQ中的锁是个必须使用的技术
  2. 使用锁会降低系统性能

如何正确使用锁?

异步和并发设计可大幅提升性能,但程序更复杂:

多线程

执行时,充斥不确定性。对一些需并发读写的共享数据,一着不慎满盘皆输。

案例:团建

老板说:“部门准备团建,愿意参加的回消息报名,统计下人数。都按我规定格式报名。”

老板发了:“A,1人”。这时候B和C都要报名,过一会儿,他俩几乎同时各发了一条消息,“B,2人”“C,2人”,每个人发的消息都只统计了老板和他们自己,一共2人,而这时,其实已经有3个人报名了,并且,在最后发消息的C的名单中,B的报名被覆盖。

典型并发读写导致的数据错误。使用锁可有效解决:任何时间都只能有一个线程持锁,持锁线程才能访问被锁保护的资源。

团建案例中,可认为群中有把锁,想要报名的人必须先拿到锁,然后才能更新名单。这就避免了多人同时更新消息,报名名单也就不会出错了。

避免滥用锁

难道遇到这种情况都用锁?

如果能不用锁,就不用锁;

如果你不确定是不是应该用锁,那也不要用锁。

因为使用锁虽然可以保护共享资源,但代价不小。

  1. 加锁和解锁都要CPU时间,这是性能损失。另外,使用锁就有可能导致线程等待锁,等待锁过程中线程是阻塞的状态,过多的锁等待会显著降低程序的性能
  2. 如果锁使用不当,很容易死锁,导致程序卡死。多线程本就难以调试,再加锁,出现并发问题或者死锁问题,程序更难调试。

所以,你在使用锁以前,一定要非常清楚明确地知道,这个问题必须要用一把锁来解决。切忌看到一个共享数据,也搞不清它在并发环境中会不会出现争用问题,就“为了保险,给它加个锁吧。”千万不能有这种不负责任的想法,否则你将会付出惨痛的代价!我曾经遇到过的严重线上事故,其中有几次就是由于不当地使用锁导致的。

只有并发下的共享资源不支持并发访问,或者并发访问共享资源会导致系统错误的情况下,才需使用锁。

锁的用法

在访问共享资源之前,先获取锁。

如果获取锁成功,就可以访问共享资源了。

最后,需要释放锁,以便其他线程继续访问共享资源。

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) {
    // 在这里安全的访问共享资源
  }
}      

使用锁要注意:

使用完锁一定要释放。若在访问共享资源时抛异常,后面释放锁代码就不会再执行,导致死锁。所以要考虑代码可能走的所有分支,确保所有情况下的锁都能释放。

接下来我们说一下,使用锁的时候,遇到的最常见的问题:死锁。