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),实现了锁的基本功能。
在这里插入图片描述

并使用FairSync内部类实现公平锁,使用NonfairSync实现非公平锁,这两个内部类都继承自Sync
在这里插入图片描述

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();
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值