Java 线程知识笔记 (十) 常用的锁工具

15 篇文章 1 订阅
9 篇文章 0 订阅

前言

上一篇【Java 线程知识笔记 (九) 并发框架AQS】我们在最后写了一个自己的锁工具,如果每次进行开发的时候都整一套那也太麻烦了,好在Java已经在内部提供了一整套的锁工具,比如可重入锁工具ReentrantLock,信号量Semaphore,计数器CountDownLatch等等做个简单的讲解和例子。本来不想写这方面的内容,因为网上的例子是在太多了。考虑到这是一个记录线程知识笔记的系列帖子,本着宁滥勿缺的原则还是补上比较好。多线程基础比较好的读者,可以跳过不用往下看了。更多线程知识内容请点击【Java 多线程和锁知识笔记系列】

ReentrantLock

可重入锁,所谓的可重入锁就是同一个资源已经被某一个线程锁住,该线程再此使用的时候可以直接分配该资源,而不是等待自己的锁释放,实现这样功能的锁就是可重入锁。

public class ReentrantLockTest {
    private ReentrantLock lock = new ReentrantLock();
    public void lock1() {
        lock.lock(); // lock1加锁
        System.out.println("lock1 locked");
        lock2();
        lock.unlock(); // lock1释放锁
        System.out.println("lock1 unlocked");
    }
    public void lock2() {
        lock.lock(); // lock2加锁
        System.out.println("lock2 locked");
        lock.unlock(); // lock2释放锁
        System.out.println("lock2 unlocked");
    }
    public static void main(String[] args) {
        ReentrantLockTest demo=new ReentrantLockTest();
        new Thread(demo::lock1).start();
    }
}
输出打印:
lock1 locked
lock2 locked
lock2 unlocked
lock1 unlocked

上面的例子其实就是说明了ReentrantLock是一个可重入锁。因为假设ReentrantLock不是可重入锁,让我们看看会发生什么。线程中的lock首先会到lock1()中加锁输出lock1 locked;然后执行到lock2()方法里,再此执行lock.lock(),此时由于lock1()lock.lock()并没有释放,因此lock2()方法里lock.lock()只能停下来自己等待自己释放资源,于是变成了死锁状态。这点大家可以把上篇中示例中else if块删除掉,然后重新运行上面代码测试,非重入锁将会卡在lock1 locked这条输出后无法执行也无法结束。一般来说在Java开发中如果有用到锁的地方,都会用到ReentrantLock这个工具。

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
官网上对它的定义可以说是非常准确了,说:是一个可重入的排他锁,并且其基本语法和行为都和隐式使用监视器锁的synchronized方法和块一样,但是更具有扩展性。

这里笔者要吐槽一下,到目前为止Java对synchronized关键字已经进行了多次优化,但是对于ReentrantLock这个外部实现类基本上没啥变化,可见谁是亲儿子谁是后娘养的一目了然,因此未来某个版本synchronized直接把ReentrantLock代替了,一点都不奇怪。

ReentrantLock构造方法
ConstructorDescription
ReentrantLock()Creates an instance of ReentrantLock.
ReentrantLock(boolean fair)Creates an instance of ReentrantLock with the given fairness policy.

发现ReentrantLock又分出来了公平锁与非公平锁。默认就是非公平锁,如果在构造方法里传递true,那么就是一个公平锁。公平锁与非公平锁这俩又是什么概念呢?

公平锁与非公平锁

公平锁,其实就是排队,先来先得FIFO的逻辑,因此每个锁都是公平的。非公平锁,就是采用一定的算法或者优先度去对线程拿到锁的顺序进行调整,因此每个锁并不是公平获取资源的。这两个锁各有优缺点,使用谁要看具体需求,比如紧急刹车作为一个功能来说,就必须用非公平锁。

ReentrantLock主要方法

