java基础之ReentrantLock锁

Lock锁的公平性和非公平性

1、lock锁项目使用

在项目中的使用方式:

public class AQSTestOne {
    // 使用公平锁来进行测试
    private static final Lock LOCK = new ReentrantLock(true);

    public static void main(String[] args) {
        LOCK.lock();
        try {
            System.out.println("so something  ");
        }catch (Exception e){
            System.out.println("do Exception something............");
        }finally {
            LOCK.unlock();
        }
    }
}

因为对于对象来说,对于成员变量LOCK锁来说,会在堆内存中,任何一个线程进来的时候执行了对应的方法,都会执行到lock锁上来进行排队。

每个线程都会来使用lock锁,那么lock.lock()方法是如何保证多线程环境下,在JVM中在某一个时刻,只有一个线程占用锁呢?

2、AQS继承体系

对于ReentrantLock类中,存在AbstractQueuedSynchronizer类以及对应的子类Sync和Sync的两个子类:FairSync和NonfairSync

对应的是公平同步锁和非公平同步锁。

3、构造函数

首先从构造函数来讲起,因为非公平锁的效率高,所以推荐使用的是非公平锁。

从构造函数中可以看到对应的结构:

   private final Sync sync;

   public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
	
	// 默认采用非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }

	
	// 继承体系图
    abstract static class Sync extends AbstractQueuedSynchronizer {

        abstract void lock();
        
    }        

4、加锁流程

lock锁是如何保证多线程能够保证线程安全呢?

那么看一下lock锁又是如何来进行加锁的。

首先来看这行代码到底做了什么?

lock.lock();

进源码查看:

    public void lock() {
        sync.lock();
    }

那么这里看公平锁的实现方式:

        final void lock() {
            acquire(1);
        }

重点就来到了acquire方法,看看对应的代码实现:

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

通过代码可以看到首先会来尝试获取。

