深刻Java Lock锁

上篇文章已经对多线程有个初步的认识了,此次咱们来看看Java的Lock锁,主要有如下知识点:java

  • AQS
  • ReentrantLock
  • ReentrantReadWriteLock
  • Lock和synchronized的选择

在学习Lock锁以前,咱们先来看看什么是AQS?算法

AQS

  • AQS其实就是一个能够给咱们实现锁的框架,juc包中不少可阻塞的类好比ReentrantLock、 ReadWriteLock都是基于AQS构建的。
  • 内部实现的关键是:先进先出的队列、state状态
  • 在AQS中实现了对等待队列的默认实现,子类只要重写部分的代码便可实现(大量用到了模板代码)
  • AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不一样的同步逻辑。通常状况下,子类只须要根据需求实现其中一种模式,固然也有同时实现两种模式的同步类,如ReadWriteLock
注意:ReentrantLock不是AQS的子类,其内部类Sync才是AQS的子类。

image

State状态

AQS维护了一个volatile int类型的state变量,用来表示当前同步状态。
volatile虽然不能保证操做的原子性,可是保证了当前变量state的可见性。多线程

compareAndSetState

compareAndSetState用来修改state状态,它是一个原子操做,底层实际上是调用系统的CAS算法,有关CAS可移步:CAS框架

protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  }

请求资源

acquire

acquire(int arg) 以独占方式获取资源,若是获取到资源,线程直接返回,不然进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。函数

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

若是tryAcquire(int)方法返回true,则acquire直接返回,不然当前线程须要进入队列进行排队。
addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;学习

ReentrantLock

学习ReentrantLock以前先来看看它实现的Lock接口ui

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。
  • unLock()方法是用来释放锁的。
  • newCondition()方法是建立一个条件对象,用来管理那些获得锁可是不能作有用工做的线程。

ReentrantLock,意思是"可重入锁",线程能够重复地得到已经持有的锁。 ReentrantLock是惟一实现了Lock接口的类。接下来咱们来看看有关源码:this

AQS子类

ReentrantLock实现了三个内部类,分别是Sync、NonfairSync和FairSyncspa

abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync

这些内部类都是AQS的子类,这就印证了咱们以前所说的:AQS是ReentrantLock的基础,AQS是构建锁的框架.线程

构造器

public ReentrantLock() {
        sync = new NonfairSync();
    }
    
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
   }

默认实现的是非公平锁,传入true表示使用公平锁。

加锁

  • ReentrantLock中加锁使用的是lock方法
  • 默认使用非公平锁的lock方法

加锁流程

首先会经过CAS方法,尝试将当前的AQS中的State字段改为从0改为1,若是修改为功的话,说明原来的状态是0,并无线程占用锁,并且成功的获取了锁,只须要调用setExclusiveOwnerThread函将当前线程设置成持有锁的线程便可。不然,CAS操做失败以后,和普通锁同样,调用父类AQS的acquire(1)函数尝试获取锁。

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

     
        final void lock() {
            if (compareAndSetState(0, 1))//尝试获取锁
                setExclusiveOwnerThread(Thread.currentThread());
            else  //获取失败则调用AQS的acquire方法
                acquire(1);
        }

而在AQS的acquire(1)函数中,会判断tryAcquire(1)以及acquireQueued(addWaiter(Node.EXCLUSIVE), arg),若是尝试获取失败而且添加队列成功的话,那么就会调用selfInterrupt函数中断线程执行,说明已经加入到了AQS的队列中。

注意:AQS的tryAcquire(1)是由子类Sync(也就是ReentrantLockd的静态内部类)本身实现的,也就是用到了模板方法,接下来咱们去看看子类的实现。

tryAcquire是在NonfairSync类中实现的,其中调用了nonfairTryAcquire函数。

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;
        }

nonfairTryAcquire函数中,会尝试让当前线程去获取锁:

  1. 获取当前线程,以及AQS的状态
  2. 若是当前AQS的状态为0的话,那么说明当前的锁没有被任何线程获取,则尝试作一次CAS操做,将当前的状态设置成acquires,若是设置成功了的话,那么则将当前线程设置成锁持有的线程,而且返回true,表示获取成功。
  3. 若是当前的状态不为0的话,说明已经有线程持有锁,则判断当前线程与持有锁的线程是否相同,若是相同的话,则将当前的状态加上acquires从新将状态设置,而且返回true,这也就是重入锁的缘由。
  4. 若是当前线程没有获取到锁的话,那么就会返回false,表示获取锁失败。

源码参考:ReentrantLock中的NonfairSync加锁流程

ReentrantReadWriteLock

概述

咱们知道synchronized内置锁和ReentrantLock都是互斥锁(一次只能有一个线程进入到临界区(被锁定的区域))

而ReentrantReadWriteLock是一个读写锁

  • 在读取数据的时候,能够多个线程同时进入到到临界区(被锁定的区域)
  • 在写数据的时候,不管是读线程仍是写线程都是互斥

通常来讲:咱们大多数都是读取数据得多,修改数据得少。因此这个读写锁在这种场景下就颇有用了!

ReentrantReadWriteLock实现了ReadWriteLock接口.
接口只有两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操做分开,分红2个锁来分配给线程,从而使得多个线程能够同时进行读操做

public interface ReadWriteLock {  
    Lock readLock();
    Lock writeLock();
}

性质

  • 读锁不支持条件对象,写锁支持条件对象
  • 读锁不能升级为写锁,写锁能够降级为读锁
  • 读写锁也有公平和非公平模式
  • 读锁支持多个读线程进入临界区,写锁是互斥的

和ReentrantLock相比,ReentrantReadWriteLock多了ReadLockWriteLock两个内部类。

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {       
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;

读锁和写锁的状态表示

abstract static class Sync extends AbstractQueuedSynchronizer {

        static final int SHARED_SHIFT   = 16;// 高16位为读锁,低16位为写锁
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
      
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读写锁对于同步状态的实现是将变量切割成两部分,高16位表示读,低16位表示写。

看个实际例子

class czy{    
     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     ........
     public void read(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操做");
            }
            System.out.println(thread.getName()+"读操做完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

Lock和synchronized的选择

总结来讲,Lock和synchronized有如下几点不一样:

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,所以不会致使死锁现象发生;
而Lock在发生异常时,若是没有主动经过unLock()去释放锁,则极可能形成死锁现象,所以使用Lock时须要在finally块中释放锁;

3)Lock可让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不可以响应中断;

4)经过Lock能够知道有没有成功获取锁,而synchronized却没法办到。

5)Lock能够提升多个线程进行读操做的效率。

总结

有关Lock锁的知识点就到这里,若是想了解更多请参考下面连接。

参考
Java3y多线程
Java技术之AQS详解

相关文章
相关标签/搜索