一、概述
在并发编程中,可以通过关键字 synchronized
和 Lock
来实现同步访问。
问题: 两种同步机制有什么区别呢?
使用 synchronized
关键字会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。
使用 Lock
需要手动调用锁的获取和释放,扩展性比较高,一般使用 before...after模式
。
/*
* 获取锁的操作不能放在try中。
* 理由:避免在使用自定义 Lock 时,执行获取锁操作失败后自动释放了锁(获取锁失败,则锁本身没有被获取,因此无需释放锁)。
*/
lock.lock() //获取锁 before
try {
// do something
} finally {
lock.unlock() //释放锁 after
}
Lock
接口具备的优势:
二、Lock 接口 API 介绍
Lock
中几个方法的含义:
lock()
方法是用来获取锁的,如果锁已被其他线程获取,则进行等待。lockInterruptibly()
方法在获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断。tryLock()
方法是用来尝试获取锁,该方法会立即返回。- 如果获取锁成功,则返回true;
- 如果获取锁失败,则返回false;
tryLock(long time, TimeUnit unit)
方法在获取不到锁的情况下,会等待指定时间(unit
)后立即返回。
三、JDK 中 Lock 的类图
在JDK1.5之后,为我们提供了几种常用的类。
ReentrantLock
:可重入锁。ReentrantReadWriteLock
:可重入读写锁。AbstractQueuedSynchronizer
:定义了基本操作的同步器,可用于自定义同步器。ConditionObject
:内部维护了一个等待队列。
三、锁的分类
-
悲观锁、乐观锁
锁的一种宏观分类可分为:悲观锁和乐观锁,他们是在并发情况下的两种不同策略。悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点。悲观锁(Pessimistic Lock):
- 读数据:默认每次读数据时都认为别人会修改,所以在每次拿数据时都会加锁。
乐观锁(Optimistic Lock): 不是锁,而是一种循环重试CAS的算法。
- 读数据:默认每次去读数据时都认为别人不会修改,所以不会上锁。
- 写数据:会在写数据前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功。
-
自旋锁
没有抢到锁的线程进入自旋,即不停地循环判断锁是否能够被成功获取。 -
可重入锁
允许同一个线程多次获取同一把锁。 -
公平锁、非公平锁
公平锁: 多个线程申请一把锁,按照FIFO的顺序获得锁。
非公平锁: 多个线程申请一把锁,后申请的线程可能先获取到锁。 -
读写锁、共享锁、互斥锁
共享锁: ReentrantReadWriteLock中,读锁允许多个线程一起读操作。
互斥锁: ReentrantReadWriteLock中,写锁只允许一个线程进行写操作。 -
锁的升级
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
无锁: 什么都不用干。
偏向锁: 在对象头部将为偏向线程ID赋值。(对象头部可以存放很多信息,其中有对象的分代年龄、偏向线程ID等。)
轻量级锁: 自旋锁 CAS
重量级锁: synchornized、Lock等,会让线程进入阻塞队列等待被唤醒。