ReentrantLock 示例及原理(Synchronized区别)

AQS系列

1、AQS核心原理
2、ReentrantLock 原理及示例
3、CountDownLatch / Semaphore 示例及使用场景
4、BlockingQueue 示例及使用场景

一、ReentrantLock原理

ReentrantLock 基于 AQS 框架应用的实现,是 JDK1.5 提供的线程并发控制的手段,和 synchronized 关键字的功能类似,是一种互斥锁,可以保证线程的安全。
那么我们在实例化的时候可以发现,构造方法 ReentrantLock(boolean fair) 是带了 fair 参数的,参数为true 是公平锁,false 是非公平锁。
而且 ReentrantLock 是可重入锁,意思是我们可以多次调用 lock() 方法,重复加锁,当然这样的话就需要多次调用 unlock() 方法,多次解锁了。

ReentrantLock 是一种互斥锁,那是如何实现公平锁和非公平锁的呢?
在 ReentrantLock 内部是定义了一个 Sync 内部类,该类继承了 AbstractQueuedSynchronizer,实现了部分方法,这也是模板模式的实现。
还定义了 FairSync(公平锁) 和 NonfairSync(非公平锁) 类,这两个类都是继承自 Sync,也就间接基础了 AbstractQueuedSynchronizer ,所以 ReentrantLock 也就实现了 公平锁非公平锁
ReentrantLock 呢本身是实现了 Lock 接口来实现等待与唤醒的。

二、上公共厕所(Demo)

这里用一个小的 Demo 来使用一下 ReentrantLock 加锁功能,好多都是卖票的例子,多个窗口或者多个接口卖一张票,但是不能卖重复,那我这里就换一个场景,一个公共厕所多个门,每次只能有一个人使用。
场景是这样的,商场里面可以有多条道路通往卫生间,但是每次只能有一个人使用,除非别人同意和你一起使用 ^ _ ^,估计没人会。
现如今都是有素质的人们,如果某个时刻有多个人来了,看到有人,那就排队了,这就是公平锁。

2.1 公平锁

public class ReentrantLockTest {
    
    
    //表示排到第几个人了
    volatile static int index = 0;
    //卫生间门上有个锁,人民素质高: true, 素质低: false
    static ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {
    
    
        start();
    }

    /**
     * 开始活动
     */
    private static void start(){
    
    
        //下面实例化来卫生间的路线,有3个路线可以过来
        new Thread(() -> {
    
    
            for(;;){
    
    
                doing(index++);
            }
        }).start();
        new Thread(() -> {
    
    
            for(;;){
    
    
                doing(index++);
            }
        }).start();
        new Thread(() -> {
    
    
            for(;;){
    
    
                doing(index++);
            }
        }).start();
    }

    /**
     * 上厕所这个动作
     */
    private static void doing(int person){
    
    
        lock.lock();
        System.out.println("第 " + person + " 个人去了" + Thread.currentThread().getName() + " 坑位!");
        try {
    
    
            System.out.println("上卫生间中,顺便抽烟!");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("第 " + person + " 个人完事了!");
        System.out.println("----------------------------");
        lock.unlock();
    }
}

执行结果如下:

0 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 0 个人完事了!
----------------------------1 个人去了Thread-1 坑位!
上卫生间中,顺便抽烟!
第 1 个人完事了!
----------------------------2 个人去了Thread-2 坑位!
上卫生间中,顺便抽烟!
第 2 个人完事了!
----------------------------3 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 3 个人完事了!
----------------------------4 个人去了Thread-1 坑位!
上卫生间中,顺便抽烟!
第 4 个人完事了!
----------------------------5 个人去了Thread-2 坑位!
上卫生间中,顺便抽烟!

从执行结果可以看出来,Thread-0、Thread-1、Thread-2交替执行,很有规律,每次都是上一个人的后面,1 2 3 4 5 有顺序的,这就是公平锁。

2.2 非公平锁

我们把实例化 ReentrantLock 的参数改为 false 来看看。

static ReentrantLock lock = new ReentrantLock(false);

执行结果如下:

0 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 0 个人完事了!
----------------------------3 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 3 个人完事了!
----------------------------4 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 4 个人完事了!
----------------------------5 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!
第 5 个人完事了!
----------------------------6 个人去了Thread-0 坑位!
上卫生间中,顺便抽烟!

从执行结果可以看出,第1个人和第2个人没抢到坑位,第4、5、6个人也是 Thread-0 路线来的,所以 Thread-1 和 Thread-2 的人基本上 “憋死了”。

三、ReentrantLock 和 Synchronized 区别

不同点:

ReentrantLock Synchronized
ReentrantLock 是类实现加锁 Synchronized 是JDK内部实现,关键词加锁
ReentrantLock 是使用 tryLock() 尝试获取锁,避免死锁,更安全 Synchronized 会有死锁问题
ReentrantLock 需要考虑异常情况,在 finally{} 中释放锁 Synchronized不需要考虑异常情况
ReentrantLock 自己提供了加锁解锁功能,在特殊条件下加锁解锁需要借助 Condition对象来实现 Synchronized需要Object提供的 wait(),notify() 方法在特殊条件下来实现加锁解锁
ReentrantLock锁的粒度更细,使用更方便 Synchronized 只能在 方法上或代码块加锁

相同点:

