并发容器J.U.C -- AQS同步组件(三)

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile变量(state)来维护同步状态

ReentrantLock

java中两类锁: Synchronized、 J.U.C中提供的锁。
ReentrantLock与Synchronized都是可重入锁,本质上都是lock与unlock的操作。

ReentrantLock 与synchronized 的区别

可重入性:两者的锁都是可重入的,差别不大,有线程进入锁,计数器自增1,等下降为0时才可以释放锁

锁的实现:synchronized是基于JVM实现的(用户很难见到,无法了解其实现),ReentrantLock是JDK实现的。
性能区别:在最初的时候,二者性能差很多,当synchronized引入了偏向锁、轻量级锁(自旋锁)后,二者的性能差别不大,官方推荐synchronized(写法更容易、在优化时其实是借用了ReentrantLock的CAS技术,试图在用户态就把问题解决,避免进入内核态造成线程阻塞)
功能区别:

  1. 便利性:synchronized更便利,它是由编译器保证加锁与释放。ReentrantLock是需要手动释放锁,所以为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
  2. 锁的细粒度和灵活度,ReentrantLock优于synchronized

ReentrantLock 的独有的功能

  1. ReentrantLock 可指定是公平锁和非公平
    (公平锁:先等待的线程先获得锁,默认使用非公平锁)
    synchronized 只能是非公平锁
  2. 提供了一个Condition类,可以分组唤醒需要唤醒的线程。
    synchronized 要么随机唤醒一个线程,要么全部唤醒
  3. 提供能够中断等待锁的线程机制,lock.lockInterruptibly()实现。ReentrantLock
    实现是一种自旋锁,通过循环调用cas自加操作,避免了线程进入内核态发生阻塞
    synchronized 不会忘记释放锁

ReentrantLock 获取锁释放锁

ReentrantLock类图

公平锁vs非公平锁

AQS里
int c = getState();// 获取锁的开始,首先读volatile变量state
与nonfairTryAcquire(int acquires)比较,唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
公平锁vs非公平锁

hasQueuedPredecessors方法

主要是用来判断线程需不需要排队,需要判断队列中有没有相关线程的节点已经在排队了。有则返回true表示current thread需要排队,没有则返回false则表示线程无需排队。

 //Queries whether any threads have been waiting to acquire longer than the current thread.(检测该链表有没有前面排队的节点)   
 public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

竞争锁

返回false

  • h!=t 为 false:表示队列只有一个元素 或为空
  1. 头尾节点都为null,表示队列都还是空的,甚至都没完成初始化,那么自然返回fasle,无需排队。
  2. h=t头尾节点指向一个元素,表示队列中只有一个节点,这时候自然无需排队,因为队列中的第一个节点是不参与排队的,它持有着同步状态,那么第二个进来的节点就无需排队,因为它的前继节点就是头节点,所以第二个进来的节点就是第一个能正常获取同步状态的节点,第三个节点才需要排队,等待第二个节点释放同步状态。
  • h != t为true表示队列中至少有两个不同节点存在。
    (s = h.next) == null || s.thread != Thread.currentThread()为false
  1. (s= h.next )== null 为false 表示: 头节点是有后继节点的(第二个元素不为空)
  2. s.thread != Thread.currentThread() 为false 表示 :当前线程和后继节点的线程是相同的(说明已经轮到这个线程相关的节点去尝试获取同步状态了,自然无需排队,直接返回fasle)

获取锁,返回false

返回true

  • h != t返回true表示:队列中至少有两个不同节点存在。
  • ((s = h.next) == null || s.thread != Thread.currentThread())返回true
  1. (s = h.next) == null 为true: 说明头节点之后是没有后继节点的.这情况可能发生在如下情景:有另一个线程已经执行到初始化队列的操作了,介于compareAndSetHead(new Node())与tail = head之间:
    头节点后没有后继节点
    这时候头节点不为null,而尾节点tail还没有被赋值,所以值为null,所以会满足h != t结果为true的判断,以及头节点的后继节点还是为null的判断,这时候可以直接返回true,表示要排队了,因为在当前线程还在做尝试获取同步状态的操作时,已经有另一个线程准备入队了,当前线程慢人一步,自然就得去排队
  2. (s = h.next) == null返回false:首节点有后继节点
    s.thread != Thread.currentThread()为true:后继节点的相关线程不是当前线程,所以当前线程自然得老老实实的去排队。

