这篇文章,聊聊 Lock
接口以及它的核心方法,以及锁的分类,各种锁是什么,什么特点,怎么玩。
1 Lock
和 synchronized
的比较
锁是一种工具,用于控制对共享资源的访问。Lock
和 synchronized
,这两个是最常见的锁,都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。
在前面文章 juc-03-synchronized、notify、notifyAll、wait、volatile 中,我们学习了 synchronized
关键字创建一把内置锁。
synchronized 内置锁分两种:
- 对象锁:锁的是类的
对象实例
。作用在实例方法,或者实例代码块上。 - 类锁:锁的是
类的Class对象
,每个类的的Class对象在一个JVM中只有一个,所以类锁也只有一个。作用在 static 方法,或者 static 代码块上。
Lock
并不是用来代替 synchronized
的,而是当使用 synchronized
不合适或者不足以满足要求的时候,来提供高级功能的。
为什么 synchronized 不够用?
1)效率低
:锁的释放情况少,试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
2) 不够灵活(读写锁更灵活)
: 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
3)无法知道是否成功获取到锁
Lock 特点:
1)通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现可以允许并发访问,比如 ReadWriteLock
里面的ReadLock
2)获取锁可以被中断,超时获取锁,尝试获取锁,读多写少用 ReadWriteLock
读写锁
2、Lock 接口和核心方法
2.1 Lock 接口源码
public interface Lock {
/**
* 获取锁,如果锁已经被其他线程获取,则进行等待
*/
void lock();
/**
* 尝试获取锁,直到线程被中断
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
*/
boolean tryLock();
/**
* 给定等待时间内尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 返回一个绑定当前 Lock 实例的新的 Condition 实例
*/
Condition newCondition();
}
2.2、Lock 主要方法介绍
在Lock中声明了四个方法来获取锁和一个释放锁的方法:
lock()
:获取锁,如果锁已经被其他线程获取,则进行等待tryLock()
:尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败tryLock(long time, TimeUnit unit)
:给定等待时间内尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败lockInterruptibly()
:尝试获取锁,直到线程被中断unlock()
: 释放锁,一般在finally
中执行
Lock
最常见的实现类是 ReenTrantLock
,我们使用 ReenTrantLock
了解这四个获取锁的方法有何区别。
2.2.1、lock()
- lock() 就是最普通的获取锁。如果锁已经被其他线程获取,则进行等待
- Lock
不会
像 synchronized 一样在异常时自动释放锁
- 因此最佳实践是,在
finally中释放锁 lock.unlock()
,以保证发生异常时,锁一定被释放 lock()弊端
lock() 方法不能被中断,这会带来很大隐患:一旦陷入死锁
,lock()
就会陷入永久等待
finally中释放锁
public class LockTest {
// 显式锁 Lock
private static Lock lock = new ReentrantLock();
@Test
public void testLock(){
lock.lock();
try{
//获取本锁保护的资源
System.out.println(Thread.currentThread().getName()+"开始执行任务");
}finally {
// 很重要,必须记得在 finally unlock()
// Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
lock.unlock();
}
}
}
2.2.2、tryLock()
tryLock()
用来尝试获取锁
,如果当前锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败- 功能比
lock()
更强大了,可以根据是否获取到锁,决定后续程序的行为 - 该方法会立即返回,即便在拿不到锁时,不会一直在那等
2.2.2、tryLock(long time, TimeUnit unit) 超时就放弃,
用 tryLock 可以避免死锁
/**
* tryLock(long time, TimeUnit unit) 避免死锁演示
*
* @throws InterruptedException
*/
@Test
public void testTryLock() throws InterruptedException {
// 模拟两个线程同时抢占 lock1 和 lock2 ,使用 tryLock(long time, TimeUnit unit) 尝试获取锁,并避免死锁发生
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
new Thread(() -> {
// 尝试获取 lock1 , 再获取 lock2,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
lock1Tolock2(lock1, lock2);
}).start();
new Thread(() -> {
// 尝试获取 lock2 , 再获取 lock1,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
lock2Tolock1(lock1, lock2);
}).start();
Thread.sleep(10000);
}
/**
* 尝试获取 lock1 , 再获取 lock2,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
*
* @param lock1
* @param lock2
*/
private void lock1Tolock2(Lock lock1, Lock lock2) {
boolean success = false;
while (!success) {
try {
// 先尝试获取 lock1,100ms 内获取不到,则重试
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
// 尝试获取 lock2,如果 100ms 内获取不到,则返回false
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
success = true;
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "获取到了锁2");
System.out.println(Thread.currentThread().getName() + "成功获取到了两把锁");
} finally {
//释放锁 lock2
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁2");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁2失败,已重试");
}
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁1");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁1失败,已重试");
}
// sleep 一会再重试
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 尝试获取 lock2 , 再获取 lock1,只要任何一把锁超时获取不到,则释放已占有的锁,sleep 一会,再重试
*
* @param lock1
* @param lock2
*/
private void lock2Tolock1(Lock lock1, Lock lock2) {
boolean success = false;
while (!success) {
try {
// 尝试获取 lock2,如果 100ms 内获取不到,则返回false
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
// 尝试获取 lock1,如果 100ms 内获取不到,则返回false
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
success = true;
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "获取到了锁1");
System.out.println(Thread.currentThread().getName() + "成功获取到了两把锁");
} finally {
//释放锁 lock2
lock1.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁1");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁1失败,已重试");
}
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁2");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁2失败,已重试");
}
// sleep 一会再重试
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
Thread-1获取到了锁2
Thread-0获取到了锁1
Thread-1获取锁1失败,已重试
Thread-1释放了锁2
Thread-1获取锁2失败,已重试
Thread-0获取到了锁2
Thread-0成功获取到了两把锁
Thread-0释放了锁2
Thread-0释放了锁1
Thread-1获取到了锁2
Thread-1获取到了锁1
Thread-1成功获取到了两把锁
Thread-1释放了锁1
Thread-1释放了锁2
2.2.3、lock.lockInterruptibly() 尝试获取锁,直到线程被中断
相当于tryLock(long time, TimeUnit unit)把时间设置为无限
。在等待锁的过程中,线程可以被**中断
**。
/**
* lock.lockInterruptibly() 尝试获取锁,直到线程被中断
* @throws InterruptedException
*/
@Test
public void testLockInterruptibly() throws InterruptedException {
Lock lock = new ReentrantLock();
Thread thread1 = new Thread(() -> {
doRun(lock);
});
Thread thread2 = new Thread(() -> {
doRun(lock);
});
thread1.start();
thread2.start();
// 标志 thread1 中断
thread2.interrupt();
Thread.sleep(10000);
}
private void doRun(Lock lock) {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
}
}
运行结果:
Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-1获得锁期间被中断了
Thread-0释放了锁
3、锁的分类
- 线程要不要锁住同步资源?
锁住:悲观锁
不锁柱:乐观锁 - 同一个线程是否可以重复获取同一把锁
可以:可重入锁
不可以:不可重入锁 - 多线程竞争时,是否排队
排队:公平锁
先尝试插队,插队失败再排队:非公平锁 - 多线程能否共享一把锁
可以:共享锁
不可以:独占锁 - 等待锁过程
自旋:自旋锁
阻塞:非自旋锁 - 是否可中断
可以:可中断锁
不可以:非可中断锁
- 这些分类,是从各种
不同的角度
出发去看的 - 这些分类并
不是互斥
的,也就是多个类型可以并存
:有可能一个锁,同时属于多种类型,比如ReentrantLock
既是互斥锁
,又是可重入锁
。
4、乐观锁和悲观锁
4.1 为什么会诞生"非互斥同步锁"(乐观锁)——互斥同步锁(悲观锁)的劣势
>互斥同步锁(悲观锁)的劣势
>- 阻塞
和 唤醒
带来的性能劣势
>- 永久
阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行。
4.2 什么是乐观锁和悲观锁
>从是否锁住资源
的角度分类
悲观锁
>如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问数据,这样就可以确保数据内容万无一失。
java中悲观锁的实现就是 synchronized
和 Lock
相关类。
场景举例:
- 线程1和线程2同时抢悲观锁
- 线程1抢到锁,线程2等待
- 线程1释放锁后,线程2拿到锁
- 线程2释放锁
乐观锁
>认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住
被操作的对象
在更新
的时候,去对比在我修改的时候,数据有没有被其他人改变过:
- 如果
没被改变过
,就说明真的是只有我自己在操作,那我就正常去修改数据; - 如果数据和我一开始拿到的
不一样
了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃,报错,重试等策略
乐观锁的实现一般都是利用CAS
算法来实现的。
乐观锁逻辑举例,类似 CAS
算法:
- 线程1和线程2同时获取共享资源并各自计算
- 线程1先计算完成,并判断资源是否已被修改,线程1发现计算期间资源没有被修改,于是把自己的计算结果写到资源中
- 线程2后计算完成,并判断资源是否已被修改,发现资源已经被修改过,于是报错或者重试
4.3 典型例子
- 悲观锁:
synchronized
和lock
接口,数据库select for update
就是悲观锁 - 乐观锁的典型例子就是
原子类、并发容器
等
4.4 开销对比
- 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
- 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
4.5 两种锁各自的使用场景
悲观锁
适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
- 临界区有
IO
操作 - 临界区
代码复杂
或者循环量大 - 临界区
竞争非常激烈
乐观锁
适合并发写入少,大部分是读取
的场景,不加锁能让读取性能大幅提高
5、可重入锁与非可重入锁,以 ReentrantLock 为例
>可重入锁也叫递归锁:一个线程可以多次拿到这把锁,无需释放锁,就可以直接获取到。
可重入锁
ReentrantLock 部分源码
public class ReentrantLock implements Lock, java.io.Serializable {
...
// 获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 获取锁时,先判断占有锁的线程是不是正在请求锁的线程,如果是,则设置 state 为重复占用锁的数量
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 先判断占有锁的线程是不是正在请求锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果剩余占有当前锁的数量 == 0,则完全释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
...
}
不可重入锁
/**
* 获取锁
*
* @param arg
* @return
*/
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// 获取锁失败,锁已经被占用,则返回 false
return false;
}
/**
* 释放锁
*
* @param arg
* @return
*/
protected boolean tryRelease(int arg) {
if (getState() == 0) {
//锁未被占用时,不支持释放锁操作
throw new UnsupportedOperationException();
}
// 设置没有线程占有锁
setExclusiveOwnerThread(null);
//释放锁,只有拥有锁的线程有资格 setState(0);所以这里不需要用原子操作
setState(0);
return true;
}
好处:
- 避免死锁:如果有两个方法,都被一把锁锁住,运行到第一个方法拿到这把锁,运行到第二个方法,如果不是可重入锁,就需要等锁释放才可以获取,可能会造成死锁。
- 提升封装性:避免一次次加锁解锁
可重入锁 ReentrantLock
/**
* Description: 可重入锁 ReentrantLock
* @author Xander
* datetime: 2020-11-12 18:26
*/
public class ReentrantLockDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println(lock.getHoldCount());
if (lock.getHoldCount() < 5) {
// 如果重复占有锁的数量小于5,重复获取锁
accessResource();
}
System.out.println("已经对资源进行了处理");
} finally {
lock.unlock();
System.out.println("释放锁后:" + lock.getHoldCount());
}
}
public static void main(String[] args) {
accessResource();
}
}
运行结果:
1
2
3
4
5
已经对资源进行了处理
释放锁后:4
已经对资源进行了处理
释放锁后:3
已经对资源进行了处理
释放锁后:2
已经对资源进行了处理
释放锁后:1
已经对资源进行了处理
释放锁后:0
ReentrantLock 的其他方法
isHeldByCurrentThread()
:看出锁是否被当前的线程持有getQueueLength()
:返回当前正在等待这把锁的队列有多长
一般是开发和调试时候使用这两个方法
6、公平锁与非公平锁
6.1 什么是公平和非公平
>公平指的是按照线程请求的顺序,来分配锁;
>非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
>注意:
非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。
6.2 为什么要有非公平锁
java设计者这样设计的目的,是为了提高效率,避免唤醒带来的空档期
,提高吞吐量。
6.3 公平和不公平的情况(以ReentrantLock为例)
公平
线程1、线程2、线程3、线程4 依次排队等待锁,线程1等待时间最长,先获取到锁,在线程1执行 unlock() 释放锁之后,此时,线程2等待时间最久,线程2会获取到锁,然后再依次是线程3,线程4.
不公平
线程1、线程2、线程3、线程4 依次排队等待锁,线程1获取到锁,如果在线程1释放锁的时候,线程5恰好去执行lock(),由于 ReentrantLock 发现此时并没有线程持有
lock 这把锁(线程2还没来得及获取到
,因为获取需要时间),线程5可以插队,直接拿到这把锁,这也是 ReentrantLock默认的策略
,也就是“不公平”。
6.4 如何创建公平锁和非公平锁
- ReentrantLock():ReentrantLock无参构造器,创建非公平锁
- ReentrantLock(boolean fair):如果 fair 等于 true,则是公平锁,否则是非公平锁。
ReentrantLock 构造器源码
public class ReentrantLock implements Lock, java.io.Serializable {
...
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
...
}
特例 tryLock()
针对 tryLock()
方法,它是很猛的,它不遵守设定的公平的规则
例如,当有线程执行 tryLock() 的时候,一旦线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了。
6.5 对比公平和非公平的优缺点
优势 | 劣势 | |
---|---|---|
公平锁 | 各线程公平平等,每个线程等待一段时间后,总有执行的机会 | 更慢,吞吐量更小 |
不公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿,也就是某些线程在长时间内始终,得不到执行 |
6.6 源码分析
ReentrantLock 类中公平锁源码
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors() 查询是否有任何线程等待获取的时间比当前线程长。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
ReentrantLock 类中非公平锁源码
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果当前锁未被占用,直接尝试获取锁,不排队
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
7、共享锁与排它锁
7.1 什么是共享锁与排它锁
>排他锁,又称独占锁,独享锁,例如 synchronized
>共享锁,又称为读锁,获得共享锁
之后,可以查看但无法修改和删除
数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除
数据。
共享锁与排它锁的典型就是读写锁 ReentranReadWriteLock
,其中读锁是共享锁,写锁是排他锁。
7.2 读写锁的作用
- 在没有读写锁之前,我们假设使用
ReentrantLock
,那么虽然我们保证了线程安全,但也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题。 - 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。
7.3 读写锁的规则
-
- 多个线程只申请读锁,都可以申请到
-
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
-
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
>总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)
7.4 ReentranReadWriteLock 具体用法
演示 ReentrantReadWriteLock
public class ReadWriteLockDemo {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(() -> read(), "Thread1").start();
new Thread(() -> read(), "Thread2").start();
new Thread(() -> read(), "Thread3").start();
new Thread(() -> read(), "Thread4").start();
new Thread(() -> write(), "Thread5").start();
new Thread(() -> write(), "Thread6").start();
}
}
运行结果:
Thread1得到了读锁,正在读取
Thread2得到了读锁,正在读取
Thread3得到了读锁,正在读取
Thread4得到了读锁,正在读取
Thread2释放读锁
Thread3释放读锁
Thread1释放读锁
Thread4释放读锁
Thread5得到了写锁,正在写入
Thread5释放写锁
Thread6得到了写锁,正在写入
Thread6释放写锁
7.5 读锁插队策略
插队抢锁。
>策略的选择取决于具体锁的实现,ReentrantReadWriteLock 的实现是选择了 策略2
,是很明智的。
总结:
-
- 公平锁:不允许插队
-
- 非公平锁: 写锁可以随时插队(尝试去抢,抢不到就排队),读锁仅在
等待队列头节点不是想获取写锁的线程
的时候可以插队。
- 非公平锁: 写锁可以随时插队(尝试去抢,抢不到就排队),读锁仅在
7.6 锁的升降级
>支持锁的降级,不支持升级
,如果已经获取写锁,则可以在不释放写锁的情况下,直接获取读锁,成功降级。
>为什么不支持锁的升级?死锁
7.7 共享锁和排他锁总结
-
- ReentrantReadWriteLock 实现了 ReadWriteLock 接口,最主要的有两个方法:readLock() 和 writeLock() 用来获取读锁和写锁
-
- 锁申请和释放的策略
1)多个线程只申请读锁,都可以申请到
2)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
4)要么是一个或者多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。
- 锁申请和释放的策略
总结:要么多读,要么一写
-
- 插队策略:为了防止饥饿,读锁不能插队
-
- 升降级策略:只能降级,不能升级
-
- 使用场合:相比于 ReentrantLock 适用于一般场合,
ReentrantReadWriteLock
适用于读多写少的情况,合理使用可以进一步提高并发效率。
- 使用场合:相比于 ReentrantLock 适用于一般场合,
8、自旋锁与阻塞锁
8.1 概念
>阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
。
>在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复线程
的花费可能会让系统得不偿失。如果物理机器有多个处理器,能让两个或者以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就释放锁。而为了让当前线程“稍等一下
”,我们需要让当前线程进行自旋,如果在自旋完成后,前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销
,这就是自旋锁。
阻塞锁
和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,等待被唤醒。
8.2 自旋锁缺点
- 如果锁被占用的时间很长,那么自旋锁的线程只会白浪费处理器资源
在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
8.3 原理和源码分析
在java1.5 版本以上的并发框架 java.util.concurrent
的 atomic
包下的类基本都是自旋锁的实现。
AtomicInteger
的实现:自旋锁的实现原理是 CAS
,AtomicInteger
中调用 unsafe 进行自增操作的源码中的 do-while 循环
就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在 while 里死循环,直至修改成功
AtomicInteger 中的 getAndAddInt 实现
public class AtomicInteger extends Number implements java.io.Serializable {
...
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
public final class Unsafe {
...
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
...
}
8.4 适用场景
- 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
- 另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放,持有锁的时间长),那也是不合适的。
9、可中断锁
synchronized
是不可中断锁,而 lock
是可中断锁,因为 trylock(time)
与 lockInterruptibly()
都可以相应中断。
>如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁。
上面,我们比较了 Lock
和 synchronized
两种锁的用法, synchronized
使用简单,但是 Lock
可控性更强。我们通过源码学习了 Lock
的核心方法,以及通过 Lock
接口的实现类 ReentrantLock
来简单的使用了 Lock
的核心 API。最后,我们详细地介绍了锁的分类,各种锁的特点,怎么新建,如何使用。
共同学习,写下你的评论
评论加载中...
作者其他优质文章