Modifier and TypeMethodDescription
intgetHoldCount()Queries the number of holds on this lock by the current thread.
protected ThreadgetOwner()Returns the thread that currently owns this lock, or null if not owned.
protected Collection<Thread>getQueuedThreads()Returns a collection containing threads that may be waiting to acquire this lock.
intgetQueueLength()Returns an estimate of the number of threads waiting to acquire this lock.
protected Collection<Thread>getWaitingThreads(Condition condition)Returns a collection containing those threads that may be waiting on the given condition associated with this lock.
intgetWaitQueueLength(Condition condition)Returns an estimate of the number of threads waiting on the given condition associated with this lock.
booleanhasQueuedThread(Thread thread)Queries whether the given thread is waiting to acquire this lock.
booleanhasQueuedThreads()Queries whether any threads are waiting to acquire this lock.
booleanhasWaiters(Condition condition)Queries whether any threads are waiting on the given condition associated with this lock.
booleanisFair()Returns true if this lock has fairness set true.
booleanisHeldByCurrentThread()Queries if this lock is held by the current thread.
booleanisLocked()Queries if this lock is held by any thread.
voidlock()Acquires the lock.
voidlockInterruptibly()Acquires the lock unless the current thread is interrupted.
ConditionnewCondition()Returns a Condition instance for use with this Lock instance.
booleantryLock()Acquires the lock only if it is not held by another thread at the time of invocation.
booleantryLock(long timeout, TimeUnit unit)Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been interrupted.
voidunlock()Attempts to release this lock.

从上面是现实的所有方法,可以看到ReentrantLock和上篇我们的例子中在主要功能上几乎没有什么差别,但是ReentrantLock更加的强大,能够实现公平与非公平锁,能够实现返回相关线程的数据,线程的队列信息,线程的条件等等功能。

ReentrantReadWriteLock

除了重入锁以外,Java还提供了重入读写锁,这种锁的语法类似于ReentrantLock,但是这个类并不会影响Reader或者Writer获取锁的访问顺序,但是它能够支持公平锁策略。

An implementation of ReadWriteLock supporting similar semantics to ReentrantLock. This class does not impose a reader or writer preference ordering for lock access. However, it does support an optional fairness policy.

ReentrantReadWriteLock是基于ReentrantLock实现的ReadWriteLock接口类,而且也是唯一实现该接口的类,两者的区别在于ReentrantReadWriteLock的主要方法实现都在本身的静态内部类中,也是使用的AQS框架,相当于AQS框架又嵌套了两个AQS框架的实现。

Modifier and TypeMethodDescription
ReentrantReadWriteLock.ReadLockreadLock()Returns the lock used for reading.
ReentrantReadWriteLock.WriteLockwriteLock()Returns the lock used for writing.

这个的用法相对来说比较少,一般用来读不可修改的时候。比如一个数据先加上读锁reader.lock(),让任何线程都无法修改。如果需要修改先去掉读锁reader.unlock(),然后加上写锁writer.lock(),修改完毕以后,先加读锁reader.lock(),然后释放写锁writer.unlock()。这种写锁到读锁的操作叫做锁降级,反之由读锁到写锁的过程叫做锁升级。要提醒一点ReentrantReadWriteLock并不支持锁升级,为了避免死锁,不要混在一起使用,只要把握一点就好:哪里释放哪里上锁,没释放过的不加锁。

CountDownLatch

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
官网说这是一个同步辅助器,允许一个或者多个线程等待,直到一个共同组里的线程全部完成为止。

简单说就是一个线程减法计数器。它只有一个构造方法,count参数就是描述需要完成任务的线程个数。就是说在完成某个功能之前,要等待其他N个功能完成才能继续执行,这个N就是传入的count。每执行完成一个任务,计数器减一,计数器到0的时候,说明全部任务完成,主程序继续往下执行。

