决战紫禁之巅之synchronized和ReentrantLock

面试官:阳仔,在我们日常并发场景下的开发过程中,锁的运用是必不可少的,那你能告诉我Java中有哪些锁的实现吗

阳仔:当然可以!Java中的锁实现可不少,咱们可以从“锁的种类”和“锁的实现方式”两个角度来聊。
1、从锁的种类上:
乐观锁:比如CAS(Compare And Swap),它是一种无锁机制,假设操作不会冲突,先干了再说,如果冲突了就重试。典型的例子是AtomicInteger。

悲观锁:比如synchronized和ReentrantLock,它们假设操作一定会冲突,所以先加锁再操作。

2 从锁的实现方式分类:
内置锁(synchronized):这是Java最原生的锁,简单易用,直接加在方法或代码块上。

显式锁(ReentrantLock):这是java.util.concurrent.locks包下的锁,需要手动加锁和释放锁。

读写锁(ReentrantReadWriteLock):读写分离,读操作可以并发,写操作独占。

分布式锁:比如用Redis或Zookeeper实现的锁,用于分布式系统。

自旋锁:通过循环尝试获取锁,适合短时间的锁竞争。

条件锁(Condition):配合ReentrantLock使用,可以实现线程的等待和唤醒。

面试官:不错嘛,看来你对锁的种类很熟悉。那你能详细说说ReentrantLock和synchronized的区别吗?

阳仔:当然可以!ReentrantLock和synchronized是Java中最常用的两种锁,但它们之间有很多不同点。咱们从几个方面来聊:

1、首先是锁的实现方式
synchronized:这是Java的关键字,属于JVM层面的锁。它的加锁和释放锁是隐式的,JVM会自动处理。比如:


synchronized (obj) {
    // 代码块
}

你看,多省事,连释放锁都不用管。

而synchronized的实现原理:

(1)首先synchronized是通过monitor对象来实现锁机制的。每个Java对象都有一个monitor与之关联,monitor可以理解为一种同步工具,它内部维护了一个计数器,用来记录线程重入的次数。

(2)当一个线程进入synchronized代码块时,它会尝试获取monitor的所有权。如果获取成功,计数器加1;如果获取失败,线程会进入阻塞状态,直到monitor被释放。

(3)当线程退出synchronized代码块时,计数器减1。当计数器为0时,monitor被释放,其他线程可以尝试获取。

ReentrantLock:这是java.util.concurrent包下的一个类,属于API层面的锁。它的加锁和释放锁是显式的,需要手动调用lock()和unlock()。比如:

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 代码块
} finally {
    lock.unlock();
}

你看,这得多写几行代码,但灵活性也更高。

ReentrantLock的实现原理:

(1) ReentrantLock是基于AQS(AbstractQueuedSynchronizer)实现的。AQS是一个用于构建锁和同步器的框架,它内部维护了一个state变量和一个等待队列。

(2)当一个线程尝试获取锁时,它会通过CAS操作尝试将state从0改为1。如果成功,表示获取锁成功;如果失败,线程会被加入到等待队列中,并进入阻塞状态。

(3)当锁被释放时,AQS会从等待队列中唤醒一个线程,让它尝试获取锁。

面试官:嗯,那它们的性能有什么区别呢?

阳仔:
synchronized:在早期的Java版本中,synchronized的性能比较差,因为它涉及到用户态和内核态的切换。但自从Java 6之后,JVM对synchronized做了很多优化,比如引入了“偏向锁”、“轻量级锁”和“重量级锁”的机制,性能已经大幅提升。在低竞争的场景下,synchronized的性能和ReentrantLock差不多。

synchronized的性能优化:

**偏向锁:**当一个线程第一次获取锁时,JVM会将锁标记为偏向锁,并记录下线程ID。如果以后这个线程再次尝试获取锁,JVM会直接让它获取,不需要进行任何同步操作。

**轻量级锁:**如果多个线程竞争锁,JVM会将偏向锁升级为轻量级锁。轻量级锁是通过CAS操作来实现的,避免了线程的上下文切换。

**重量级锁:**如果竞争非常激烈,JVM会将轻量级锁升级为重量级锁。重量级锁是通过操作系统的互斥量来实现的,性能较差。

ReentrantLock:它的性能在高竞争场景下表现更好,因为它底层使用了CAS(Compare And Swap)机制,避免了线程的上下文切换。但它的实现更复杂,代码量也更多。

面试官:那它们在使用上有什么不同呢?

阳仔:

synchronized

简单易用,直接加在方法或代码块上。

不支持中断,如果一个线程在等待锁,它只能一直等下去。

不支持超时,如果一个线程在等待锁,它只能一直等下去。

不支持公平锁,锁的获取是非公平的,谁抢到就是谁的。

ReentrantLock

需要手动加锁和释放锁,灵活性更高。

支持中断,如果一个线程在等待锁,可以调用lockInterruptibly()方法中断等待。

支持超时,可以设置获取锁的超时时间,比如tryLock(5, TimeUnit.SECONDS)。

支持公平锁,可以通过构造函数设置是否为公平锁。

面试官:这里说的公平锁和非公平锁是什么意思?

阳仔:

公平锁:锁的获取是按照线程的等待顺序来的,先到先得。比如:

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

公平锁的好处是避免了“饥饿”现象,但性能会差一些,因为需要维护一个队列。

非公平锁:锁的获取是随机的,谁抢到就是谁的。比如:

ReentrantLock lock = new ReentrantLock(); // 默认是非公平锁

非公平锁的性能更好,但可能会导致某些线程一直抢不到锁。

面试官:那它们的功能上还有什么区别吗?

阳仔:synchronized只能绑定一个条件变量(wait()和notify()),无法知道是否成功获取锁。
而ReentrantLock:可以绑定多个条件变量(Condition),比如:

Condition condition = lock.newCondition();
condition.await(); // 等待
condition.signal(); // 唤醒

可以通过tryLock()尝试获取锁,并返回是否成功。

面试官:听起来ReentrantLock功能更强大啊,那为什么还要用synchronized呢?

阳仔:其实主要有以下几点:
简单易用:synchronized是Java的关键字,使用起来非常方便,不需要手动释放锁。

性能足够:JVM对synchronized做了很多优化,比如偏向锁、轻量级锁等,在低竞争的场景下,synchronized的性能已经足够好。

代码简洁:synchronized的代码更简洁,适合简单的同步场景。

面试官:那你平时更喜欢用哪个?

阳仔:总的来说,简单场景,没有很高的并发和竞争情况,我更喜欢用synchronized,因为代码简洁,不容易出错。

而复杂场景,存在高并发和激烈的锁竞争,则需要更灵活的控制,比如超时、中断、公平锁等,我会选择ReentrantLock。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值