前言

前面我们对并发容器和线程协作工具进行了相关源码分析,今天我们将从使用出发,并继续深入源码,看看ReentraientLock是如何对锁的使用进行封装和优化的

下面,正文开始

使用ReentraientLock实现顺序打印

在篇一:为什么CountDownlatch能保证执行顺序?中,我们使用CountdownLatch实现了顺序打印的需求,并且分析了其原理其实是线程间的通知和唤醒

ReentraientLock作为锁的封装和实现,同样支持线程间的通知和唤醒,针对到具体的方法为singal()(通知其他线程获取锁),await()释放锁并等待

具体实现中会创建一个ReentrantLock对象,并创建了4个条件变量Condition

  1. 当condition不满足时,调用condition.await()释放锁并等待
  2. 当线程执行完成后,调用condition.single()唤醒对应的线程继续执行

具体代码如下

/**
 * 使用Condition条件变量控制打印流程
*/
public void conditionPrint() {
        ReentrantLock lock = new ReentrantLock();

        Condition aCondition = lock.newCondition();
        Condition bCondition = lock.newCondition();
        Condition cCondition = lock.newCondition();
        Condition dCondition = lock.newCondition();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    printStr("a");
                    aCondition.signal();
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    aCondition.await();
                    printStr("b");
                    bCondition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    bCondition.await();
                    printStr("c");
                    cCondition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    cCondition.await();
                    printStr("d");
                    dCondition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        t3.start(); t4.start(); t2.start();t1.start();
        printStr("conditionPrint-打印开始");
        try {
            lock.lock();
            dCondition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            printStr("conditionPrint-打印结束");
        }
    }

ReeentraientLock详解

到底什么是ReentraientLock

故名思意,ReentraientLock应该是一个可重入锁

可重入锁的意思是,当一个线程持有一个锁对象,在对其进行解锁后再执行加锁操作,此时可以直接获取到锁对象继续执行

关于多个锁对一把锁的争抢问题,在AbstractQueuedSynchronizer(后面简称AQS)中会维护一个线程队列,当通知线程去获取锁时,会从线程队列中取出申请锁的线程,让该线程继续执行

ReentraientLock中,会继承AQS实现自己的Sync,其中的非公平锁实现NonfairSync及公平锁实现FairSync都是Sync的子类对象

ReentraientLock的默认构造会使用NonfairSync(非公平锁,AQS的实现类)进行获取和释放的逻辑,同时可以在构造中传入fair的参数,控制是否使用公平锁,如果传入true,则会使用FairSync进行锁的释放

什么是公平锁和非公平锁?

在聊具体的代码前,我们先来讲一下什么叫公平锁

上文也提到了,在AQS中会维护一个Thread的双向循环链表,用来第获取锁的线程进行保存

如果所有线程对于加锁和解锁操作都完全按照申请顺序进行存取,那就是公平锁的实现思路

如果在某种条件下,允许某些线程获取锁的操作进行插队,那就是非公平锁的实现思路

在具体的使用中,我们在进行加锁时,会调用lock.lock()在进行解锁时会调用lock.unlock(),其内部会调Synclock()函数及release()函数

在进行线程等待时会调用await(),唤醒等待线程会调用signal()

下面,我们就针对具体的实现来分析下

非公平锁NonfairSync

默认构造中,会调用非公平锁的实现,执行lock()方法, 其中会通过CAS操作对AQS中的state值进行修改

ReentraientLock中,state表示对锁的重入次数,state为0表示没有线程持有锁,state非0表示有线程正在持有锁

具体代码如下

static final class NonfairSync extends Sync { //非公平锁
        //...
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

如果修改成功(state为0),表示没有线程获取当前的锁,会将当前线程的Thread对象保存在AQSexclusiveOwnerThread中,表示该线程获取到锁,可以执行相关逻辑

如果修改失败(state非0),表示线程锁已被持有,会调用到AQSacquire()函数,将当前线程添加到请求队列的链表中

其中的acquire(1)会调用到AQS中的函数,并调用到了子类(即NonfairSync)的tryAcquire(1)函数申请锁的执行权,最终调用到了nonfairTryAcquire()

代码如下

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); //state表示线程重入的次数
            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); //更新state值
                return true;
            }
            return false;
        }