可以根据代码来画一幅流程图来描述是如何来获取得到锁的:

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 判断当前锁是否被其他线程持有!利用一个int类型的变量来进行修饰
            int c = getState();
            if (c == 0) {
                // 这里判断是无锁状态之后,并不是直接获取得到锁,还需要来进行判断CLH队列中是否有线程在排队
                // 因为这里是公平锁,公平锁就需要保证如果CLH队列中有在排队的线程,那么让他们先获取得到
                if ( !hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    // 设置为独占
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果是当前的线程,那么表示的是可重入锁。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

看一下是如何查看队列中是否是有线程在排队的:

    public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
    }

在这里来组成一个双向链表,又称之为CLH队列。head指向队头,tail指向队尾。

如果h!=t,表示的是CLH队列中是有线程排队的,后面的两个判断只要有一个判断是成功的,那么就说明队列中是有值的。

两个判断:1、如果头结点的下一个节点为空;2、头结点的下一个节点不为空并且不为当前线程;

如果返回true的话,那么表示CLH队列中是有线程在排队的;

如果是false的话,那么表示的是CLH队列中是没有线程在排队的。

这里就是直接判断是否存在首节点,head的节点的下一个节点(首节点)是有是有值的,如果有值,那么说明CLH队列中是有值的。

如果队列中没有线程在等待锁,可以看到利用CAS来获取得到锁,然后设置锁被哪个线程获取得到;

如果已经有线程持有了锁,那么判断是否是当前的线程,如果是,那么进行再次加锁;

如果队列中有线程在排队等待锁并且不是当前线程,那么直接获取得到锁失败;

那么对应的流程如下所示:

对应的流程如下所示:

  • 1、每个线程在获取锁的时候,判断锁的状态,如果是无锁状态,那么进入到队列中查看队列中是否有其它线程,如果没有,那么去获取得到锁;如果有的话,那么获取锁事变,排队等锁;
  • 2、如果当前线程检查是有锁状态,那么判断持有锁的是否是当前线程,如果是,那么锁重入次数+1;
  • 3、如果不是当前线程持有锁,那么获取得到锁失败;

4.1、加锁流程的两种情况

总结起来,获取得到锁的线程只有两种情况

1、当前锁状态是无锁,且队列中没有线程在排队,那么获取得到锁;

2、当前锁状态是有锁,且持有锁的线程是自己,那么这个时候锁是可重入的;

5、线程没有抢到锁之后需要排队

没有抢到锁的线程需要进行排队,继续看下源码:

    public final void acquire(int arg) {
        // 没有抢到锁,返回false,取反为true,那么执行后面的逻辑
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先会创建线程等待者,当前是独占模式

    private Node addWaiter(Node mode) {
        // 创建节点保存当前的线程
        Node node = new Node(Thread.currentThread(), mode);
        // 获取得到尾结点
        Node pred = tail;
        if (pred != null) {
            // 如果尾结点不为空
            // 1、首先将尾节点的前驱指针指针尾指针指向的节点
            node.prev = pred;
            // 2、比较并交换,并将尾结点指针后移一位
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 这里有两种情况存在。因为上面如果存在多线程竞争,称为不了尾结点的线程将会走到这里来。看一下入队操作
        // 1、tail为null;2、比较并交换称为尾结点的节点
        enq(node);
        return node;
    }

注意看下上面的if判断,在入队尾的时进行比较并交换时,是失败的,那么这个时候将会再次执行enq方法。

看一下enq方法:

    private Node enq(final Node node) {
        // 死循环
        for (;;) {
            Node t = tail;
            // 如果tail为null,那么这种也是上面的一种的情况。这里需要来进行初始化
            // 从这里也可以看到初始化的是一个空节点,不保存任何线程
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 上面的另外一种情况。和上面的addWaiter中判断是一致的
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里一直在使用死循环,这里存在着两个作用:

  • 1、初始化,让head和tail节点指向一个空的节点;

  • 2、将排队的节点称为尾结点(队列特点:先进先出FIFO)

这里的for循环是为了构建CLH阻塞队列。这里是第一个死循环队列来进行构建的。

总之是为了将所有没有抢到锁的线程来进行入队,这里的图形画出来:

这里就对应着上面的for循环操作。上面在死循环中构建一个CLH队列,但是此时还没有做任何操作,比如说节点中的值还没有来得及对其进行设置。

6、CLH队列中线程先抢锁后阻塞

    final boolean acquireQueued(final Node node, int arg) {
        // 获取锁失败为true
        boolean failed = true;
        try {
            // 线程中断状态
            boolean interrupted = false;
            for (;;) {
                // 获取得到前驱节点。已经之前已经排好队
                final Node p = node.predecessor();
                // 如果当前节点的头结点是头结点!那么再次来尝试获取得到一次锁看看能不能成功
                // 因为可能在执行到这一步的时候,线程已经将锁释放了,所以这里再次来尝试一下
                if (p == head && tryAcquire(arg)) {
                    // 将当前节点设置为头结点
                    setHead(node);
                    // 当前的头结点没有作用了,需要GC掉
                    p.next = null; // help GC
                    // 因为抢占锁成功,所以这里标注为fasle;
                    failed = false;
                    // 不是因为中断引起的线程抢占锁中断
                    return interrupted;
                }
                // 在失败获取得到锁的时候,应该将线程进行阻塞!这是才是重点!
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

6.1、for循环

这里的for循环是为了修改每个线程对应的节点的执行状态的。只有节点状态是SINGAL的时候才会在唤醒的时候有机会获取得到线程。

而刚刚入队的节点是并不是SINGAL状态的,所以这里是在循环设置。那么看一下在失败获取得到锁之后是如何将线程进行阻塞的:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 首先获取得到每个线程排队节点的前驱节点中的等待状态!
        int ws = pred.waitStatus;
        // 如果是SINGAL,那么标识,就等着被唤醒来获取得到锁
        if (ws == Node.SIGNAL)
            return true;
        // 这里标识的是线程抢锁取消,不再去抢锁了
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
            // 如果是其他的,那么比较并交换,将当前的接地那的waitstatus进行设置
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

因为只有当这一步为true的时候,才会将线程进行阻塞。那么为true的就只有一步

if (ws == Node.SIGNAL)
    return true;

只有所有的节点中的status为Node.SIGNAL的时候,才会为true。

那么为fales的时候,将会再次走到下面的死循环中来:

            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    // 只有当线程阻塞过程中,是因为线程中断而导致排队中的线程被唤醒,那么这里标记
                    // 将会被设置为true;
                    interrupted = true;
            }

知道为true的时候,才会走到parkAndCheckInterrupt方法中来,而这一步是真正的做到将线程进行终止的操作的方法:

    private final boolean parkAndCheckInterrupt() {
        // 将当前线程阻塞到对象上来
        LockSupport.park(this);
        // 当前线程是否是以中断引起的?如果是,那么返回true;如果不是,那么返回false;
        return Thread.interrupted();
    }

被park住的线程,此时被阻塞了,要是想苏醒过来,必须要等到前一个线程来将其进行唤醒。

注意park()和park(this)的使用区别:

将队列中的前驱节点中的waitstatus进行修改,表示的是可以将后来的线程来进行唤醒。

7、锁释放

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

直接看尝试释放锁的代码:

        protected final boolean tryRelease(int releases) {
            // 这里体现出现来可重入锁的特性
            int c = getState() - releases;
            
            // 如果不是当前线程来释放锁,那么将会报错!
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            // 将释放标记来进行标记
            boolean free = false;
            // 如果c==0,那么表示的是锁能够释放。如果是可重入锁,那么这里不为0的时候,将会继续来进行设置
            // 所以这里也就要求!加了多少次锁,就要释放多少次锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            // 只有c==0的时候,这里才为true
            return free;
        }

那么再次回到上一步,成功释放锁之后操作

    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }

获取得到头结点,然后判断头节点不为空以及等待状态不为0(必须是SIGNAL状态)的时候,唤醒头结点的下一个节点:

    private void unparkSuccessor(Node node) {
        // 获取得到头结点的状态信息
        int ws = node.waitStatus;
        // 如果<0,那么比较并交换,将当前节点的状态的status修改成0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 获取得到下一个节点
        Node s = node.next;
        // 如果为空或者是等待状态>0(明显为0)
        if (s == null || s.waitStatus > 0) {
            // 失去引用,那么会GC掉
            s = null; 
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 唤醒线程
            LockSupport.unpark(s.thread);
    }

具体的执行图如下所示:

当然,这里并没有将将前驱节点断掉,而是在唤醒线程后做的操作:

那么又再次回到原始起点:

            for (;;) {
                // 唤醒之后,又会来到这里
                final Node p = node.predecessor();
                // 尝试获取得到锁。对于公平锁来说,一定会执行到这一步,公平锁一定会获取得到
                if (p == head && tryAcquire(arg)) {
                    // 当前节点设置为头结点,并消除掉前置指针
                    setHead(node);
                    // 后置指向置为空
                    p.next = null; // help GC
                    failed = false;
                    // 跳出循环
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }

看一下这里的setHead(node)方法:

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

将当前的节点设置为head节点,然后将节点中的线程置为空,然后断掉前置指针。

最终的结果就是图如下所示:

8、线程等待状态补充说明

waitStatus 非常重要,也是关键,有下面几个枚举值:

枚举 含义
CANCELLED 为1,表示线程获取锁的请求已经取消了
SIGNAL 为-1,表示线程已经准备好了,就等资源释放了
CONDITION 为-2,表示节点在等待队列中,节点线程等待唤醒
PROPAGATE 为-3,当前线程处在SHARED情况下,该字段才会使用
0 当一个Node被初始化的时候的默认值

至此公平锁的流程分析结束:

lock.lock();
xxxx;
lock.unlock();

这段代码的执行逻辑分析结束。

9、总结公平锁的获取流程

这里对应的是我自己画的一个流程图:

两个for循环的作用:

  • 1、第一个for循环是排队进入阻塞队列队尾;
  • 2、第二个for循环是修改每个节点的状态;

队头唤醒之后进入循环

可以看到锁在释放的时候会唤醒下一个节点,唤醒下一个节点的时候,是线程自己进入到for循环中来再次尝试获取得到锁。

对于公平锁而言,队头元素肯定是可以获取得到锁的。因为有个判断,判断前驱节点是队头的才可以。

10、非公平锁的加锁流程

直接看对应的代码:

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

之所以是非公平的,上来就进行比较并交换,如果成功,就比较并设置为当前的线程,野蛮至极。

就这一步的区别,其他的也没有任何的变化了,所以总结起来只需要来画个图即可。

其实就是在线程刚刚进来的时候直接抢锁,非常类似syncronized的流程。因为syncronzied锁是非公平性的,也会尝试抢占。

11、Lock锁的特性讲解

11.1、Lock锁特性

  • 阻塞等待队列
  • 共享锁和独占锁(排他锁)
  • 公平和非公平性
  • 可重入
  • 可中断

11.2、Lock锁是用变量state标识

用一个state标识来表示当前的锁是否被占有,需要注意的是state变量是用volatile关键字来进行修饰的。

能够及时让其他线程线程看到锁是否被抢占。

11.3、两种队列

  • 阻塞队列
  • 条件队列

阻塞对象是将没有获取得到锁的线程放到CLH队列中来进行阻塞;

条件队列是在满足条件的地方,调用condition.await方法的时候,将当前线程占有的锁释放掉,然后放入到条件队列中来;当调用condition.sign()或者是condition.sinalAll()方法的时候,将被放在条件队列中的线程追加到阻塞队列上来,让条件队列中的线程有机会获取得到锁。

:条件队列的使用及其类似多线程中原始的wait()/notify()/notifyAll()方法的使用

示例:

/**
 * @Description  等待唤醒机制  除了wait() 和notify\notifyAll方法而唤醒的
 *
 *              使用lock锁的condition十分类似于notify和notifyAll的机制,只是通知,但是并没有真正的将锁给释放掉
 *              而await方法,是将当前线程的执行权让出去;让当前的线程陷入到阻塞中去,等待其他线程的唤醒!
 *
 *              await在当前持有锁的阶段中释放锁,然后将自己放入到阻塞线程中去;
 *              sinal在持有锁阶段,将因为放到条件队列中的线程唤醒,将条件队列中的线程追加到阻塞队列上去;
 * @Author liguang
 * @Date 2022/03/19/10:27
 */
public class LockTestOne {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        // 条件队列
        Condition condition = lock.newCondition();

        new Thread(()->{
            lock.lock();
            try {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName+"---------开始处理任务");
                condition.await();
                System.out.println(threadName+"---------处理任务结束");
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName+"---------开始处理任务");
                condition.signal();
                System.out.println(threadName+"---------处理任务结束");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();

    }
}

11.4、节点五种状态

  • 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
  • CANCELLED,值为1,表示当前的线程被取消;
  • SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
  • CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  • PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享

资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出

队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现

它。

tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但

没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待

结点返回true,否则返回false。

12、测试Lock锁特性

12.1、可重入锁

/**
 * 测试可重入性
 */
public class LockTestThree {
    public static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                method1();
            }).start();
        }
    }

    public static void method1(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"method1");
            method2();
        }finally {
            lock.unlock();
        }
    }

    private static void method2() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"method2");
            method3();
        }finally {
            lock.unlock();
        }
    }
    private static void method3() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"method3");
        }finally {
            lock.unlock();
        }
    }
}

