Java并发编程的魅力之synchronized隐式锁和Lock显式锁
大家好,我是技术宅星云, 这篇博文我们来聊聊Java 并发编程中的锁。
1.1 写在前面的话
我们知道并发编程要处理的就是多个线程对共享资源的争夺问题,而锁的存在则是解决这类问题的一种有效解决方案。
一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)
在JDK5
之前,Java 程序是靠synchronized 关键字来实现锁功能的,JDK 5
之后,并发包中新增了Lock
接口以及相关的实现类用来实现锁功能,它提供了与synchronized 关键字类似的同步功能,只是在使用时候需要显示的获取和释放锁,这一点和synchronized 关键字略有不同,synchronized 关键字 获取和释放锁都是隐式的。因此使用Lock 接口的实现类实现的锁,由于需要显示获取和释放锁,虽然使用不再像以前那么方便,但是却增加了更好的灵活性,可以自由控制锁的获取和释放。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。
1.2 synchronized 隐式锁
同步代码块一般使用Java的synchronized 关键字来实现, 有两种方式对方法进行加锁操作:
- 第一,在方法签名处加synchronized 关键字
- 第二,使用synchronized (对象或类)进行同步。
值得注意的是,这里的原则是锁的范围尽可能小,锁的时间尽可能短,即能锁对象,就不要锁类,能锁代码块就不要锁方法。
synchronized 锁特性由JVM
负责实现。
在
JDK
的不断迭代优化中,synchronized锁的性能得到极大的提高,特别是偏向锁的实现,使得synchronized
已经不是昔日那个低性能且笨重的锁了。
那么 JVM
是如何实现synchronized 同步的?
JVM
底层是通过监视锁来实现synchronized同步的。监视锁即monitor,是每个对象与生俱来的一个隐藏字段,使用synchronized 时,JVM
会根据synchronized 的当前使用环境,找到对应对象的monitor,再根据monitor 的状态进行加,解锁的判断。- 例如,线程在进入同步方法或代码块时,会获得该方法或代码块所属对象的monitor, 进行加锁并判断。 如果加锁成功就成为该monitor
的唯一持有者,monitor被释放前,不能再被其他线程获取。
JVM
是如何对synchronized 进行优化的?
JVM
对synchronized 的优化主要在于对monitor的加锁和解锁上。JDK6
后不断优化使得synchronized 提供三种锁的实现,包括偏向锁,轻量级锁,重量级锁,还提供自动的升级和降级机制。
什么是偏向锁?
JVM
就是利用CAS
在对象头上设置线程ID,表示这个对象偏向于当前线程,这就是偏向锁。
1.3 Lock 显式锁
1.3.1 Lock显式锁示例
Lock 锁的应用场景我们来举个例子,例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock lock=new ReentrantLock();
lock.lock();
try{
....
}finally{
lock.unlock();
}
- 在
finally
块中释放锁,目的是保证在获取到锁之后,最终能够被释放。- 不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
1.3.2 Lock显式锁优点
Lock接口提供的而synchronized关键字所不具备的主要特性如下表所示:
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 与synchronized 不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常能够被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回。 |
1.3.3 Lock显式锁接口
Lock是一个接口,它定义了锁获取和释放的基本操作
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回。 |
void lockInterruptibly() throws InterruptedException |
可中断地获取锁,和lock() 方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true, 否则返回false. |
boolean tryLock(long time,TimeUnit unit)throws InterruptedException | 超时的获取锁,当前线程在以下三种情况会返回: 1. 当前线程在超时时间内获得了锁2. 当前线程在超时时间内被中断3. 超时时间结束,返回false |
void unlock() | 释放锁 |
Condition newCondition () |
获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait方法,而调用后,当前线程释放锁 |
1.3.4 Lock显式锁接口实现类
Lock 锁的实现逻辑并未用到synchronized ,而是利用了volatile 的可见性。
Lock 显式锁的继承关系如下图所示:
ReentrantLock
对于Lock接口的实现主要依赖了Sync,而Sync 继承了AbstractQueuedSynchronizer(AQS)
,它是实现并发包中实现同步的基础工具。
在AQS 中,定义了一个volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步FIFO队列中等待。
如果成功获取资源就执行临界区代码,执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队并执行。
JDK 8
提供了一个新的锁,StampedLock
,改进了读写锁 ReentrantReadWriteLock
.
本篇完~