前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >正确使用锁保护共享数据,协调异步线程

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

作者头像
JavaEdge
发布2021-02-22 16:08:45
4440
发布2021-02-22 16:08:45
举报
文章被收录于专栏:JavaEdgeJavaEdge

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

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

如何正确使用锁?

异步和并发设计可大幅提升性能,但程序更复杂:多线程执行时,充斥不确定性。对一些需并发读写的共享数据,一着不慎满盘皆输。

案例:团建

老板说:“部门准备团建,愿意参加的回消息报名,统计下人数。都按我规定格式报名。” 老板发了:“A,1人”。这时候B和C都要报名,过一会儿,他俩几乎同时各发了一条消息,“B,2人”“C,2人”,每个人发的消息都只统计了老板和他们自己,一共2人,而这时,其实已经有3个人报名了,并且,在最后发消息的C的名单中,B的报名被覆盖。

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

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

避免滥用锁

难道遇到这种情况都用锁? 如果能不用锁,就不用锁; 如果你不确定是不是应该用锁,那也不要用锁。

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

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

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

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

锁的用法

在访问共享资源之前,先获取锁。 如果获取锁成功,就可以访问共享资源了。 最后,需要释放锁,以便其他线程继续访问共享资源。

Java使用锁

代码语言:javascript
复制
private Lock lock = new ReentrantLock();

public void visitShareResWithLock() {
  lock.lock();
  try {
    // 在这里安全的访问共享资源
  } finally {
    lock.unlock();
  }
}

也可以使用synchronized关键字,它的效果和锁是一样的:

代码语言:javascript
复制
private Object lock = new Object();

public void visitShareResWithLock() {
  synchronized (lock) {
    // 在这里安全的访问共享资源
  }
}

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

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

避免死锁

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

导致死锁的原因不多

  1. 获取锁后没释放,有经验的程序员很少犯这种错误,即使出现也很容易解决
  2. 锁的重入 看下面代码:
代码语言:javascript
复制
public void visitShareResWithLock() {
  lock.lock(); // 获取锁
  try {
    lock.lock(); // 再次获取锁,会导致死锁吗?
  } finally {
    lock.unlock();
  }

当前的线程获取到了锁lock,然后在持有这把锁的情况下,再次去尝试获取这把锁,这样会导致死锁吗? 不一定。会不会死锁取决于,你获取的这把锁它是不是可重入锁。如果是可重入锁,那就没有问题,否则就会死锁。

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

最后一种死锁的情况是最复杂的,也是最难解决的。如果你的程序中存在多把锁,就有可能出现这些锁互相锁住的情况。 模拟最简单最典型的死锁情况。在这个程序里面,我们有两把锁:lockA和lockB,然后我们定义了两个线程,这两个线程反复地去获取这两把锁,然后释放。 程序执行一会儿就卡住了,发生死锁。 他们获取锁的顺序不一样。 第一个线程,先获取lockA,再获取lockB; 第二个线程正好相反,先获取lockB,再获取lockA。 这最简单的两把锁两个线程死锁的情况,还可以分析清楚,如果你的程序中有十几把锁,几十处加锁解锁,几百线程,如果出现死锁你还能分析清楚是什么情况吗?

避免死锁

  1. 程序尽量少用锁
  2. 同把锁,加锁和解锁必须放在同一方法
  3. 尽量避免同时持有多把锁,即持有一把锁时,又去获取另外一把锁
  4. 若需要持多把锁,注意加解锁顺序,解锁顺序要和加锁顺序相反
  5. 给你程序中所有的锁排一个顺序,在所有需要加锁的地方,按照同样的顺序加解锁。 如果两个线程都按照先获取lockA再获取lockB的顺序加锁,就不会产生死锁。

使用读写锁

共享数据,如果某方法访问它时,只读取,并不更新,就不需要加锁? 还是需要的,因为如果一个线程读时,另外一个线程同时在更新,那么你读数据有可能是更新到一半的。 所以,无论只读还是读写访问,都是需要加锁的。

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

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

读可并发执行。 写的同时不能并发读,也不能并发写。 这就兼顾性能和安全。读写锁就为这此而设计。 Java读写锁实例

代码语言:javascript
复制
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呢?

代码语言:javascript
复制
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();
}

}

}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-08-09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 案例:团建
  • 避免滥用锁
  • 锁的用法
    • Java使用锁
    • 避免死锁
    • 避免死锁
    • 使用读写锁
    • 在Java中实现一个try-with-lock呢?
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档