每次去获取得到锁的时候,都会发现占用的锁的是当前的线程,所以会在state基础之上+1。

问题:加了多少次锁,就要释放多少次锁。不能多一次,否则将会导致一把锁一直被一个线程一直占用。

示例:

public class ThreadLockCount {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                lock.lock();
                System.out.println("hello,world");
            }).start();
        }
    }
}

这里正是因为一个线程一直持有锁(state),一直不为0,那么所有的线程都将会阻塞在队列中,无法继续向下继续运行。

12.2、可中断性

线程被唤醒有两种情况

  • 锁释放,唤醒后续节点
  • 线程中断

可以在源码中的if判断中,因为中断而唤醒的,会有对应的中断标记表示的是因为中断而引起的线程中断。

而给线程打上标记之后,在外部的某个地方,可以通过判断线程状态,来获取得到对应的。对应的源码体现:

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果是因为中断引起的,那么会清除中断标记并返回true
                    parkAndCheckInterrupt())
                    // 然后会给这个标识修改为true,表示是以为线程中断引起的,外部可以做另外操作。
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

然后将标记暴露给外部:

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

对应的状态:

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

在外部程序可以检测到这个异常信息。

来写个代码表示一下:

/**
 *
 * 100个线程来进行自增操作,但是其中一个线程在排队时,发送中断标记,告知该线程不应该继续操作
 * 终止其当前线程正在运行的动作
 * @author liguang
 * @date 2022/7/28 10:00
 */