CountDownLatch构造方法
ConstructorDescription
CountDownLatch(int count)Constructs a CountDownLatch initialized with the given count.
CountDownLatch主要方法
Modifier and TypeMethodDescription
voidawait()Causes the current thread to wait until the latch has counted down to zero, unless the thread is interrupted. 唤醒,全部任务执行完毕唤醒主线程执行
booleanawait(long timeout, TimeUnit unit)Causes the current thread to wait until the latch has counted down to zero, unless the thread is interrupted, or the specified waiting time elapses. 设置等待时间,如果接口对时间有要求,可以通过这个设置起到直接唤醒的作用。
voidcountDown()Decrements the count of the latch, releasing all waiting threads if the count reaches zero.递减计数器,如果计数达到0则释放所有的等待。
longgetCount()Returns the current count. 拿到当前的计数

它的使用场景是什么呢?比如都用过携程、支付宝或者百度的第三方提供的各种票务系统去查询酒店、机票、电影院等等余票信息,或者直接订票吧。那么比如我现在需要查询某天的酒店的空余房间,一个查询过去,分别会调用7天、如家、速8等等这些企业提供的接口,为了展示结果的正确性,必须等待他们三家返回的数据才能执行输出,此时用CountDownLatch就非常的合适。

public class CountDownLatchTest {
    private static List<String> queryList=new ArrayList<>();
    private static List<String> resultList=new ArrayList<>();
 	//便于演示的原因,日期就不算了
    public static void main(String[] args) throws InterruptedException {
        queryList.add("7天");
        queryList.add("速8");
        queryList.add("如家");
        CountDownLatch downLatch=new CountDownLatch(queryList.size());
        System.out.println("开始查询:");
        for (int i = 0; i <queryList.size() ; i++) {
            int queryId = i;
            new Thread(()->{
                resultList.add(queryList.get(queryId)+":今天有房间。"); //拿到结果存下
                downLatch.countDown(); //计数器减一
            }).start();
            TimeUnit.SECONDS.sleep(1); //延时
        }
        //唤醒主线程
        downLatch.await();
        System.out.println("查询结果如下:");
        resultList.forEach(System.out::println); //输出结果
    }
}
输出结果,打印如下
开始查询:
查询结果如下:
7:今天有房间。
速8:今天有房间。
如家:今天有房间。

在这里插入图片描述

CyclicBarrier

上面说完做减法的,这个工具就是做加法的。每当完成一个任务加一,直到加到预定的数量,然后继续执行主程序。相当于给每个线程设置一个屏障,当所有的线程都到这个屏障前面的时候,一起打开执行。

CyclicBarrier构造参数
ConstructorDescription
CyclicBarrier(int parties)Creates a new CyclicBarrier that will trip when the given number of parties (threads) are waiting upon it, and does not perform a predefined action when the barrier is tripped.指定拦截的数量,数量达到要求以后执行等待的线程
CyclicBarrier(int parties, Runnable barrierAction)Creates a new CyclicBarrier that will trip when the given number of parties (threads) are waiting upon it, and which will execute the given barrier action when the barrier is tripped, performed by the last thread entering the barrier. 不仅指定数量,还指定线程任务,当数量达到以后,会直接执行指定的任务,然后在执行等待的线程
CyclicBarrier主要方法
Modifier and TypeMethodDescription
intawait()Waits until all parties have invoked await on this barrier.
intawait(long timeout, TimeUnit unit)Waits until all parties have invoked await on this barrier, or the specified waiting time elapses.
intgetNumberWaiting()Returns the number of parties currently waiting at the barrier.
intgetParties()Returns the number of parties required to trip this barrier.
booleanisBroken()Queries if this barrier is in a broken state.
voidreset()Resets the barrier to its initial state.