  1. ReentrantLock 和 Synchronized 都是一个线程可以多次获取同一个锁。
  2. 都是同步方式加锁。

四、总结

其实到现在的 JDK8 之后 Synchronized 就做了优化,在并发了并不是特别高的情况下,和 ReentrantLock 差不多了。Synchronized 从开始时无锁状态,随着线程数量的增加变为 偏向锁,在升级为 轻量级锁,最后升级到 重量级锁。不过一般的电商项目或者广告项目,快递等都是高并发的项目,那种量级的情况下单体应用肯定是不行的了,不过在一般的场景下还是需要用 ReentrantLock 更灵活方便一点的。

五、Synchronized(面试扩展)

前面说到了 Synchronized 关键字加锁,但 Synchronized 关键字能单独作为一个 代码块 使用,也能添加到方法上使用,添加到代码块上面则就是锁住了这一段代码,那添加到方法上锁的是什么呢?

我们知道,Synchronized 同步代码块每次只有一个线程执行,做个小Demo来试试。
下面是一个静态方法 mF(),打印 5 次,开始和结尾打印 start 和 end.

public static void mF(){
    
    
    System.out.println("start...");
    synchronized (Object.class){
    
    
        for(int i=0; i<=5; i++){
    
    
            System.out.println("method F!" + Thread.currentThread().getName());
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
    System.out.println("end...");
}

用两个线程调用 mF() 方法。

new Thread(() -> {
    
    
            SynchronizedTest.mF();
        }).start();
        new Thread(() -> {
    
    
            SynchronizedTest.mF();
        }).start();

执行结果:

start...
start...
method F!Thread-0
method F!Thread-0
method F!Thread-0
method F!Thread-0
method F!Thread-0
method F!Thread-0
end...
method F!Thread-1
method F!Thread-1
method F!Thread-1
method F!Thread-1
method F!Thread-1
method F!Thread-1
end...

可以发现打印了两个 “start…”, Thread-0 执行完,Thread-1 才执行。

那如果是 Synchronized 关键字加载 方法上呢,我们知道,一个类可以有 静态方法实例方法,那作用在这两种方法上有什么不同呢?
答案实例锁与类锁之间互不干扰!写个例子来看看。

public synchronized static void mA(){
    
    
    for(;;){
    
    
        System.out.println("method A!");
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

public static synchronized void mB(){
    
    
    for(;;){
    
    
        System.out.println("method B!");
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

public synchronized void mC(){
    
    
    for(;;){
    
    
        System.out.println("method C!");
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

public synchronized void mD(){
    
    
    for(;;){
    
    
        System.out.println("method D!");
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

执行结果:

method A!
method D!
method C!
method C!
method D!
method A!
method D!
method A!
method C!

从执行结果上可以看出,方法 A D C 都执行了,那 B 没有执行,总结如下:
方法 A 和 B 都是静态方法,在静态方法用 Synchronized 关键字加锁,方法是同一把锁,所以只能有一个方法执行,而 方法 C 和 D 都是实例方法,和 静态方法 A 互不干涉,所以都在执行。那另个实例方法为啥能通知执行呢?因为 实例方法 上加 Synchronized 关键字加锁,锁是 this 对象,所以都会执行。

猜你喜欢

转载自blog.csdn.net/qq_19283249/article/details/128512367