请你谈谈多线程基本知识 - ReentrantLock的特性的理解?

1 ReentrantLock可重入

同一个线程,如果首次获取到该锁资源,则它就有权力再次获取到该锁,这就是锁的重入。

package com.zs.thread;

import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptibly {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        fun_1();
    }
    public static void fun_1() {
        lock.lock();
        try {
            System.out.println("fun_1 lock");
            fun_2();
        } finally {
            lock.unlock();
        }
    }

    private static void fun_2() {
        lock.lock();
        try {
            System.out.println("fun_2 lock");
            fun_3();
        } finally {
            lock.unlock();
        }
    }

    private static void fun_3() {
        lock.lock();
        try {
            System.out.println("fun_3 lock");
        } finally {
            lock.unlock();
        }
    }
}

在这里插入图片描述

2 ReentrantLock可中断

所谓的中断锁指的是锁在执行时可被中断,也就是在执行时可以接收 interrupt 的通知,从而中断锁执行。 ​默认情况下 Lock 也是不可中断锁,但是可以通过特殊的“手段”,可以让其变为可中断锁。
不可中断锁的问题是:当出现“异常”时,只能一直阻塞等待,别无其他办法,比如下面这个程序:
下面的这个程序中有两个线程,其中线程 1 先获取到锁资源执行相应代码,而线程 2 在 0.5s 之后开始尝试获取锁资源,但线程 1 执行时忘记释放锁了,这就造成线程 2 一直阻塞等待的情况,实现代码如下:

package com.zs.thread;

import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        // 创建线程 1
        Thread t1 = new Thread(() -> {
            lock.lock();
            System.out.println("线程 1:获取到锁.");
            // 线程 1 未释放锁
        });
        t1.start();

        // 创建线程 2
        Thread t2 = new Thread(() -> {
            // 先休眠 0.5s,让线程 1 先执行
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取锁
            System.out.println("线程 2:等待获取锁.");
            lock.lock();
            try {
                System.out.println("线程 2:获取锁成功.");
            } finally {
                lock.unlock();
            }
        });
        t2.start();
    }
}

在这里插入图片描述
线程 2 还在阻塞等待获取线程 1 释放锁资源,此时的线程 2 除了等之外,并无其他方法。 ​

然而,中断锁的出现,就可以打破这一僵局,它可以在等待一定时间之后,主动的中断线程 2,以解决线程阻塞等待的问题。 ​中断锁的核心实现代码是 lock.lockInterruptibly() 方法,它和 lock.lock() 方法作用类似,只不过使用 lockInterruptibly 方法可以优先接收中断的请求,中断锁的具体实现如下:

package com.zs.thread;

import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        // 创建线程 1
        Thread t1 = new Thread(() -> {
            // 加锁操作
            lock.lock();
            System.out.println("线程 1:获取到锁.");
            // 线程 1 未释放锁
        });
        t1.start();

        // 创建线程 2
        Thread t2 = new Thread(() -> {
            // 先休眠 0.5s,让线程 1 先执行
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取锁
            try {
                System.out.println("线程 2:尝试获取锁.");
                lock.lockInterruptibly(); // 可中断锁
                System.out.println("线程 2:获取锁成功.");
            } catch (InterruptedException e) {
                System.out.println("线程 2:执行已被中断.");
            }
        });
        t2.start();

        // 等待 2s 后,终止线程 2
        Thread.sleep(2000);

        if (t2.isAlive()) { // 线程 2 还在执行
            System.out.println("执行线程的中断.");
            t2.interrupt();
        } else {
            System.out.println("线程 2:执行完成.");
        }
    }
}

在这里插入图片描述
从上述结果可以看出,当我们使用了 lockInterruptibly 方法就可以在一段时间之后,判断它是否还在阻塞等待,如果结果为真,就可以直接将他中断,如上图效果所示。 ​但当我们尝试将 lockInterruptibly 方法换成 lock 方法之后(其他代码都不变),执行的结果就完全不一样了。
在这里插入图片描述
通过显示锁 Lock 的 lockInterruptibly 方法来完成,它和 lock 方法作用类似,但 lockInterruptibly 可以优先接收到中断的通知,而 lock 方法只能“死等”锁资源的释放。 ​

3 ReentrantLock超时时间

如果没有指定时间,则立即失败(没有获取到锁):

package com.zs.thread;