CyclicBarrier在自己的内部定义了一个ReentrantLock,每当一个线程调用await()方法时,将拦截的线程数加一,然后判断剩余拦截数是不是等于parties。如果不等于,进入ReentrantLock对象的条件队列等待;如果相等,执行指定Runnable对象的任务(如果有的话),然后给所有等待的线程分配锁、释放锁执行自己的任务。这种工具就比较适合线程集合完毕后才开始主线程的任务,比如游戏中下副本,只有玩家Ready的人数到达一定数量了才能开副本。

public class CyclicBarrierTest {
    private static int playerNums=5;
    public static void main(String[] args) {
        CyclicBarrier barrier=new CyclicBarrier(playerNums); //玩家数5人
        for (int i = 0; i <playerNums ; i++) {
            int num=i;
            new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5)); //模拟玩家准备时间
                    System.out.println("玩家"+num+"到达副本,已经Ready。"); //玩家点击Ready按钮
                    barrier.await(); //等待其他玩家Ready
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("玩家"+num+"开始加载副本。"); //全部的玩家都Ready,开始加载副本
            }).start();
        }
        System.out.println("全部玩家Ready,开始加载副本");
    }
}
输出打印:
全部玩家Ready,开始加载副本
玩家1到达副本,已经Ready。
玩家0到达副本,已经Ready。
玩家4到达副本,已经Ready。
玩家3到达副本,已经Ready。
玩家2到达副本,已经Ready。
玩家2开始加载副本。
玩家1开始加载副本。
玩家3开始加载副本。
玩家0开始加载副本。
玩家4开始加载副本。

通过例子看目的基本已经达到,5位玩家必须都到齐了才能开始加载副本,但是由于主线程运行的最快,直接开始加载副本了,明显不符合要求,所以修改一下使用两个参数的构造方法。

public class CyclicBarrierMoreTest implements Runnable{
    private static int playerNums=5;
    public static void main(String[] args) {
        CyclicBarrier barrier=new CyclicBarrier(playerNums, new CyclicBarrierMoreTest()); //玩家数5人
        for (int i = 0; i <playerNums ; i++) {
            int num=i;
            new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5)); //模拟玩家准备时间
                    System.out.println("玩家"+num+"到达副本,已经Ready。"); //玩家点击Ready按钮
                    barrier.await(); //等待其他玩家Ready
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("玩家"+num+"开始加载副本。"); //全部的玩家都Ready,开始加载副本
            }).start();
        }
    }

    @Override
    public void run() {
        System.out.println("全部玩家Ready,开始加载副本");
    }
}
输出打印:
玩家1到达副本,已经Ready。
玩家0到达副本,已经Ready。
玩家4到达副本,已经Ready。
玩家3到达副本,已经Ready。
玩家2到达副本,已经Ready。
全部玩家Ready,开始加载副本
玩家2开始加载副本。
玩家3开始加载副本。
玩家4开始加载副本。
玩家0开始加载副本。
玩家1开始加载副本。

这次任务顺序就是正确的了,次数到了会先执行Runnable指定的任务。
在这里插入图片描述

上面两个类比较类似,但是执行await()方法的逻辑正好相反。CountDownLatch适用于多个线程合作才能出一个结果的场景,而CyclicBarrier倾向于所有的线程准备好了才往下执行的场景,大家可以再自己品味一下,然后选择合适的类使用。

Semaphore

A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each acquire() blocks if necessary until a permit is available, and then takes it. Each release() adds a permit, potentially releasing a blocking acquirer. However, no actual permit objects are used; the Semaphore just keeps a count of the number available and acts accordingly. Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource.
计数信号量。一个信号量维护一个许可证集合。如果有必要的话,每一个acquire()方法都会阻塞,直到一个许可证是可用的,然后获取这个许可证。每一个release()都会添加一个许可证,潜在的释放一个正在阻塞的请求者。但是许可证并没有一个实际的使用对象;信号量只是保持一定可用数量的计数和相应的行为。信号量通常限制线程数量,而不是能够访问某些资源(无论是物理资源还是逻辑资源)。