public class Test2 {

    private static final ReentrantLock lock = new ReentrantLock(false);

    private static int i = 0;

    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                reentrantLock();
            });
            threadList.add(thread);
            thread.start();
        }
        try {
            // 发一个中断信号,将其进行唤醒
            threadList.get(90).interrupt();
            Thread.sleep(2000);
            System.out.println("最终确定的值是:"+i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void reentrantLock(){
        try {
            lock.lock();
            // 如果线程中断了,就不应该再来进行后续的任务了
            while (Thread.currentThread().isInterrupted()){
                // 擦除掉中断标记,然后返回,后面的事情不做了
                boolean interrupted = Thread.interrupted();
                System.out.println("线程标记装填清除了"+interrupted);
                System.out.println(Thread.currentThread().isInterrupted());
            }
            i++;
        } catch (Exception e) {
            System.out.println(e);
            System.out.println("线程中断了"+Thread.currentThread().getName());
            // 说明有一个线程是因为线程中断唤醒的!所以需要将其进行替换掉
            System.out.println("当前线程状态是:"+Thread.currentThread().isInterrupted());
        }finally {
            lock.unlock();
        }
    }

}

多运行几次,会出现以下效果。因为线程运行过程中,如果中断了,碰上了Thread.sleep的话,会导致异常出现。

因为没有将线程睡眠一会儿,所以线程中断可能在线程运行完成之后,也可能是在线程运行之前。

线程标记装填清除了true
false
最终确定的值是:100

也有另外一个方法

lock.lockInterruptibly();

看看其实现原理:

    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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果是因为线程中断引起,那么直接抛出异常
                    throw new InterruptedException();
            }
        } finally {
            // 抛出异常之前,这里执行取消对应的排队节点
            if (failed)
                cancelAcquire(node);
        }
    }

