Lock
Lock 简介、地位、作用
锁是一种工具, 用于控制对共享资源的访问。
Lock 和 synchronzied,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。
Lock 并不是用来代替 synchronized 的,而是当使用 synchronized 不适合或不足以满足要求的时候,来提供高级功能的。
Lock 接口最常见的实现类是 ReentrantLock.
为什么需要 Lock?
synchronized 的痛点
- 效率低:锁的释放情况少(等待 IO 时候也不释放锁,无法中途跳出,白白浪费 CPU 时间片),试图获得锁时不能设置超时(死锁的成因之一)、不能中断一个正在试图获得锁的线程。
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
- 无法知道是否成功获取到锁。
Lock 主要方法介绍
在 Lock 中声明了四个方法来获取锁。
lock()
tryLock()
tryLock(long time, TimeUnit unit)
lockInterruptibly()
lock()
- 就是最普通的获取锁,如果锁已被其他线程获取,则进行等待。
- Lock 不会像 synchronized 一样,在异常时自动释放锁。
- 因此最佳实践是,在 finally 中释放锁,以保证发生异常时锁一定被释放。
- lock() 方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock() 就会陷入永久等待。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: Lock 不会像 synchronized 那样,异常时自动释放锁
* 最佳实践需要利用 finally 释放锁
*/
public class MustUnlock {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
//获取本所保护的资源
System.out.println(Thread.currentThread().getName() + "开始执行任务");
}finally {
lock.unlock();
}
}
}
tryLock()
- tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功(返回 true),否则获取失败(false)。
- 相比于 lock(),这样的方法显然功能更强大了,可以根据是否能获取到锁来决定后续程序的行为。
- 该方法会立即返回,即便在拿不到锁时不会一直在等待。
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 演示 tryLock()
*/
public class TryLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread.sleep(25);
new Thread(new Runnable() {
@Override
public void run() {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到了lock.");
Thread.sleep(50);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取lock失败!");
}
}
}).start();
}
}
}
tryLock(long Time, TimeUnit unit)
- 超时就放弃。
- 可以很好的避免死锁。
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 演示 tryLock(long Time, TimeUnit unit): 超时就放弃
* 避免死锁
*
* Thread-1 获取到了lock2
* Thread-0 获取到了lock1
* Thread-0 获取lock2失败, 已重试
* Thread-1 获取到了lock1
* Thread-1 已获取两把锁
* Thread-0 获取到了lock1
* Thread-0 获取到了lock2
* Thread-0 已获取两把锁
*/
public class TryLockWithOutDeadLock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockWithOutDeadLock r1 = new TryLockWithOutDeadLock();
TryLockWithOutDeadLock r2 = new TryLockWithOutDeadLock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到了lock1");
Thread.sleep(new Random().nextInt(1000));
//获取lock2
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到了lock2");
System.out.println(Thread.currentThread().getName() + " 已获取两把锁");
break;
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));//避免刚解锁就又获取
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取lock2失败, 已重试");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));//避免刚解锁就又获取
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取lock1失败, 已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到了lock2");
Thread.sleep(new Random().nextInt(1000));
//获取lock2
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到了lock1");
System.out.println(Thread.currentThread().getName() + " 已获取两把锁");
break;
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));//避免刚解锁就又获取
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取lock1失败, 已重试");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));//避免刚解锁就又获取
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取lock2失败, 已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
lockInterruptibly()
- 相当于 tryLock(long time, TmeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断(synchronized 阻塞的过程中不可以被中断)。
- lockInterruptibly() 在获取锁的过程中如果被 interrupt(),则会抛出
InterruptedException
。
import sun.awt.windows.ThemeReader;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 在获取锁的过程中,可以在中途被打断。
*/
public class LockUnterrtuptibly implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockUnterrtuptibly instance = new LockUnterrtuptibly();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 尝试获取锁");
try {
//尝试获取锁
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取到了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("睡眠期间被中断");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("等锁期间被中断");
}
}
}
unlock()
- 解锁,一定要放在 finally {} 中执行。因为 Lock 不会像 synchronized 一样,在异常时自动释放锁
锁的可见性保证
- Lock 的加锁和 synchronized 有同样的内存语义(happens-before 保证了下一个线程加锁后可以看到前一个线程解锁前,发生的所有操作。)
锁的分类
- 分类是从不同角度出发去看的。
- 分类并不是互斥的,同一个锁从不同的角度去看可以同时属于多种类型。例如:
ReentrantLock
即是互斥锁,又是可重入锁。
乐观锁、悲观锁
互斥同步锁的劣势(悲观锁)
Java 中悲观锁的实现就是 synchronized(经过优化前面会有一些乐观的过程,但整体还是悲观锁)、Lock 相关类。
- 阻塞和唤醒带来的性能劣势
* 用户态、核心态切换
* 上下文切换
* 检测有没有阻塞线程,哪些需要被唤醒等…
- 永久阻塞:如果持有锁的线程被永久阻塞,那么等待该线程释放锁的几个线程将永远得不到执行,永久处于阻塞状态。
- 优先级反转:优先级低的线程一旦获取锁,如果长时间不释放(释放的比较慢),优先级高的线程依然需要等待。
非互斥同步锁(乐观锁)
乐观锁的典型例子就是原子类、并发容器等。
- 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作的对象。
- 在更新的时候,对比在我修改的期间数据有没有其他人改变过:如果没有被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据。
- 乐观锁的实现一般都是利用 CAS 算法来实现的。
import java.util.concurrent.atomic.AtomicInteger;
/**
* 描述: 比较乐观锁、悲观锁
*/
public class PessimismOptimismLock {
int a;
public static void main(String[] args) {
//乐观锁的实现 (看不出加锁、但是底层容器使用CAS 在原子类中保证了线程安全)
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
}
/**
* 悲观锁的实现:通过加锁实现线程安全
*/
public synchronized void testMethod() {
a++;
}
}
开销对比
- 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越长,也不会对互斥锁的开销造成影响。
- 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多。
两种锁各自的使用场景:各有千秋
- 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况。
* 临界区有IO操作
* 临界区代码复杂或循环量大
* 临界区竞争非常激烈
- 乐观锁:适合并发写入少,大部分是读取的场景下,不加锁能让读取性能大幅度提高。
* 临界区竞争不激烈
可重入锁、非可重入锁
可重入锁(递归锁)
当一个线程对某临界区进行操作时,发现本线程已持有该锁(执行上层函数时获取的锁),那么无序等待锁的释放,可以直接使用已获取的锁。同一个线程,可以多次获取同一把锁。 例如:ReentrantLock、synchronized
等…
# 可重入锁的优点
* 避免死锁(避免在同一个线程中,多层函数使用同一个锁造成的死锁问题)
* 提高了封装性(避免多层函数中频繁的加锁、解锁)
示例:演示可重入性,打印重入次数。lock.getHoldCount() 可以获取当前线程对锁的重入次数。
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 演示 可重入 次数
*/
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();// HoldCount++
try {
System.out.println("已经对资源进行了处理");
if (lock.getHoldCount()<5) {
System.out.println(lock.getHoldCount());
accessResource();//recursion
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
已经对资源进行了处理
1
已经对资源进行了处理
2
已经对资源进行了处理
3
已经对资源进行了处理
4
已经对资源进行了处理
ReentranLock 的其他方法介绍(可重入相关的方法)
以下两个方法一般是开发和调试时候使用,上线后用到的不多。
- isHeldByCurrentThread() 可以看出锁是否被当前线程持有。
- getQueueLength() 可以返回当前正在等待这把锁的队列有多长,
公平锁和非公平锁
公平锁
按照线程请求顺序来分配锁。
- ReentrantLock 默认是公平锁,创建对象的默认初始化参数为
true
非公平锁
不完全按照请求的呼死你徐,在一定情况下,可以插队。
非公平也不提倡插入,这里的非公平意味“在合适的时机”插入,而不是盲目的插入。
默认策略是非公平。
非公平锁可以带来一种双赢的局面,充分利用了线程唤醒时间锁带来的空档期问题。提高 CPU 利用率,提高并发任务的吞吐量。
# 非公平锁的优点
* 避免唤醒带来的空档期(唤醒挂起线程需要时间,如果是公平锁,这段时间谁都拿不到锁,谁都无法处理。)
A持有->
B挂起->
A释放->
B(需要唤醒)、C加入排队(非挂起状态)->
C 很有可能在B唤醒之前已经使用完了锁,并且释放锁
演示:公平锁、非公平锁的示例
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 演示公平和不公平两种情况
*/
public class FairLock {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}
class PrintQueue {
private Lock queueLock = new ReentrantLock(true);//公平锁
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
控制台输出
//公平锁结果
Thread-0开始打印
Thread-0正在打印,需要9
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-1正在打印,需要6
Thread-2正在打印,需要10
Thread-3正在打印,需要1
Thread-4正在打印,需要5
Thread-5正在打印,需要3
Thread-6正在打印,需要5
Thread-7正在打印,需要10
Thread-8正在打印,需要6
Thread-9正在打印,需要6
Thread-0正在打印,需要5秒
Thread-0打印完毕
Thread-1正在打印,需要7秒
Thread-1打印完毕
Thread-2正在打印,需要8秒
Thread-2打印完毕
Thread-3正在打印,需要6秒
Thread-3打印完毕
Thread-4正在打印,需要3秒
Thread-4打印完毕
Thread-5正在打印,需要10秒
Thread-5打印完毕
Thread-6正在打印,需要2秒
Thread-6打印完毕
Thread-7正在打印,需要1秒
Thread-7打印完毕
Thread-8正在打印,需要8秒
Thread-8打印完毕
Thread-9正在打印,需要6秒
Thread-9打印完毕
//非公平锁结果
Thread-0开始打印
Thread-0正在打印,需要1
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-0正在打印,需要6秒
Thread-0打印完毕
Thread-1正在打印,需要9
Thread-1正在打印,需要5秒
Thread-1打印完毕
Thread-2正在打印,需要9
Thread-2正在打印,需要3秒
Thread-2打印完毕
Thread-3正在打印,需要9
Thread-3正在打印,需要3秒
Thread-3打印完毕
Thread-4正在打印,需要6
Thread-4正在打印,需要1秒
Thread-4打印完毕
Thread-5正在打印,需要1
Thread-5正在打印,需要5秒
Thread-5打印完毕
Thread-6正在打印,需要8
Thread-6正在打印,需要10秒
Thread-6打印完毕
Thread-7正在打印,需要5
Thread-7正在打印,需要5秒
Thread-7打印完毕
Thread-8正在打印,需要7
Thread-8正在打印,需要3秒
Thread-8打印完毕
Thread-9正在打印,需要2
Thread-9正在打印,需要2秒
Thread-9打印完毕
特例 tryLock()
针对 tryLock()方法,它不遵守设定的公平的规则。
当有线程执行 tryLock() 的时候,一旦有线程释放了锁,那么正在 tryLock() 的线程就能优先获取到锁,即使在它之前已经有其线程在等待队列中。
总结
advantage | defect | |
---|---|---|
公平锁 | 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 | 唤醒带来的空档期,空档期之间任何线程都无法获取锁,造成 CPU 浪费,并发吞吐量相对小 |
不公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿 |
共享锁、排它锁
排它锁
排它锁,又称独占锁、独享锁。
线程获取排它锁之后,既能读、又能写。与此同时,其他线程无法获取该锁,保证了线程安全。
例如:synchronized
ReentrantReadWriteLock 实现了 ReadWriteLock 接口,最主要的有两个方法: readLock()、writeLock() 用来获取读锁和写锁。
相比于 ReentrantLock
适用于一般场景,ReentrantReadWriteLock
适用于读多写少的情况,合理使用可以进一步提高并发效率。
共享锁
共享锁、又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也获取到共享锁,也可以查看,但无修改和删除数据。
例如 ReentrantReadWriteLock
,其中读锁是共享锁,写锁是排他锁。
读写锁的作用
在没有读写锁之前,假设使用 ReentrantLock,虽然保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题。
在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果在没有写锁持有的情况下,读是无阻塞的,提高了程序的执行效率。
读写锁的规则
换一种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定:读锁定、写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定。多读单写。
- 多个线程只申请读锁,都可以同时申请到
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
- 如果一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
示例:读写锁的用法
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 描述: TODO
*/
public class CinemaReadWrite {
//读写锁
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(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
}
Thread1得到了读锁,正在读取
Thread2得到了读锁,正在读取
Thread2释放读锁
Thread1释放读锁
Thread3得到了写锁,正在写入
Thread3释放写锁
Thread4得到了写锁,正在写入
Thread4释放写锁
读锁和写锁的交互方式
- 选择规则:从等待队列中选择哪一个线程执行。
- 读线程插队
- 升降级(写锁级别更高)
ReentrantReadWriteLock 的实现
- 插队:不允许读锁随意插队。避免写锁饥饿策略 (ReentrantReadWriteLock 的选择)
- 升降级:允许降级,不允许升级。
读写锁插队策略
- 公平锁:不允许插队
非公平锁:
* 写锁可以随时插队(写锁本身插队就能抢到临界区锁就已经很不容易,写锁本身是排他锁,如果当前临界区的线程持有读锁,那么尝试获得写锁的线程就会插队失败,所以无论如何都不应该限制获取写锁插队的能力。)
* **读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队。也就是头结点如果是写锁,则读锁插队失败。**(因为读锁本身的插队能力就比写锁强,读锁可以多个线程都是持有,如果再不加以限制,那么会造成一直插队,想要获得写锁的线程可能会发生饥饿。)
# 读锁插队策略
* 公平锁:不允许插队。
* 非公平:假设线程 2 和线程 4 正在同时读取,线程 3 想要写入,拿不到锁,于是进入等待队列,线程 5 不在队列里,现在过来想要读取。则有以下两种策略:
# 策略一:读锁插队策略
优点:线程5 插队成功。读不会造成线程不安全,提高并发吞吐量。
缺点:想要得到写锁的线程容易造成饥饿。如果线程6、线程7、线程8 都是读,那么都可以插队。线程3将永远不会得到执行。
# 策略二:避免写锁饥饿策略 (ReentrantReadWriteLock 的选择)
优点:线程5 不能插队,需要继续排队,线程2、线程4执行完毕,线程3执行写锁,线程5需要等待。不会造成写锁饥饿。
缺点:吞吐量相对低,无法最大程度上发挥读锁的优势。
实际:读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队。也就是头结点如果是写锁,则读锁插队失败。
演示非公平和公平的 ReentrantReadWriteLock 的策略
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 描述: 演示非公平和公平的ReentrantReadWriteLock的策略
*/
public class NonfairBargeDemo {
//非公平锁 false 公平锁 true
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
//因为线程4试图获取写,所以线程5不能插队到 线程2、3一起读。
new Thread(()->read(),"Thread5").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread thread[] = new Thread[1000];
for (int i = 0; i < 1000; i++) {
thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
thread[i].start();
}
}
}).start();
}
}
锁的升降级
为什么需要升降级
任务 = 写入 + 读取,后续的读取操作没有必要占用独享锁。
如果直接释放锁,那么下次就不知道什么时候才能轮到,所以可以通过持有的写锁拿到读锁,再释放写锁。
* 支持锁的降级:写锁 -> 拿到读锁 -> 释放写锁 -> 所有人都可以一起读
* 不支持锁的升级
代码演示:支持锁的降级,不支持升级
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 描述: 演示ReentrantReadWriteLock可以降级,不能升级
*/
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
/**
* 升级
*/
private static void readUpgrading() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000);
System.out.println("升级会带来阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了写锁,升级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
/**
* 降级
*/
private static void writeDowngrading() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("先演示降级是可以的");
Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
thread1.start();
thread1.join();
System.out.println("------------------");
System.out.println("演示升级是不行的");
Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
thread2.start();
}
}
先演示降级是可以的
Thread1得到了写锁,正在写入
在不释放写锁的情况下,直接获取读锁,成功降级
Thread1释放写锁
------------------
演示升级是不行的
Thread2得到了读锁,正在读取
升级会带来阻塞
为什么不能升级?
死锁。A 和 B 线程都想从 R 锁升级为 W 锁,陷入相互等待对方释放 R 锁,形成死锁。
自旋锁、阻塞锁
自旋锁
阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种转换需要耗费处理器时间。
如果同步代码块中的内容简单(执行速度快,不值得等待获取锁的线程去:先阻塞,再唤醒)。
这种线程状态切换的性能开销有可能会远大于同步代码块中任务的开销。
为了应对这种同步资源锁定时间很短的场景,为了避免这种线程状态切换的得不偿失,自旋锁诞生了。
如果处理器有多核 CPU,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。
为了让当前线程直接进入阻塞,需要让线程进行自旋,如果在自旋转完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而直接获取同步资源,从而避免线程状态切换的性能开销,这就是自旋锁的初衷。
缺点如下:
* 如果同步代码块执行时间很长,那么子旋锁只会白白占用CPU资源
自选锁的适用场景
自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高。
另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不适合的。
阻塞锁
就是不自旋的情况下,获取同步代码块失败之后直接进入阻塞状态。
示例:自定义自旋锁
import java.util.concurrent.atomic.AtomicReference;
/**
* 描述: 自旋锁
*/
public class SpinLock {
//原子引用类(具备CAS能力)
private AtomicReference<Thread> sign = new AtomicReference<>();
/**
* 加锁
*/
public void lock() {
Thread current = Thread.currentThread();
//期待没有人持有锁(null)、让当前线程持有锁(current)
//直到原子引用被赋值为当前的线程之后,才会停止。
while (!sign.compareAndSet(null, current)) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "自旋获取失败,再次尝试");
}
}
/**
* 解锁
*/
public void unlock() {
Thread current = Thread.currentThread();
//期待持有锁的人是current线程,然后清除锁
sign.compareAndSet(current, null);
}
public static void main(String[] args) throws InterruptedException {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
Thread.sleep(50);
thread2.start();
}
}
Thread-0开始尝试获取自旋锁
Thread-0获取到了自旋锁
Thread-1开始尝试获取自旋锁
Thread-1自旋获取失败,再次尝试
...
Thread-1 自旋获取失败,再次尝试
Thread-0释放了自旋锁
Thread-1自旋获取失败,再次尝试
Thread-1获取到了自旋锁
Thread-1释放了自旋锁
可中断锁
如果A在操作临界区,B等待,B等待时间太久,不想让B等待了,想让B处理其他事情,我们可以中断它,这种就是可中断锁。
在Java中,synchronized
就不是可中断锁,而 Lock
是可中断锁,因为 tryLock(time)
和 lockInterrupt
都能响应中断。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 在获取锁的过程中,可以在中途被打断。
*/
public class LockUnterrtuptibly implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockUnterrtuptibly instance = new LockUnterrtuptibly();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 尝试获取锁");
try {
//尝试获取锁
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取到了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("睡眠期间被中断");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("等锁期间被中断");
}
}
}
Thread-0 尝试获取锁
Thread-0 获取到了锁
Thread-1 尝试获取锁
java.lang.InterruptedException
等锁期间被中断
Thread-0 释放了锁
锁优化
- JVM对锁的优化
* 自旋锁、自适应锁(自旋锁尝试固定的次数,还是失败,就转为阻塞锁。)
* 锁消除(有些代码不需要加锁,根本不可能存在线程安全隐患的情况下)
* 锁粗化(消除锁过于细化带来的性能开销,讲多个细锁合并成一个粗锁)
- 并发程序编码优化原则
* 缩小同步代码块(只锁住需要锁的部分,把其他不相关的代码放在同步代码块外面)
* 尽量不要锁住方法
* 减少请求锁的次数
* 避免人为制造“热点”:ConcurrentHashMap.size() -> 改为单独维护计数器
* 锁中尽量不要再包含锁
* 选择合适锁类型或合适的工具类