Java中的锁你了解多少?

在多线程的编程中,我们经常会涉及到锁的使用。今天来聊一聊Java中的锁。

一、悲观锁

1.1 含义

坏事一定会发生,所以不管进行任何操作前,先上锁。

1.2 常见实现:

数据库中的行锁,表锁,读锁,写锁,

以及Java中的Synchornized关键字都是悲观锁的实现。

二、乐观锁

2.1 含义

坏事未必会发生,如果发生了再做处理。

自旋锁(CAS)是一种常见的乐观锁实现。CAS全称 CompareAndSwrap

2.2 说明:

比如:对变量i进行++操作,写入数据库之前会重新获取i值,如果值发生了改变,则重新将新值进行++;入库之前再去判断值有没有发生改变,若发生改变,重复上述操作。这样一次次循环,直到值未发生改变,写入数据库。

Java中的AtomicInteger底层实现的就是CAS

2.3 ABA问题

上述例子中对变量i进行入库前检查时,可能会有一个问题,i可能经过了由mn又变为m的情况,这个时候数据会写入成功,但不代表数据是没有问题的。

ABA问题的常见解决方案:

  • 版本号
  • Boolean

版本号: 在每一次对i的操作,都进行版本号的修改,最终以版本号是否改变来判断是否入库。

Boolean: 添加一个Bollean类型的标识,比如默认为false,,如果发生了改变,修改为true

以上两种方式都可以解决ABA问题,至于怎么选择:如果不在乎值改变的次数,可使用Boolean方式,否则使用版本号方式。

三、排他锁、共享锁

3.1 排他锁

也成为独享锁、独占锁。

锁在同一时刻只能有一个线程使用,同一时刻不能被多个线程一同占用,一个线程占用后其它线程只能等待。

ReentrantLocksynchronizedReentrantReadWriteLock的写锁等都是排他锁的实现。

3.2 共享锁

锁在同一时刻可以被多个线程共享使用,一个线程对资源加了共享锁后其它线程对资源也只能加共享锁。共享锁有着很好的读性能。

ReentrantReadWriteLock的读锁就是一种共享锁的实现。

  • 获取排他锁的线程可以读或写数据;
  • 获取共享锁的线程只能读取数据,不能修改数据。(在共享锁的代码块中修改数据,可能会导致其他获取共享锁的线程对数据不可见!)

六、统一锁、分段锁

  • 统一锁: 大粒度的锁
  • 分段锁: 分成一段一段的小粒度的锁

6.1 举例:

统一锁: 一个线程锁定A等待B,一个线程锁定B等待A,就会容易造成死锁。这个时候就可以将A+B统一称为大锁。

分段锁: 比如有一个特别长的链表,几万甚至几十万的数据。当多线程对这个链表插入元素的时候,每次插入,都要锁定整个链表,效率会非常低。如果想要提高效率,可以将此链表分为一段一段,每次添加元素,只需要锁定某一段即可。

七、公平锁、非公平锁

7.1 原理

  • 公平锁: 多线程中保障了各线程获取锁的顺序,先到的线程优先获取锁;

  • 非公平锁: 多线程中可能后来的线程先获取到锁。

以下是Java并发包下获取公平锁和非公平锁的源码:

// 公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键代码 hasQueuedPredecessors() 判断是否有队列 或 是否是队列的第一个
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// 非公平锁
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键代码 无须判断队列
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0// overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平锁在获取锁之前,需要判断等待队列是否为空,或者自己是否是队列的第一个,满足此条件,才可以获取到锁。

对于非公平锁,则不需要判断队列,当线程来获取锁时,如果持有锁的线程刚巧释放锁,这个时候排在队列中的第一个线程还没有被唤醒(因为线程的上下文切换是需要不少开销的),非公平锁的线程就可以成功抢占到锁;否则,就要像公平锁一样排队等待。

上面说的线程切换的开销,其实也正是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率。

ReentrantLockReadWriteLock 默认都是非公平锁模式。

7.2 ReentrantLock 的使用

// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
try {
    if (fairLock.tryLock()) {
        log.info("--------获取到公平锁--------");
    }
finally {
    fairLock.unlock();
}

// 非公平锁
ReentrantLock nonFairLock = new ReentrantLock(false);
try {
    if (nonFairLock.tryLock()) {
        log.info("--------获取到非公平锁--------");
    }
finally {
    nonFairLock.unlock();
}

ReentrantLock内部类Sync继承自AbstractQueuedSynchronizer类(AQS),实现了锁的基本功能。

Java中的锁你了解多少?

并使用FairSync内部类实现公平锁,使用NonfairSync实现非公平锁,这两个内部类都继承自Sync

Java中的锁你了解多少?

7.3 饥饿效应

正是因为非公平锁获取锁时是不公平的,因此可能导致排队的线程迟迟获取不到锁,进而形成饥饿效应。

虽然非公平锁有饥饿效应,但它相对于公平锁在获取锁的性能上更优,不会像公平锁一样每次都需要通知队列中的等待者去获取锁。

如何解决饥饿效应?

饥饿效应产生的根本原因是:线程在排队等待获取锁的过程中,非公平锁使用插队的方式来减少CPU的开销,而导致后边的线程一直在等待。

解决的方法可以是,让等待时间过长的线程有重新获取锁的机会。可以给每一个等待的线程设置一个超时时间,超时后可以重新获取一次锁。

代码示例:

ReentrantLock nonfair = new ReentrantLock(false);
try {
  while (nonfair.tryLock(1, TimeUnit.SECONDS)) {
    if (nonfair.isLocked()) {
      System.out.println("获取公平锁成功!");
    }
  }
catch (InterruptedException e) {
  nonfair.unlock();
finally {
  nonfair.unlock();
}


原文始发于微信公众号(连帆起航):Java中的锁你了解多少?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/239262.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!