正确使用锁保护共享数据,协调异步线程(下)

避免死锁

  • 死锁
    由于某种原因,锁一直没释放,后续需要获取锁的线程都将处于等待锁的状态,这样程序就卡死。


导致死锁的原因不多

  1. 获取锁后没释放,有经验的程序员很少犯这种错误,即使出现也很容易解决
  2. 锁的重入

看下面代码:

public void visitShareResWithLock() {
  lock.lock(); // 获取锁
  try {
    lock.lock(); // 再次获取锁,会导致死锁吗?
  } finally {
    lock.unlock();
  }

当前的线程获取到了锁lock,然后在持有这把锁的情况下,再次去尝试获取这把锁,这样会导致死锁吗?

不一定。会不会死锁取决于,你获取的这把锁它是不是可重入锁。如果是可重入锁,那就没有问题,否则就会死锁。


大部分编程语言都提供了可重入锁,若无特别要求,尽量使用可重入锁。因为若程序复杂,调用栈很深,很多情况下,当需要获取一把锁时,你不太好判断在n层调用之外的某个地方,是不是已经获取过这把锁,这时,获取可重入锁就有必要。



最后一种死锁的情况是最复杂的,也是最难解决的。如果你的程序中存在多把锁,就有可能出现这些锁互相锁住的情况。

模拟最简单最典型的死锁情况。在这个程序里面,我们有两把锁:lockA和lockB,然后我们定义了两个线程,这两个线程反复地去获取这两把锁,然后释放。

程序执行一会儿就卡住了,发生死锁。

他们获取锁的顺序不一样。

第一个线程,先获取lockA,再获取lockB;

第二个线程正好相反,先获取lockB,再获取lockA。

这最简单的两把锁两个线程死锁的情况,还可以分析清楚,如果你的程序中有十几把锁,几十处加锁解锁,几百线程,如果出现死锁你还能分析清楚是什么情况吗?


避免死锁


程序尽量少用锁

同把锁,加锁和解锁必须放在同一方法

尽量避免同时持有多把锁,即持有一把锁时,又去获取另外一把锁

若需要持多把锁,注意加解锁顺序,解锁顺序要和加锁顺序相反

给你程序中所有的锁排一个顺序,在所有需要加锁的地方,按照同样的顺序加解锁。

如果两个线程都按照先获取lockA再获取lockB的顺序加锁,就不会产生死锁。



使用读写锁


共享数据,如果某方法访问它时,只读取,并不更新,就不需要加锁?

还是需要的,因为如果一个线程读时,另外一个线程同时在更新,那么你读数据有可能是更新到一半的。

所以,无论只读还是读写访问,都是需要加锁的。


锁虽然解决安全问题,但牺牲性能无法并发。


若无线程在更新,即使多线程并发读,也没问题。大部分情况下,数据读要远多于写,所以,我们希望的是:


读可并发执行。

写的同时不能并发读,也不能并发写。

这就兼顾性能和安全。读写锁就为这此而设计。

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()方法可多线程并行执行,这使读性能很好。

写数据,获取写锁,当一个线程持有写锁,其他线程既无法获取读锁,也不能获取写锁,从而保护共享数据。

如此读写锁就兼顾了性能和安全。


在Java中实现一个try-with-lock呢?

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();
}

}

}
上一篇:java多线程--线程池的使用


下一篇:《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一2.6 内存映射文件