import java.util.concurrent.locks.ReentrantLock;
public class Demo {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1尝试获取锁");
            if(!lock.tryLock()){
                System.out.println("没有获取到锁");
                return;
            }
            try {
                System.out.println("t1 获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");

        //主线程上锁
        lock.lock();
        System.out.println("main 获取到锁");
        t1.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

在这里插入图片描述
在主线程还没释放到锁,t1线程是无法获取到锁的,因此reentrantLock.tryLock()返回false,执行没有锁的相关业务。如果有指定时间,在指定最大时间内没有获取到锁则失败:

package com.zs.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1尝试获取锁");
            try {
                if(!lock.tryLock(4, TimeUnit.SECONDS)){
                    System.out.println("没有获取到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println("t1 获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");

        //主线程上锁
        lock.lock();
        System.out.println("main 获取到锁");
        t1.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

当等待获取锁的时间大于主线程释放锁的时间,则肯定可以获取到锁。

4 ReentrantLock类与Condition类配合实现等待/通知机制

关键字synchronized与wait()合notify()或notifyAll()结合可以实现等待/通知机制,而ReentrantLock和Condition配合也可以实现等待/通知机制。使用notify()和notifyAll()无法选择性的通知线程,使用Condition的优点就是可以在Lock对象里面创建多个Condition(对象监视器)实例,线程对象可以注册在指定的Condition中,从而实现有选择性地进行线程通知,使线程调度更加的灵活。
Condition类方法:

await():使线程等待相当于Object对象中的wait()方法。
await(long time, TimeUnit unit):相当于Object对象中的wait(long timeout)方法。
signal():通知单个线程相当于Object对象中的notify()方法。
signalAll():通知所有线程相当于Object对象中的notifyAll()方法。

4.1 通过ReentrantLock和Condition配合实现单个线程的等待和通知

package com.zs.thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class MyService {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    /**
     *  等待
     */
    public void await(){
        try{
            lock.lock();
            System.out.println("await time: " + System.currentTimeMillis());
            condition.await();     // 相当于wait(): 使线程处于等待, 释放锁
            System.out.println("condition.await()之后的业务...");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    /**
     *  唤醒
     */
    public void signal(){
        try{
            lock.lock();
            System.out.println("signal time: " + System.currentTimeMillis());
            condition.signal();     // 相当于notify(): 唤醒condition上的另一个线程
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
class MyThread extends Thread {
    private MyService myService;
    public MyThread(MyService myService) {
        this.myService = myService;
    }
    @Override
    public void run() {
        myService.await();
    }
}
public class TestCondition {
    public static void main(String[] args) {
        try{
            MyService myService = new MyService();
            MyThread myThread = new MyThread(myService);
            // 线程启动之后调用run方法,然后执行方法中的await方法使线程处于等待状态
            myThread.start();
            // 使当前主线程睡眠,晚点执行
            Thread.sleep(5000);
            // 调用signal方法,通知MyService中的Condition(监听器)中注册的线程进行唤醒执行
            myService.signal();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

在这里插入图片描述

4.2 如果我们要指定通知的下一个进行顺序怎么办呢?

我们可以使用Condition来指定通知进程:
在这里插入图片描述

package com.zs.thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestCondition {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"C").start();
    }
}
class Data {
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int num = 1; // 1A 2B 3C

    public void printA() {
        lock.lock();
        try {
            // 业务代码 判断 -> 执行 -> 通知
            // 阻塞条件
            while (num != 1) {
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "==> AAAA");
            num = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            // 业务代码 判断 -> 执行 -> 通知
            while (num != 2) {
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "==> BBBB");
            num = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            // 业务代码 判断 -> 执行 -> 通知
            while (num != 3) {
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "==> CCCC");
            num = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

5 ReentrantLock与公平锁、非公平锁实现

所谓公平锁,就是线程按照执行顺序排成一排,依次获取锁,但是这种方式在高并发的场景下极其损耗性能;这时候,非公平锁应运而生了,所谓非公平锁,就是不管执行顺序,每个线程获取锁的几率都是相同的,获取失败了,才会采用像公平锁那样的方式。这样做的好处是,JVM可以花比较少的时间在线程调度上,更多的时间则是用在执行逻辑代码里面。

Reentrantlock可以实现公平锁和非公平锁:

   // Creates an instance of ReentrantLock. This is equivalent to using ReentrantLock(false)
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    // Creates an instance of ReentrantLock with the given fairness policy.
    // Params: fair – true if this lock should use a fair ordering policy
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

5.1 NonfairSync 非公平锁

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

        final void lock() {
            // 如果cas尝试获取锁成功(将state锁状态从0设置为1)
            if (compareAndSetState(0, 1))
                // 设置当前线程为独占锁的线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    // 如果获取失败返回false 会继续执行将当前线程链入队尾并挂起
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt(); // 中断自己
    }

如代码所示,在lock的时候,先是尝试将AQS的status从0设为1,成功的话就把当前线程设置为锁的持有者,如果尝试失败了,基于模板方法,实际调用的是Sync的nonfairTryAcquire(int acquires)方法,该方法源码如下:

       final boolean nonfairTryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
    		// 获取锁状态
            int c = getState();
            if (c == 0) {
                //如果当前没有线程在使用,直接使用cas尝试获取锁,新的线程可能抢占已经排队的线程的锁的使用权
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);// 设置当前线程独占锁
                    return true;
                }
            }
		    //如果不为0则判断当前线程是不是独占锁的线程
            else if (current == getExclusiveOwnerThread()) {
                // 如果是将锁数量状态值+1(可重入锁来源)
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
		    //请求即没有获取到锁,也不是当前独占锁的线程,返回false
            return false;
       }

非公平锁主要就是线程来了之后就进行抢占锁的操作,不管队列中是否有排队的线程与否。
在这里插入图片描述

5.2 FairSync 公平锁

公平锁就是每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中获取,下面是FairSync 的源码:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
	// 获取锁状态
    int c = getState(); 
    // 如果当前没有线程获取锁,
    if (c == 0) {
        // !hasQueuedPredecessors()保证线程都按顺序使用锁
        // 判断当前线程是否为等待队列中的头结点
        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;
}

公平锁中能够保证公平的就是这个方法 hasQueuedPredecessors() 来保证线程都按顺序使用锁,就是来判断当前线程是否处于队列中的头结点,如果不是头结点那么就需要排队,如果是头结点就去尝试获取锁。

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值