7. 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.


本篇完~

发布了162 篇原创文章 · 获赞 219 · 访问量 40万+

猜你喜欢

转载自blog.csdn.net/hadues/article/details/103726212