具体实例如下所示:

/**
 *
 * 100个线程来进行自增操作
 * @author liguang
 * @date 2022/7/28 10:00
 */
public class Test1 {

    private static final ReentrantLock lock = new ReentrantLock(false);

    private static int i = 0;

    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                reentrantLock();
            });
            threadList.add(thread);
            thread.start();
        }
        try {
            threadList.get(90).interrupt();
            Thread.sleep(1000);
            System.out.println("最终确定的值是:"+i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void reentrantLock(){
        try {
            lock.lockInterruptibly();
            Thread.sleep(10);
            i++;
        } catch (Exception e) {
            System.out.println("线程中断了,抛出异常了");
        }finally {
            lock.unlock();
        }
    }

}

12.3.1、立即失败

/**
 * 尝试获取得到锁,这里是立即失败,不管是公平锁还是非公平锁,都是理解返回的状态
 */
public class LockTestFive {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("线程t1启动");
            if (!lock.tryLock()){
                System.out.println("线程t1没有获取得到锁,返回");
                return;
            }
            try {
                System.out.println("获取得到了锁!");
            }finally {
                // 将锁释放
                lock.unlock();
            }
        }, "lig");

        // main线程
        lock.lock();
        try {
            System.out.println("main线程获取得到了锁");
            t1.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }
}

