深入Java Lock锁

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

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

在学习Lock锁之前,我们先来看看什么是AQS?

AQS

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

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接口

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接口的类。接下来我们来看看有关源码:

AQS子类

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

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详解

你可能感兴趣的:(java,多线程,lock)