官方文档说的很复杂,其实只有最后一句是有用的。信号量就是可以使用的资源数量。

Semaphore构造参数
ConstructorDescription
Semaphore(int permits)Creates a Semaphore with the given number of permits and nonfair fairness setting.
Semaphore(int permits, boolean fair)Creates a Semaphore with the given number of permits and the given fairness setting.

两个构造参数基本没什么区别,permits都是初始化许可集的数量类似线程池,此外只是是否实现公平锁的差别,ture为公平。

Semaphore主要方法
Modifier and TypeMethodDescription
voidacquire()Acquires a permit from this semaphore, blocking until one is available, or the thread is interrupted.获取一个许可
voidacquire(int permits)Acquires the given number of permits from this semaphore, blocking until all are available, or the thread is interrupted.获取给定数量的许可
voidacquireUninterruptibly()Acquires a permit from this semaphore, blocking until one is available.
voidacquireUninterruptibly(int permits)Acquires the given number of permits from this semaphore, blocking until all are available.
intavailablePermits()Returns the current number of permits available in this semaphore.
intdrainPermits()Acquires and returns all permits that are immediately available.
protected Collection<Thread>getQueuedThreads()Returns a collection containing threads that may be waiting to acquire.
intgetQueueLength()Returns an estimate of the number of threads waiting to acquire.
booleanhasQueuedThreads()Queries whether any threads are waiting to acquire.
booleanisFair()Returns true if this semaphore has fairness set true.
protected voidreducePermits(int reduction)Shrinks the number of available permits by the indicated reduction.
voidrelease()Releases a permit, returning it to the semaphore.释放一个许可
voidrelease(int permits)Releases the given number of permits, returning them to the semaphore.释放给定数量的许可
booleantryAcquire()Acquires a permit from this semaphore, only if one is available at the time of invocation.
booleantryAcquire(int permits)Acquires the given number of permits from this semaphore, only if all are available at the time of invocation.
booleantryAcquire(int permits, long timeout, TimeUnit unit)Acquires the given number of permits from this semaphore, if all become available within the given waiting time and the current thread has not been interrupted.
booleantryAcquire(long timeout, TimeUnit unit)Acquires a permit from this semaphore, if one becomes available within the given waiting time and the current thread has not been interrupted.

信号量的使用一般分为三步:请求资源acquire();使用资源进行业务处理;释放资源release()。注意acquire()release()应该是成对出现的。常见的场景比如公司停车场,请求停车获取车位,停车进去下来上班,工作完了开车走人释放车位。

public class SemaphoreTest {
    public static void main(String[] args) {
        //创建信号量semaphore,停车场能停5辆车
        Semaphore request=new Semaphore(5);
        /*request.hasQueuedThreads();//是否有等待的线程
        request.getQueueLength();//拿到等待队列长度*/
        //请求许可,一个线程代表一个汽车,一共10辆车
        for (int i = 0; i < 10; i++) {//依次进行停车请求
            new Thread(()->{
                try {//请求停车位(请求资源)
                    TimeUnit.SECONDS.sleep(2);
                    request.acquire();
                    System.out.println(Thread.currentThread().getName()+"请进入停车场。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {//进入停车场(使用资源)
                    int val=new Random().nextInt(5);
                    TimeUnit.SECONDS.sleep(val);
                    System.out.println(Thread.currentThread().getName()+"停留了"+val+"秒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {//离开停车场(释放资源)
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                request.release();
                System.out.println(Thread.currentThread().getName()+"离开停车场。");
            },"Car["+i+"]").start();
        }
    }
}

一开始会直接停5辆车进入,然后随机某个车出来释放资源,下一辆车才能进去,依次类推,直到10辆车全部离开停车场为止。由于输出是动态的,就不贴打印内容了。

总结

本篇简单的介绍了ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier,Semaphore等等官网解释,以及使用场景和例子,多线程工具类很多,想起来了再往这篇博客里补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值