返回true :当前线程不是后继节点,不能获取锁,需要排到后面

释放锁

Sync:tryRelease()

//java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
/**
 * The synchronization state.
 */
private volatile int state;
protected final int getState() {return state;}
// 调用该方法表明该线程是持有锁的,因此不需要CAS操作
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//读取state
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 锁释放完了
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);// 释放锁的最后,写volatile变量state
            //free默认是false,只有c==0时才被置为true,否则都是false代表没有获取到锁
            return free;
        }

公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。int c = getState()- releases;
根据volatile的happens-before规则(一个volatile变量的写操作发生在这个volatile变量随后的读操作之前),释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变的对获取锁的线程可见。

对公平锁和非公平锁的内存语义

CAS和volatile, Java并发的基石

对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。

公平锁与非公平锁释放时,最后都要写一个volatile变量state
公平锁获取时,首先去读volatile变量
非公平锁获取时,首先用CAS更新volatile变量,该操作同时具有volatile读和volatile写的内存语义。

CAS如何同时具有volatile读和volatile写的内存语义?
编译器不会对volatile读与volatile读后面的任意内存操作重排序;
编译器不会对volatile写与volatile写前面的任意内存操作重排序。
组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。

综上:
公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。
非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

放弃synchronized?

ReentrantLock不仅拥有synchronized的所有功能,而且有一些功能synchronized无法实现的特性。性能方面,ReentrantLock也不比synchronized差,那么我们要不要放弃使用synchronized呢?答案是不要这样做。

J.U.C包中的锁定类是用于高级情况和高级用户的工具,除非说你对Lock的高级特性有特别清楚的了解以及有明确的需要,或这有明确的证据表明同步已经成为可伸缩性的瓶颈的时候,否则我们还是继续使用synchronized。相比较这些高级的锁定类,synchronized还是有一些优势的,比如synchronized不可能忘记释放锁。还有当JVM使用synchronized管理锁定请求和释放时,JVM在生成线程转储时能够包括锁定信息,这些信息对调试非常有价值,它们可以标识死锁以及其他异常行为的来源。

ReentrantLock的使用

//创建锁:使用Lock对象声明,使用ReentrantLock接口创建
private final static Lock lock = new ReentrantLock();
//使用锁:在需要被加锁的方法中使用
private static void add() {
    lock.lock();//获取锁
    try {
        count++;
    } finally {
        lock.unlock();//释放锁
    }
}

源码

//初始化方面:
//在new ReentrantLock的时候默认给了一个不公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//加参数来初始化指定使用公平锁还是不公平锁
//fair=true时公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 函数方法

tryLock():仅在调用时锁定未被另一个线程保持的情况下才获取锁定。
tryLock(long timeout, TimeUnit unit):如果锁定在给定的时间内没有被另一个线程保持且当前线程没有被中断,则获取这个锁定。
lockInterruptbily:如果当前线程没有被中断的话,那么就获取锁定。如果中断了就抛出异常。
isLocked:查询此锁定是否由任意线程保持
isHeldByCurrentThread:查询当前线程是否保持锁定状态。
isFair:判断是不是公平锁

Condition相关特性:

hasQueuedThread(Thread):查询指定线程是否在等待获取此锁定
hasQueuedThreads():查询是否有线程在等待获取此锁定
getHoldCount():查询当前线程保持锁定的个数,也就是调用Lock方法的个数

Condition的使用

Condition可以非常灵活的操作线程的唤醒,下面是一个线程等待与唤醒的例子,其中用1234序号标出了日志输出顺序