12.3.2、超时失败

/**
 * 在指定的时间内没有获取得到锁之后,失败
 */
public class LockTestSix {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("线程t1启动");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)){
                    System.out.println("线程t1没有获取得到锁,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 获取得到锁之后,下面的就不需要再来进行执行了
                return;
            }
            try {
                System.out.println("获取得到了锁!");
            }finally {
                // 将锁释放
                lock.unlock();
            }
        }, "lig");

        // main线程
        lock.lock();
        try {
            System.out.println("main线程获取得到了锁");
            t1.start();
            try {
                // 休眠两秒钟来进行测试
                Thread.sleep(2000);
                System.out.println("main线程释放了锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }
}

12.4、公平锁和非公平锁

/**
 * 尝试获取得到锁,这里是立即失败,不管是公平锁还是非公平锁,都是理解返回的状态
 */
public class LockTestSeven {

    public static void main(String[] args) {
        // 尝试公平锁和非公平锁
        // ReentrantLock lock = new ReentrantLock(true);
        ReentrantLock lock = new ReentrantLock();
        for (int i = 0; i < 5000; i++) {
            new Thread(()->{
                lock.lock();
                try {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("当前线程"+Thread.currentThread().getName()+" is running.........");
                }finally {
                    lock.unlock();
                }
            },"t"+i).start();
        }
        // 休眠之后再次去抢锁
        try {
             Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 5000; i++) {
            new Thread(()->{
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+" is running.........");
                }finally {
                    lock.unlock();
                }
            },"强行抢锁"+i).start();
        }
    }
}

让控制台交替打印,可以看到非公平锁是可以来交替打印对应的线程Name的。

12.5、条件变量

调用 Condition.await() 方法使线程等待,其他线程调用Condition.signal() 或 Condition.signalAll() 方法唤醒等待的线程。

注意:调用Condition的await()和signal()方法,都必须在lock保护之内

/**
 * 条件变量:模拟生产者和生产者!这是这里明显,可以有多个条件,可以利用多个条件来进行操作
 */
public class LockTestEight {

    private static Lock lock = new ReentrantLock();
    private static Condition cigCon = lock.newCondition();
    private static Condition takeCon = lock.newCondition();
    private static boolean hasCig;
    private static boolean hasTakeOut;

    public void cigratee(){
        lock.lock();
        try {
            while (!hasCig){
                try {
                    System.out.println("没有烟了,歇一会儿");
                    cigCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("有烟了,开始干活...........");
        }finally {
            lock.unlock();
        }
    }

    /**
     * 送外卖
     */
    public void takeOut(){
        lock.lock();
        try {
            while (!hasTakeOut){
                try {
                    System.out.println("饭还没有好,歇一会儿");
                    takeCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("有饭了,吃完饭开始干活...........");
        }finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        LockTestEight lockTestEight = new LockTestEight();
        new Thread(()->{
            lockTestEight.cigratee();
        }).start();

        new Thread(()->{
            lockTestEight.takeOut();
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                hasCig = true;
                cigCon.signal();
            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                hasTakeOut = true;
                takeCon.signal();
            }finally {
                lock.unlock();
            }
        }).start();

    }
}

这个具体分析会在后面给列出来。

posted @ 2022-03-13 15:28  雩娄的木子  阅读(1460)  评论(0编辑  收藏  举报