【JDK1.8】JUC.Lock综述

一、前言

前段时间结束了jdk1.8集合框架的源码阅读,在过年的这段时间里,一直在准备JUC(java.util.concurrent)的源码阅读。平时接触的并发场景开发并不很多,但是有网络的地方,就存在并发,所以想找几本书阅读深入一下,看到网上推荐较多的两本书《Java并发编程实战》和《Java多线程编程核心技术》。看了两书的优缺点后,笔者选择了先看后者,据说代码例子较多,书到手后,看完后的印象就是对并发的关键字、几个常见类的api进行了介绍,内容挺早以前,讲的也是不是很深,对Java SE5新加的类介绍很少,只能说对于刚接触并发编程的人来说,还是值得一看的。

二、java.util.concurrent.locks图概览

在JUC的包里面,有一个包专门用于存放锁相关的类,笔者将其中的大部分内容整理进了下面UML中:

具体关系大家可以去看看UML的关系图,顺便介绍个生成UML的工具:PlantUml,UML界的markdown,真的挺好用。

图中要提的是:圆圈里面有个+的关系,代表内部类,笔者为了图片看的更简洁,把ReentrantLockSemaphore等类中的内部类Sync合到了一起,其实它们是一个类,只不过都叫这个名字。

从图中我们可以看到,关系较为紧密的是AbstractQueuedSynchronizer抽象类,而它则直接依赖了LockSupport这个类,笔者将在后面先分析这个类的源码。

三、基础接口的源码解析

3.1 Lock接口

在JDK1.5以后,添加了Lock接口,它用于实现与Synchronized关键字相同的锁操作,来实现多个线程控制对共享资源的访问。但是能提供更加灵活的结构,可能具有完全不同的属性,并且可能支持多个相关的Condition对象。基本用法如下:

Lock l = ...;
l.lock();
try {
    // 访问被锁保护的资源
} finally {
    l.unlock();
}

下面我们来简单看一下它下面的具体内容:

public interface Lock {
    // 获得锁资源
    void lock();
    // 尝试获得锁,如果当前线程被调用了interrupted则中断,并抛出异常,否则就获得锁
    void lockInterruptibly() throws InterruptedException;
    // 判断能否获得锁,如果能获得,则获得锁,并返回true(此时已经获得了锁)
    boolean tryLock();
    // 保持给定的等待时间,如果期间能拿到锁,则获得锁,同样如果期间被中断,则抛异常
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    // 返回与此Lock对象绑定Condition实例
    Condition newCondition();
}

其中,tryLock只会尝试一次,如果返回false,则走false的流程,不会一直让线程一直等待。

3.2 Condition接口

Condition与Lock要结合使用,使用Condition可以用来实现wait()notify()/notifyAll()类似的等待/通知模式。与Object对象里不同的是,Condition更加灵活,可以在一个Lock对象里创建多个Condition实例,有选择的进行线程通知,在线程调度上更加灵活。使用Condition注释上的例子:

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull  = lock.newCondition(); 
    final Condition notEmpty = lock.newCondition(); 

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            // 当count等于数组的大小时,当前线程等待,直到notFull通知,再进行生产
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            // 当count为0,进入等待,直到notEmpty通知,进行消费。
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

可以通过多个线程来调用put和take方法,来模拟生产者和消费者。 我们来换成常规的wait/notify的实现方式:

class BoundedBuffer {
    private final Object lock;
    