public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition = reentrantLock.newCondition();//创建condition
    //线程1
    new Thread(() -> {
        try {
            reentrantLock.lock();
            log.info("wait signal"); // 1
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("get signal"); // 4
        reentrantLock.unlock();
    }).start();
    //线程2
    new Thread(() -> {
        reentrantLock.lock();
        log.info("get lock"); // 2
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        condition.signalAll();//发送信号
        log.info("send signal"); // 3
        reentrantLock.unlock();
    }).start();
}

输出结果:
1–wait signal
2–get lock
3–send signal
4–get signal

输出过程讲解:
1、线程1调用了reentrantLock.lock(),线程进入AQS等待队列,输出1号log
2、接着调用了awiat方法,线程从AQS队列中移除,锁释放,直接加入condition的等待队列中
3、线程2因为线程1释放了锁,拿到了锁,输出2号log
4、线程2执行condition.signalAll()发送信号,输出3号log
5、condition队列中线程1的节点接收到信号,从condition队列中拿出来放入到了AQS的等待队列,这时线程1并没有被唤醒。
6、线程2调用unlock释放锁,因为AQS队列中只有线程1,因此AQS释放锁按照从头到尾的顺序,唤醒线程1
7、线程1继续执行,输出4号log,并进行unlock操作。

读写锁:ReentrantReadWriteLock

排他锁在同一时刻只允许一个线程进行访问(ReentrantLock属于排他锁)。
读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行。

使用场景

public class LockExample3 {
    private final Map<String, Data> map = new TreeMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();//读锁
    private final Lock writeLock = lock.writeLock();//写锁
    //加读锁
    public Data get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }
    //加写锁  设置key对应的value,并返回旧的value
    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    class Data {}
}

票据锁:StempedLock

写、读、乐观读
一个StempedLock的状态是由版本模式两个部分组成。锁获取方法返回一个数字作为票据(stamp),他用相应的锁状态表示并控制相关的访问。数字0表示没有写锁被授权访问,在读锁上分为【悲观读、乐观读】。

乐观读: 读多写少,乐观的认为读、写操作同时发生几率很小,因此不悲观的使用完全的读取锁定。程序可以查看读取之后是否遭到写入的变更,再采取相应措施。

使用

//定义
private final static StampedLock lock = new StampedLock();
//需要上锁的方法
private static void add() {
    long stamp = lock.writeLock();
    try {
        count++;
    } finally {
        lock.unlock(stamp);
    }
}

源码

class Point {
        private double x, y;
        private final StampedLock sl = new StampedLock();
        void move(double deltaX, double deltaY) {
            long stamp = sl.writeLock();
            try {
                x += deltaX;
                y += deltaY;
            } finally {
                sl.unlockWrite(stamp);
            }
        }

        //下面看看乐观读锁案例
        double distanceFromOrigin() { // A read-only method
            long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
            double currentX = x, currentY = y;  //将两个字段读入本地局部变量
            if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
                stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
                try {
                    currentX = x; // 将两个字段读入本地局部变量
                    currentY = y; // 将两个字段读入本地局部变量
                } finally {
                    sl.unlockRead(stamp);
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }

        //下面是悲观读锁案例
        void moveIfAtOrigin(double newX, double newY) { // upgrade
            // Could instead start with optimistic, not read mode
            long stamp = sl.readLock();
            try {
                while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                    if (ws != 0L) { //这是确认转为写锁是否成功
                        stamp = ws; //如果成功 替换票据
                        x = newX; //进行状态改变
                        y = newY;  //进行状态改变
                        break;
                    } else { //如果不能成功转换为写锁
                        sl.unlockRead(stamp);  //我们显式释放读锁
                        stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
                    }
                }
            } finally {
                sl.unlock(stamp); //释放读锁或写锁
            }
        }
    }

总结

synchronized :JVM实现,可通过一些监控工具监控,出现未知异常时,JVM会自动帮助释放锁。
ReetrantLock 、ReetrantReadWriteLock 、StempedLock都是对象层面的锁定,为保证锁一定释放,要放到finally里才会更安全,StempedLock对性能有很大改进,特别是读线程越来越多情况。

如何选择锁

1、当只有少量竞争者,使用synchronized
2、竞争者不少但是线程增长的趋势是能预估的,使用ReetrantLock

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值