【每日一学】(Java多线程)可重入锁

今天学习的是可重入锁。
参考资料:https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA

一、什么是锁

使用Java进行多线程开发,使用锁是一个不可避免的问题

(1)锁的含义
  1. 在计算机科学中,锁是一种同步机制,在有许多执行线程的环境中强制对资源的访问控制。
  2. 锁旨在强制实施互斥排它,并发控制策略。

我的理解,锁如同字面含义的“锁”。我进了一个房间,把门锁上,不让其他人进来。在Java中,我(某一线程)在做某个事情(执行某方法)时加锁,只允许它自己执行该方法,别人不允许插队进来。

(2)为什么要使用锁
  1. 场景

有一个User对象,包含一下两个属性。有一个线程A进入,对该对象进行写操作。为username赋完值,还没来得及对password进行操作。此时,线程B进入,直接读这个对象,此时,它只读了半个数据。线程B认为该对象的手机号的不存在。

public class User{
	private String username;
	private String password;
}
  1. 出现原因
    由于写用户名和写密码不是原子操作
  2. 解决方案
    写操作前,加锁,不允许其他线程使用该资源。
    写完数据,释放锁。
    读线程加锁,不允许其他线程操作
    读完数据,释放锁。

二、什么是可重入锁

  1. 如果一个线程中,连续两次对同一把锁进行lock(这里,我不是很清楚。意思是一个线程,多次加lock?),这个线程就会被卡死
  2. 实际开发中,一个线程调用多个方法,可能出现方法的嵌套。如主方法调用methodA(),methodA()调用methodB(),若是mainA()中存在lock,methodB()也存在lock,线程就会卡死。
  3. 可重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,不会导致线程卡死。唯一需要保证的是lock()的次数和unlock()的次数一致

三、Java中的重入锁

  1. 所在包
    java.util.concurrent.locksReentrantLock

  2. 主要方法

void lock()
加锁,如果锁已经被占用了,就无限等待。它提供了锁最基本的功能,拿到就返回,拿不到就等待。

boolean tryLock(long timeout,TimeUnti unit) throws InterruptedException
尝试获取锁,等待timeout时间,可以响应中断(程序收到关机信号,中断就会触发,进入中断异常,线程可以做一些清理工作)。

它的好处:

  • 可以不用进行无线等待,如果一段时间拿不到锁,可以直接放弃,同时释放自己已经得到的资源。避免死锁的产生。
  • 可以在应用层进行自旋,自己决定尝试几次,或者放弃
  • 可以响应中断,进入中断异常后,线程可以做一些清理工作,防止终止线程时出现数据写坏,数据丢失等异常情况。

public boolean trylock()
不带参数的trylock()方法更加直接,如果能获取到锁,直接返回true;获取失败,返回false。特别适合在应用层自己对锁进行管理,在应用层自旋等待。

关于自旋
自旋锁是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能被成功获取,直到获取到锁才会退出循环

void unlock() 释放锁

四、重入锁的实现原理

  1. ReentrantLock实现了Lock接口
  2. 重入锁的功能由它的内部类Sync实现,根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。(后期将学习策略模式)
(1)重入锁的实现
  1. AbstractQueuedSynchronizer对象中存在一个变量state。重入锁的实现,就是基于这个状态变量。它(state)的值表示当前线程重复占用这个锁的次数
    0表示锁是空闲的
    private volatile int state;
  2. Sync中的方法
  final void lock() {
            //compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            //否则,说明别的线程已经使用了这个锁,那么就需要等待
                acquire(1);
        }
  1. 关于acquire()方法,它也是在AbstractQueuedSynchronizer中。若是详细学,还可以将if()中涉及的方法理解。
 public final void acquire(int arg) {
        //1.当tryAcquire()为true,表示获取锁成功,更新state的值。不再执行&&后的相关操作
        //2.tryAcquire()为false(表示获取锁失败),执行&&后面的操作。入队等待,直到获取锁,如果在等待过程中,被中断了,那么重新把标志位设置上。
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
(2)公平锁的实现

默认情况下,重入锁是不公平的

  1. 公平与非公平
    如果有1,2,3,4四个线程,按顺序,依次请求锁。
    若是非公平,则拿到锁的线程是随机的;若是公平,则按照顺序获得锁。
  2. 公平锁的声明
    ReentrantLock fairLock = new ReentrantLock(true);
    新建重入锁时,传入参数,表示它是一个公平锁;默认不传参,即是非公平锁。使用公平锁对性能有影响
  3. 公平锁与非公平锁实现的区别
    如果,非公平锁第一次争抢失败,后面的操作和公平锁一样,都是入队等待
//非公平锁
 final void lock() {
            //直接抢占资源,抢不到(else)则入队等待
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
//公平锁,直接入队等待
 final void lock() {
            acquire(1);
        }

tryLock()的实现

//非公平锁
   final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //直接抢资源
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果当前线程占用了锁,更新一下state,表示重复占用的次数,这是重入的关键
            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;
        }
        
//公平锁
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //先看看有没有在等待,没有人等我才会去抢,有人在我前面,我就不抢了。
                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;
        }

五、Condition

(1)含义

可以理解为重入锁的伴生对象,它提供了在重入锁的基础上,进行等待和通知的机制
可以使用newCondition()方法生成一个Condition对象。

private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
(2)使用场景

java.util.concurrent包下的ArrayBlockingQueue,它是一个队列,和普通队列一样,有入队(enqueue(E x))和出队(take())的操作。
但是,有个条件,如果队列是空的,那么take就需要等待,一直到有元素了,再返回。

(3)使用方式,结合ArrayBlockingQueue

首先,初始化,ArrayBlockingQueue中的构造方法中维护了一个Condition对象:notEmpty,它用来通知其他线程,队列是否是空着的。

 public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

当我们需要从队列中拿出一个元素时

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        //如果队列长度为0,那么就在notEmpty condition上等待,一直到有元素进来为止。
        //注意,await()方法,一定是要先获取Condition伴生的那个lock。
            while (count == 0)
                notEmpty.await();
            return dequeue();  //一旦队列中有数据,count不为0,就返回出队
        } finally {
            lock.unlock();
        }
    }

当有元素入队时

 public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        //先拿到锁,拿到锁才能操作对应的Condition对象。
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            //入队,在这个函数中,就会有notEmpty的通知,通知相关线程,有数据准备好了。
            enqueue(e);
            return true;
        } finally {
        //释放锁
            lock.unlock();
        }
    }

  private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //发出一个信号量,元素已经放好了,通知那个拿东西的人吧
        notEmpty.signal();
    }

整个流程图


六、重入锁的使用示例

public class Counter {

    //重入锁
    private final Lock lock = new ReentrantLock();
    private int count;

    private void incr(){
        //访问count时,需要加锁。
        lock.lock();
        try {
            count++;
        }finally {
            lock.unlock();
        }
    }

    private int getCount(){
        //读取数据时也要加锁,才能保证数据的可见性
        lock.lock();

        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        
    }
}

七、总结

  1. 对于同一个线程,重入锁允许你反复获得同一把锁,但是,申请和释放锁的次数要一致。
  2. 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平的。
  3. 重入锁的内部实现是基于CAS操作
  4. 重入锁的伴生对象Condition提供了await()和singal()的功能,可用于线程间消息通信

小记

本打算每日一篇。但是一个知识点连带的其他知识点想要搞清楚,还是有点复杂,我不想糊弄自己,还是希望多花时间理解清楚。

涉及源码,根据不同阶段自己的水平慢慢理解。

学习不停,积累不停,你会走得更远~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值