JMQ为提升性能,使用近乎无锁的设计:
如何正确使用锁?
异步和并发设计可大幅提升性能,但程序更复杂:多线程执行时,充斥不确定性。对一些需并发读写的共享数据,一着不慎满盘皆输。
老板说:“部门准备团建,愿意参加的回消息报名,统计下人数。都按我规定格式报名。”
老板发了:“A,1人”。这时候B和C都要报名,过一会儿,他俩几乎同时各发了一条消息,“B,2人”“C,2人”,每个人发的消息都只统计了老板和他们自己,一共2人,而这时,其实已经有3个人报名了,并且,在最后发消息的C的名单中,B的报名被覆盖。
典型并发读写导致的数据错误。使用锁可有效解决:任何时间都只能有一个线程持锁,持锁线程才能访问被锁保护的资源。
团建案例中,可认为群中有把锁,想要报名的人必须先拿到锁,然后才能更新名单。这就避免了多人同时更新消息,报名名单也就不会出错了。
难道遇到这种情况都用锁?
如果能不用锁,就不用锁;
如果你不确定是不是应该用锁,那也不要用锁。
因为使用锁虽然可以保护共享资源,但代价不小。
所以,你在使用锁以前,一定要非常清楚明确地知道,这个问题必须要用一把锁来解决。切忌看到一个共享数据,也搞不清它在并发环境中会不会出现争用问题,就“为了保险,给它加个锁吧。”千万不能有这种不负责任的想法,否则你将会付出惨痛的代价!我曾经遇到过的严重线上事故,其中有几次就是由于不当地使用锁导致的。
只有并发下的共享资源不支持并发访问,或者并发访问共享资源会导致系统错误的情况下,才需使用锁。
在访问共享资源之前,先获取锁。
如果获取锁成功,就可以访问共享资源了。
最后,需要释放锁,以便其他线程继续访问共享资源。
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) {
// 在这里安全的访问共享资源
}
}
使用锁要注意:
使用完锁一定要释放。若在访问共享资源时抛异常,后面释放锁代码就不会再执行,导致死锁。所以要考虑代码可能走的所有分支,确保所有情况下的锁都能释放。
接下来我们说一下,使用锁的时候,遇到的最常见的问题:死锁。
导致死锁的原因不多
public void visitShareResWithLock() {
lock.lock(); // 获取锁
try {
lock.lock(); // 再次获取锁,会导致死锁吗?
} finally {
lock.unlock();
}
当前的线程获取到了锁lock,然后在持有这把锁的情况下,再次去尝试获取这把锁,这样会导致死锁吗?
不一定。会不会死锁取决于,你获取的这把锁它是不是可重入锁。如果是可重入锁,那就没有问题,否则就会死锁。
大部分编程语言都提供了可重入锁,若无特别要求,尽量使用可重入锁。因为若程序复杂,调用栈很深,很多情况下,当需要获取一把锁时,你不太好判断在n层调用之外的某个地方,是不是已经获取过这把锁,这时,获取可重入锁就有必要。
最后一种死锁的情况是最复杂的,也是最难解决的。如果你的程序中存在多把锁,就有可能出现这些锁互相锁住的情况。
模拟最简单最典型的死锁情况。在这个程序里面,我们有两把锁:lockA和lockB,然后我们定义了两个线程,这两个线程反复地去获取这两把锁,然后释放。
程序执行一会儿就卡住了,发生死锁。
他们获取锁的顺序不一样。
第一个线程,先获取lockA,再获取lockB;
第二个线程正好相反,先获取lockB,再获取lockA。
这最简单的两把锁两个线程死锁的情况,还可以分析清楚,如果你的程序中有十几把锁,几十处加锁解锁,几百线程,如果出现死锁你还能分析清楚是什么情况吗?
共享数据,如果某方法访问它时,只读取,并不更新,就不需要加锁?
还是需要的,因为如果一个线程读时,另外一个线程同时在更新,那么你读数据有可能是更新到一半的。
所以,无论只读还是读写访问,都是需要加锁的。
锁虽然解决安全问题,但牺牲性能无法并发。
若无线程在更新,即使多线程并发读,也没问题。大部分情况下,数据读要远多于写,所以,我们希望的是:
读可并发执行。
写的同时不能并发读,也不能并发写。
这就兼顾性能和安全。读写锁就为这此而设计。
Java读写锁实例
ReadWriteLock rwlock = new ReentrantReadWriteLock();
public void read() {
rwlock.readLock().lock();
try {
// 在这儿读取共享数据
} finally {
rwlock.readLock().unlock();
}
}
public void write() {
rwlock.writeLock().lock();
try {
// 在这儿更新共享数据
} finally {
rwlock.writeLock().unlock();
}
}
需要读数据的时候,我们获取读锁,不是互斥锁,read()方法可多线程并行执行,这使读性能很好。
写数据,获取写锁,当一个线程持有写锁,其他线程既无法获取读锁,也不能获取写锁,从而保护共享数据。
如此读写锁就兼顾了性能和安全。
java7开始io就有try-with-resource。
可以利用这一个特性,来说实现,自动释放。
代码如下:
public class AutoUnlockProxy implements Closeable {
private Lock lock;
public AutoUnlockProxy(Lock lock) {
this.lock = lock;
}
@Override
public void close() throws IOException {
lock.unlock();
System.out.println("释放锁");
}
public void lock() {
lock.lock();
}
public void tryLock(long time, TimeUnit unit) throws InterruptedException {
lock.tryLock(time, unit);
}
public static void main(String[] args) {
try (AutoUnlockProxy autoUnlockProxy = new AutoUnlockProxy(new ReentrantLock())) {
autoUnlockProxy.lock();
System.out.println("加锁了");
} catch (IOException e) {
e.printStackTrace();
}
}
}