HelloCoder HelloCoder
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
随笔
关于作者
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
随笔
关于作者
  • 《从0到1学习Java多线程》

    • (一)线程是什么
    • (二)Java线程与系统线程和生命周期
    • (三)Java线程创建方式
    • (四)为什么要使用线程池
    • (五)四种线程池底层详解
    • (六)ThreadPoolExecutor自定义线程池
    • (七)线程池的大小如何确定
    • (八)Callable和Runnable的区别
    • (九)线程池异常捕获
    • (十)线程池参数——workQueue用法
    • (十一)sleep(1)、sleep(0)和sleep(1000)的区别
    • (十二)yield、notify、notifyAll、sleep、join、wait的区别
    • (十三)synchronized
    • (十四)volatile
    • (十五)ThreadLocal的使用
    • (十六)ReentrantLock可重入锁
    • (十七)AtomicInteger
    • (十八)Worker线程管理
  • 《从0到1搭建服务器》

  • RPC

  • Spring源码

  • 《Java日志框架》

  • 可观测和监控

  • 玩转IDEA

  • 专栏
  • 《从0到1学习Java多线程》
#ReentrantLock #可重入锁
码农阿雨
2022-05-26
目录

(十六)ReentrantLock可重入锁

# 1、ReentrantLock介绍

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。

虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

两者的相同点:

1、ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

但是实现上两者不同:

synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;

ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

2、ReentrantLock和synchronized都是可重入的。

synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;

ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

不同点:

1、ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。

2、ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

3、ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

# 2、ReentrantLock的额外功能

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。(保证)

非公平锁则随机分配这种使用权。

和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。

在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

//公平锁
ReentrantLock lock = new ReentrantLock(true);

上个例子:

public class FairReentrantLock {
  //   static Lock lock = new ReentrantLock(true);
    static Lock lock = new ReentrantLock(false);
    public static void main(String[] args) {
        myThreadDemo[] threadDemos = new myThreadDemo[10];
        for (int i = 0; i < 5; i++) {
            threadDemos[i] = new myThreadDemo(i);
        }
        for (int i = 0; i < 5; i++) {
            threadDemos[i].start();
        }
    }

    static class myThreadDemo extends Thread {
        int threadId;

        myThreadDemo(int threadId) {
            this.threadId = threadId;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1 * 100);
                for (int i = 0; i < 3; i++) {
                    lock.lock();
                    System.out.println("当前获得锁的线程--->>>" + threadId);
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

可以看到非公平锁,几乎是一个线程同时获取锁后再到下一个线程执行。

如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

如果换成公平锁:

static Lock lock = new ReentrantLock(true);

可以看到,看起来就有那么一点打乱的顺序,系统会公平地分配资源给每个线程,而不是一个线程一直霸占着,线程几乎是轮流的获取到了锁。

# 3、ReentrantLock可响应中断问题

# synchronized死锁例子:

class SynchronizedDeadLock implements Runnable {
    private String lockA;
    private String lockB;

    public SynchronizedDeadLock(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
            //至关重要是这个sleep,因为这里睡眠是为了让 第二个线程有机会进来
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
            }
        }
    }
}

public class DeadLockDemo {

    public static void main(String[] args) throws InterruptedException {
        String lockA = "locka";
        String lockB = "lockb";
        new Thread(new SynchronizedDeadLock(lockA, lockB), "Thread1").start();
        new Thread(new SynchronizedDeadLock(lockB, lockA), "Thread2").start();
    }
}

# ReentrantLock死锁例子:

public class ReentrantLockDeadLock {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
        Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
        thread1.start();
        thread2.start();
    }

    static class DeadLockDemo implements Runnable {
        Lock lockA;
        Lock lockB;

        public DeadLockDemo(Lock lockA, Lock lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override
        public void run() {
            try {
                lockA.lock();
//                lockA.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
                TimeUnit.SECONDS.sleep(2);
                lockB.lock();
//                lockB.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lockA.unlock();
                lockB.unlock();
                System.out.println(Thread.currentThread().getName() + "正常结束!");
            }
        }
    }
}

以上这两个例子都会发生死锁,它们的资源竞争是这样的:

Thread1线程执行,首先获取lockA资源,上锁,然后休眠2秒...

Thread2线程执行,然后获取lockB资源,上锁,然后休眠2秒...

Thread1线程醒来,获取lockB资源,发现被锁住了,只能等待....

Thread2线程醒来,获取lockA资源,发现被锁住了,只能等待....

这样,死锁出现了....

# ReentrantLock可响应中断:

ReentrantLock的优点还是有的,它提供了lockInterruptibly()方法,用于感知线程中断,从而退出程序。

public class ReentrantLockDeadLock {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
        Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
        thread1.start();
        thread2.start();