该函数是可重入锁逻辑的核心实现:

  1. 如果state==0,表示当前锁没有被持有,将其设置为执行线程并返回true
  2. 如果state!=0,且申请锁的线程与当前持有锁的线程一直,则通过state维护锁的重入次数累加,并返回true
  3. 如果state!=0,且申请锁的线程不是当前线程,则申请失败,返回false

如果返回true,表示当前线程申请到锁,继续执行doAcquireInterruptibly(),接着将Thread封装为Node节点添加到链表中,设置为头节点

代码如下

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE); 
        
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node); 
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                //...
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

至此,我们将非公平锁的实现分析完毕了,现在来简单总结一下

小结

非公平锁的实现中,当前持有锁的线程在锁释放后再获取锁,可以直接获取到锁继续执行,这就是所谓的非公平实现,也可以叫可重入锁(针对当前持有锁的线程是可重入的)

Java中的synchronizelock的实现都是可重入的,非公平锁(或者说可重入锁)的好处在于,

  1. 节省了线程唤醒的开销,性能上更优
  2. 当前持有锁的线程在释放后再申请锁时,可以直接获取到锁对象,避免了死锁的发生

公平锁FairSync

针对FairSync而言,实现了绝对的公平,不论当前线程是否持有锁,再次获取锁对象时都需要添加到线程的等待队列,按插入顺序去等待被唤醒

其优点在于,保证了绝对的公平,可以按照线程的启动顺序(先进先出)去顺序执行

缺点在于,不可重入性导致了性能上不如非公平锁

具体代码此处不再展示,感兴趣的小伙伴可以自行查看

加餐:不使用锁如何保证线程安全?

在源码分析的时候,我们发现ReentraientLock中对线程互斥的维护并没有使用加锁的逻辑,而是使用CAS(compareAndSwap)对state值进行操作去实现的

在java并发包中,原子类数据结构也是通过CAS指令去实现的原子性操作,保证无锁条件下的并发安全

AtomicInteger来举例,其中也是类似与ReentraientLock中对state变量的CAS修改操作去实现的

因此我们可以也可以使用原子类来实现本文中的顺序打印需求

  1. 创建一个AtomicInteger,并初始化其value为4
  2. 在各线程进行循环判断,当value满足条件时执行打印,并将value值--
  3. value倒数到0后,说明4各线程都已经执行完成,在主线程中循环等待条件满足时终止子线程

代码如下

public void atomicPrint() {
        AtomicInteger lock = new AtomicInteger(4);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    if (lock.get() == 4) {
                        System.out.println("Thread:" + "a" + " lock count:" + lock.get());
                        System.out.println("a");
                        lock.decrementAndGet();
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    if (lock.get() == 3) {
                        System.out.println("Thread:" + "b" + " lock count:" + lock.get());
                        System.out.println("b");
                        lock.decrementAndGet();
                    }
                }
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    if (lock.get() == 2) {
                        System.out.println("Thread:" + "c" + " lock count:" + lock.get());
                        System.out.println("c");
                        lock.decrementAndGet();
                    }
                }
            }
        });
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    if (lock.get() == 1) {
                        System.out.println("Thread:" + "d" + " lock count:" + lock.get());
                        System.out.println("d");
                        lock.decrementAndGet();
                    }
                }
            }
        });
        System.out.println("atomicPrint-执行开始");
        t3.start(); t4.start();t2.start();t1.start();
        while (!Thread.currentThread().isInterrupted()) {
            if (lock.get() == 0) {
                t3.interrupt();
                t4.interrupt();
                t2.interrupt();
                t1.interrupt();
                System.out.println("atomicPrint-执行结束");
                lock.decrementAndGet();
                return;
            }
        }
        Thread.currentThread().interrupt();
    }

小结

我们在使用原子类维护线程按顺序执行时,需要在各线程中,通过原子类的方法循环判断其是否满足条件,相当与自旋操作,因此在开销上比循环等待要高,一般情况下我们还是应该使用线程协作的方式去实现

当需要对某种状态进行原子性操作时,可以使用原子类的相关实现,比加锁的方式效率更高