Reentrantlock

 使用步骤: 
           在成员位置创建一个实现Lock接口对象
           在可能会出现安全问题的代码前调用lock接口中的方法lock获取锁 
           在可能会出现安全问题的代码后调用lock接口中的方法unlock释放锁
  先总体描述下 ReentrantLock 的大致实现,有一个成员属性 sync,所有的方法都是调用该属性的方法。
  Sync 继承 AbstractQueuedSynchronizer(简称 AQS),AQS 封装了锁和线程等待队列的基本实现。
  Sync 有两个子类 NonfairSync 和 FairSync,分别对应非公平锁和公平锁。
  AQS 内部使用volatile int state表示同步状态,在 ReentrantLock 中 state 表示占有线程对锁的持有数量,
  为 0 表示锁未被持有,为 1 表示锁被某个线程持有,> 1 表示锁被某个线程持有多次(即重入)。

      调用lock方法时,会根据是否是公平锁去调用的sync的子类中的方法,一个是公平锁fairSync,一个是非公平 NonfairSync


final void NonfairSync.lock() {
    // 锁未被持有,则获取锁,并将当前线程设置为锁的独占线程
    // 这里可能为其他线程刚刚释放锁,还有其他线程在等待,但这时直接获取,所以是不公平的
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    // 若锁被持有,则调用 AQS.acquire(1) 方法
    else
        acquire(1);
}