    public BoundedBuffer(Object lock) {
        this.lock = lock;
    }
    public void put(Object x) {
        try {
            synchronized (items) {
                while (count == items.length) {
                    items.wait();
                }
                items[putptr] = x;
                if (++putptr == items.length) putptr = 0;
                ++count;
                // items.notify();
                items.notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public Object take() {
        try {
            synchronized (items) {
                while (count == 0) {
                    items.wait();
                }
                Object x = items[takeptr];
                if (++takeptr == items.length) takeptr = 0;
                --count;
                // items.notify();
                items.notifyAll();
                return x;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

如果将items.notifyAll()换成items.notify(),在多生产者和多消费者模式情况下,可能出现take唤醒了take的情况,导致生产者在等待消费者消费,而消费者等待生产者生产,最终导致程序无限等待,而用notifyAll(),则唤醒所有的生产者和消费者,不像Condition可以选择性的通知。下面我们来看一下它的源码:

public interface Condition {
    // 让当前线程等待,直到被通知或者被中断
    void await() throws InterruptedException;
    // 与前者的区别是,当等待过程中被中断时,仍会继续等待,直到被唤醒,才会设置中断状态
    void awaitUninterruptibly();
    // 让当前线程等待,直到它被告知或中断,或指定的等待时间已经过。
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    // 与上面的类似,让当前线程等待,不过时间单位是纳秒
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    // 让当前线程等待到确切的指定时间,而不是时长
    boolean awaitUntil(Date deadline) throws InterruptedException;
    // 唤醒一个等待当前condition的线程,有多个则随机选一个
    void signal();
    // 唤醒所有等待当前condition的线程
    void signalAll();
}

3.3 ReadWriteLock接口

读写锁与一般的互斥锁不同,它分为读锁和写锁,在同一时间里,可以有多个线程获取读锁来进行共享资源的访问。如果此时有线程获取了写锁,那么读锁的线程将等待,直到写锁释放掉,才能进行共享资源访问。简单来说就是读锁与写锁互斥。

读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。

public interface ReadWriteLock {
    // 返回写锁
    Lock writeLock();
    // 返回读锁
    Lock readLock();
}

再看一下源码里提供的示例:

class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        // 获得写锁
        rwl.readLock().lock();
        // 缓存无效,则重写数据
        if (!cacheValid) {
            // 在获得写锁之前,必须先释放读锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 重写检查一次,因为其他线程可能在这段时间里获得了写锁,并且修改了状态
                if (!cacheValid) {
                    data = ...
                        cacheValid = true;
                }
                // 在释放写锁之前,通过获取读锁来降级。
                rwl.readLock().lock();
            } finally {
                // 释放写锁
                rwl.writeLock().unlock();
            }
        }
        // cacheValid,直接获取数据,并释放读锁
        try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}

ReentrantReadWriteLock中,读锁可以获取写锁,而返过来,写锁不能获得读锁,所以在上面代码中,要先释放写锁,再获取读锁,具体的源码分析后面再细说。

四、总结

开了个新坑,边看边学。最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linyb极客之路

并发编程之各种锁的简介

一、公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优...

3566
来自专栏java达人

多线程设计模式解读2—Promise(承诺)模式

上次我们讲到多线程设计模式的Guarded Suspension(保护性暂挂模式),Guarded Suspension是条件未满足时线程一直处于等待状态,直到...

733
来自专栏老司机的技术博客

java面试必备之ThreadLocal

按照传统的经验,如果某个对象是非线程安全的,在多线程环境下对象的访问需要采用synchronized进行同步。但是模板类并未采用线程同步机制,因为线程同步会降低...

962
来自专栏Java职业技术分享

「编程架构实战」——Java并发包基石-AQS详解

protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;

120
来自专栏Java职业技术分享

【编程架构实战】——Java并发包基石-AQS详解

 Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共...

60
来自专栏Brian

Python 多线程的同步方法

---- 概述 这篇博客是我翻译Python threads synchronization: Locks, RLocks, Semaphores, Condi...

4056
来自专栏待你如初见

JavaIO流输入输出流-字符流

721
来自专栏三流程序员的挣扎

RxJava 变换操作符

按照规定大小缓存,每次取 count 个数,取完一次跳过 skip 个数,将每次取的数据合并到一个列表里。

1745
来自专栏Java职业技术分享

【编程架构实战】——Java并发包基石-AQS详解

 Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共...

80
来自专栏皮皮之路

【JDK1.8】JUC.Lock综述

3338

扫码关注云+社区