        Thread.sleep(5 * 1000);
        if (Thread.activeCount() >= 4) {
            thread1.interrupt();//让thread1线程中断
        }
    }

    static class DeadLockDemo implements Runnable {
        Lock lockA;
        Lock lockB;

        public DeadLockDemo(Lock lockA, Lock lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override
        public void run() {
            try {
//                lockA.lock();
                lockA.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
                TimeUnit.SECONDS.sleep(2);
//                lockB.lock();
                lockB.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lockA.unlock();
                lockB.unlock();
                System.out.println(Thread.currentThread().getName() + "正常结束!");
            }
        }
    }
}

结果:

程序在休眠5秒后,假如Thread.activeCount() >= 4,主线程、守护线程、Thread1、Thread2 四个都在, 表示死锁发生了。

通过 thread1.interrupt(),中断了Thread1,lockInterruptibly()方法 会释放lockA、lockb的锁,即 lockA.unlock()、lockB.unlock();Thread2感知了,就可以获取loackB的资源,即可以上锁,然后正常退出。

所以ReentrantLock相比synchronized的优势就是:无限等待获取锁的行为可以被中断

# 4、ReentrantLock锁限时等待

线程中断不是处理死锁特别好的方法,万一线程真的是执行了很久,而不是死锁了,如果贸然中断,可不是一个明智的处理方法。

ReentrantLock 提供了一个tryLock() 方法,可以指定获取锁的等待时间。

tryLock()如果拿到锁就返回true,否则返回false,不会像lock那样无限等待。

if (!lockA.tryLock(2, TimeUnit.SECONDS)) {
    System.out.println(Thread.currentThread().getName() + " 正在等待锁......");
} else {
    System.out.println(Thread.currentThread().getName() + " 拿到了锁");
}

tryLock()也会有死锁的情况,所以为了避免死锁,一个线程不要获取多个锁。

# 5、Condition

synchronized可以结合Object进行线程之间的通信,比如说wait和notify实现线程的等待和唤醒。

ReentrantLock也有,Java提供了Condition 接口,可以实现ReentrantLock线程之间的通信。

eg:

public class ConditionTest {
    static ReentrantLock lock = new ReentrantLock();
    //通过ReentrantLock创建Condition实例,并与之关联
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程执行ing...");
        new Thread(new AwaitThread()).start();
        try {
            Thread.sleep(2000);
            lock.lock();
            condition.signal();
        } finally {
            lock.unlock();
        }
        System.out.println("主线程执行结束");
    }

    static class AwaitThread implements Runnable {
        @Override
        public void run() {

            System.out.println("子线程执行ing...");
            lock.lock();
            try {
                System.out.println("子线程停止了");
                condition.await();
                System.out.println("子线程恢复执行了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

结果:

主线程执行ing...
子线程执行ing...
子线程停止了
主线程执行结束
子线程恢复执行了

# 6、ReentrantLock底层原理

ReentrantLock底层使用了CAS+AQS队列(也叫CLH队列)实现:

# 6.1、 CAS(Compare and Swap)

CAS,Compare and Swap,比较并交换,是一种无锁算法。

CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则重新获取内存地址V的当前值,并重新计算想要修改的值(重新尝试的过程被称为自旋)。

# 6.2、AQS队列

AQS是一个用于构建锁和同步容器的框架。

AQS使用一个FIFO的队列(也叫CLH队列),表示排队等待锁的线程。

也称为CLH队列:带头结点的双向非循环链表(如下图所示):

img

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

  • 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
  • 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

# 总结:

ReentrantLock比起synchronized功能更加丰富,支持公平锁和非公平锁,而且提供了响应中断。

而且提供了tryLock()锁限时等待,相比synchronized要更灵活。


参考:

  • https://www.cnblogs.com/takumicx/p/9338983.html (opens new window)
  • https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html (opens new window)
阅读全文
×

(为防止恶意爬虫)
扫码或搜索:HelloCoder
发送:290992
即可永久解锁本站全部文章

解锁
#ReentrantLock#可重入锁
上次更新: 2025-02-21 06:04:57
最近更新
01
《LeetCode 101》
02-21
02
IDEA、Golang、Pycharm破解安装
02-21
03
《LeetCode CookBook》
02-21
更多文章>
Theme by Vdoing | Copyright © 2020-2025 码农阿雨
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式