protected final boolean AQS.compareAndSetState(int expect, int update) {
    // 利用 sun.misc.Unsafe 的 CAS 原子操作
    // 如果 state 的当前值为 expect,则修改为 update,返回 true
    // 如果 state 的当前值不为 expect,返回 false
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

public final void AQS.acquire(int arg) {
    // NonfairSync.tryAcquire(1) 方法只是调用了 Sync.nonfairTryAcquire(1)
    // 先尝试获取锁
    if (!tryAcquire(arg) &&
        // 获取失败则把线程添加到等待队列中,并阻塞当前线程直到获取成功
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

final boolean Sync.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()) {
        // 锁被当前线程持有,属于重入,state ++
        int nextc = c + acquires;
        // 如果 state > 2 ^ 31 - 1, 则抛出异常,这也是最大重入次数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}


非公平锁的 lock() 的大致逻辑为:如果锁未被持有,不管等待队列中的线程直接获取;如果锁被自己(当前线程)持有,则把 state 加 1;否则将当前线程加入到等待队列中,并阻塞该线程直到获取成功。

公平锁与非公平锁的主要区别在于 FairSync.tryAcquire(1) 这一步:

protected final boolean FairSync.tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 如果锁未被持有,并且当前线程在等待队列的头部或者等待队列为空,则获取锁
        // 保证了没有线程等待时间超过当前线程,所以是公平的
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 锁被当前线程持有,属于重入,state ++
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平锁的 lock() 的大致逻辑为:如果锁未被持有,并且当前线程在等待队列的头部或者等待队列为空,则获取锁;如果锁被自己(当前线程)持有,则把 state 加 1;否则将当前线程加入到等待队列中,并阻塞该线程。


可中断的 lockInterruptibly()

       lockInterruptibly() 方法的文档介绍是获取锁除非线程中断
public final void AQS.acquireInterruptibly(int arg)
        throws InterruptedException {
    // 如果当前线程是中断的,抛出 InterruptedException
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁,即如果锁未被持有或者已被当前线程持有,直接获取
    if (!tryAcquire(arg))
        // 获取锁失败,把线程添加到等待队列,阻塞线程,直到获取成功或者线程中断,线程中断也会抛出 InterruptedException
        doAcquireInterruptibly(arg);
}


可以发现 lockInterruptibly()lock() 的主要区别有两点:(1)如果此时线程是中断的,那么直接抛出 InterruptedException 异常;(2)如果线程被阻塞,在等待过程中线程中断,抛出 InterruptedException 并取消获取,从等待队列中删除。该方法可以用线程中断防止长时间阻塞,也可以以此退出死锁。

非公平的 tryLock()

     不管是公平锁或者非公平锁,tryLock() 方法都是使用非公平策略来尝试获取锁
     tryLock() 方法只是尝试获取锁,获取失败就会返回不会阻塞线程,而使用 synchronized 关键字则会阻塞直到获取锁。

在限定时间内的 tryLock(long timeout, TimeUnit unit)

public final boolean AQS.tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果当前线程是中断的,抛出 InterruptedException
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁,即如果锁未被持有或者已被当前线程持有,直接获取
    return tryAcquire(arg) ||
        // 获取失败,把线程添加到等待队列,阻塞线程,直到限定时间、线程中断或者在此之前获取成功,线程中断也会抛出 InterruptedException
        doAcquireNanos(arg, nanosTimeout);
}


可以看出 AQS.tryAcquireNanos(arg, nanosTimeout) 方法与 AQS.acquireInterruptibly(arg) 类似,都支持线程中断,还加上了一个限定时间。如果限定时间为 0,那么就相当于调用 tryAcquire(1) 方法。上面的 tryLock() 方法在公平锁中还是使用非公平策略,但是 tryLock(0, TimeUnit.SECONDS) 在公平锁中可以实现公平的 tryLock() 方法。

unlock()

  unlock() 释放持有的锁,从获取锁的过程可以猜测到其中肯定会将 state 减 1
public final boolean AQS.release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 释放成功,并等待队列的第一个节点不为空,使用 LockSupport.unpark() 唤醒第一个节点的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean Sync.tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        // 当前线程没有持有锁,抛出异常
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // state 为 0,锁才是自由的,否则只是退出一次重入,锁的被持有线程不变
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    // 返回释放后锁是否自由,即未被持有
    return free;
}

unlock() 方法实际是将 state 减 1,之后如果锁是自由的,则会唤起等待队列的头节点中的线程。不过在两者中间,如果有其他线程获取锁的话,公平锁会判断是否有线程等待,而非公平锁则直接获取该锁。



public boolean isHeldByCurrentThread() {
    return sync.isHeldExclusively();
}

protected final boolean Sync.isHeldExclusively() {
    // 判断锁的被持有线程是否为当前线程
    return getExclusiveOwnerThread() == Thread.currentThread();
}

public boolean isLocked() {
    return sync.isLocked();
}

final boolean Sync.isLocked() {
    return getState() != 0;
}


Condition

       Condition 的作用与 Object 的 wait、notify、notifyAll 类似
       
       两两对应的功能是一样的
       Condition.await() 或 Object.wait() 
       Conditon.signal()、Object.nofity()、
       Condition.signalAll()、Object.notifyAll()

为什么 await、signal 方法需要先获取锁,wait、notify 方法需要先获取对象锁

   这样做的好处是保证 wait 和 notify 的过程是互斥的,而它们又要与某一个东西相关联,所以直接的方法与对象锁相关联

ReentrantLock 的 Condition 的内部实现

public final void AQS.ConditionObject.await() throws InterruptedException {
    // 如果线程中断,直接抛出 InterruptedException
    if (Thread.interrupted())
        throw new InterruptedException();
    // 先把当前线程添加到 condition 的等待队列中
    Node node = addConditionWaiter();
    // 释放线程当前持有的锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 判断线程是否被通知想重新获取锁
    while (!isOnSyncQueue(node)) {
        // 阻塞线程
        LockSupport.park(this);
        // 阻塞线程被唤醒后,如果此时线程中断,则跳出循环
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 所以阻塞线程被 signal 唤醒后,或者线程中断后可以跳出循环

    // 重新获取锁,获取失败则阻塞加入阻塞队列直到获取成功
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        // 如果获取的过程中线程中断,设置 interruptMode 为 REINTERRUPT
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        // 清楚等待队列中的取消的节点
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        // 如果 interruptMode 为 REINTERRUPT, 再次中断线程
        // 如果 interruptMode 为 THROW_IE,抛出 InterruptedException
        reportInterruptAfterWait(interruptMode);
}
释放锁,并且阻塞自己并添加到 condition 的等待队列,被 signal 通知或线程中断后唤醒线程,重新获取锁。

signal() 方法的实现:

public final void AQS.ConditionObject.signal() {
    // 锁不是互斥独占锁时,抛出 IllegalMonitorStateException 异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        // 如果等待队列不为空,
        doSignal(first);
}

private void AQS.ConditionObject.doSignal(Node first) {
    do {
        // 把 first 节点从队列中移除
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 循环找到第一个未取消的节点,把该节点从 condition 队列添加到 sync 等待队列(lock 队列)
    } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
                
 signal() 并没有唤起 wait 的线程,只是把等待时间最长的未取消线程添加到 sync 等待队列,等待获取锁。
而 signalAll() 方法的区别时将 condition 等待队列中所有节点移到 sync 等待队列
}

总结

ReentrantLock 是 API 的重入锁,相对 synchronized 关键字来说,额外支持公平锁(synchronized 是非公平的),
获取锁可中断、可以限定获取的最大时间、可以关联多个 Condition。内部主要实现细节是基于 AQS 的,等待队列是用链表结构存储的,
阻塞队列使用 LockSupport.park() 实现。

案例

 使用不同队列来控制生产和消费的锁唤醒问题
public class demo {

    ReentrantLock re = new ReentrantLock();
    Condition con1 = re.newCondition();  //消费队列
    Condition con2 = re.newCondition();//生产队列
    final LinkedList<String> li = new LinkedList<>();
    final int w=6;
    final int MAX = 10;
    int count = 0;

    public String get() {
        String i="";
        try {
            re.lock();
            while (li.size() == 0) {
                con1.await();
            }
            i = li.removeFirst();
            count--;
            con2.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
           re.unlock();
        }
        return i;

    }
    public  void put(String t) {
        try {
            re.lock();
            while (li.size()==MAX){
                con2.await();
            }
            li.add(t);
            ++count;
            con1.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            re.unlock();
        }
    }

    public static void main(String[] args) {
        mianshi3 m = new mianshi3();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 25; j++) m.put( Thread.currentThread().getName()+j);
            }, "sc").start();
        }


        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) System.out.println(Thread.currentThread().getName() +":"+ m.get());
            }, "xf"+i).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.li.size()+"::;"+m.count);

    }
}

这种方式虽然更安全,允许线程再任意时刻,只允许一个线程执行
但是有些时候,这种保护有点过头
如果有读写操作的话,那么读和写都是只允许一个线程的,如果是写还可以,但是读的话,应该可以同时读取

这就要说到这个类了
ReadWriteLock

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值