1. 锁概述
1.1 并发中的锁
在计算机科学中,锁(Lock)是一种同步原语:一种在有许多执行线程时强制限制对资源的访问的机制。锁旨在强制执行互斥并发控制策略,但是在控制的细节上会有很多差异,这种差异称为锁的特性。
根据锁的特性,可以将锁分为很多不同的类型,例如:公平锁/非公平锁、悲观锁/乐观锁、可重入锁/不可重入锁、共享锁/排他锁等,如下图所示。
这里需要注意,由于一个锁可能同时具备多个特性,可能被同时划入不同的类型。
1.2 从synchronized看锁的分类
可以从不同的维度来看synchronized锁的特性:
- 多个线程在竞争synchronized的锁时,不会按照先来后到的顺序获取锁,因此synchronized属于非公平锁
- synchronized会锁住同步资源,因此synchronized属于悲观锁
- 一个线程在程序中可以重复的获取synchronized的锁,例如前面一些案例中使用的嵌套的synchronized代码块,因此synchronized属于可重入锁
- 多个线程不能同时持有synchronized的锁,因此synchronized属于排他锁
2. Lock接口及实现
2.1 Lock接口
Lock接口是Java 5版本在并发工具包中新增的内容,主要用来实现锁功能,其实现类提供了各种类型的锁,应用场景非常丰富。
Lock接口提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字不具备的同步特性。
Lock的使用方式非常简单:
Lock lock = new XXXLock();
lock.lock(); // 获取锁
lock.unlock(); // 释放锁
Lock接口提供的并且synchronized关键字所不具备的主要特性如下:
- 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
- 能被中断地获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
- 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回
Lock接口的常用方法如下:
- lock():获取锁,调用该方法的当前线程会尝试获取锁,获得锁后,从该方法返回
- lockInterruptibly():可中断地获取锁,该方法会响应中断,即在锁的获取中可以中断当前线程
- tryLock():尝试非阻塞的获取锁,调用该方法后立刻返回,如果能否获取则返回true,否则返回false
- tryLock(long time, TimeUnit unit):超时的获取锁,超时时间结束,返回false
- unlock():释放锁
public class LockDemo1 {
public static void main(String[] args) {
MyRun2 run2 = new MyRun2();
ExecutorService service = Executors.newFixedThreadPool(4);
for(int i=1; i<=4; i++){
service.execute(run2);
}
service.shutdown();
}
}
class MyRun2 implements Runnable {
int i = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
String name = Thread.currentThread().getName();
try {
if (i > 50) {
break;
}
System.out.println(name + " " + i);
i++;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
2.2 ReentrantLock
ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,如下图所示:
同时,该锁还支持获取锁时的公平和非公平性选择:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo1 {
public static void main(String[] args) throws InterruptedException {
boolean fairFlag = true; // 是否使用公平锁
// 基于fairFlag创建公平锁或非公平锁
Lock lock = new ReentrantLock(fairFlag);
MyRun3 run3 =new MyRun3(lock, fairFlag? "fair": "nonFair");
ExecutorService service = Executors.newFixedThreadPool(8);
for(int i=1;i<=16;i++){
service.execute(run3);
TimeUnit.MILLISECONDS.sleep(10);
}
service.shutdown();
}
}
class MyRun3 implements Runnable {
Lock lock;
String label;
public MyRun3(Lock lock, String label){
this.lock = lock;
this.label = label;
}
@Override
public void run() {
lock.lock();
String name = Thread.currentThread().getName();
System.out.println(label + ": "
+ "name: " + name + "获取到锁");
try {
TimeUnit.MILLISECONDS.sleep(100); // 休眠1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.unlock();
}
}
2.3 一写多读场景
一写多读,顾名思义,是指对数据的并发读取频率较高,而并发写入频率较低的场景,是现实中非常普遍的一种并发场景。
例如,某电影网站的电影实时票房量:
在这个案例中,实时票房量是共享资源,被多个线程并发的访问,其中包含读线程,也包含写线程。为了防止出现线程安全问题,需要对共享资源进行加锁。
2.4 读写锁
但是目前讲到的锁都是排他锁:即同一时间仅能有一个线程获取锁。在一写多读场景中,读线程之前也会阻塞,同一时间仅能有一个线程读取票房数据,这大大降低了读的效率:
在一些多读场景中,读线程和写线程应该互斥,写线程和写线程之间应该互斥,但是读线程和读线程之间可以不互斥。行业中通常使用“读写锁“的方案来实现上述需求。
读写锁维护了一对锁:一个读锁和一个写锁。通过分离读锁和写锁,使得并发性相比一般的排他锁有很大的提升。
2.5 ReadWriteLock
Java中使用ReadWriteLock接口作为读写锁的父接口,该接口定义了两个方法:
- Lock readLock():返回该读写锁对中的读锁对象
- Lock writeLock():返回该读写锁对中的写锁对象
ReentrantReadWriteLock作为ReadWriteLock接口的实现类,除了实现了上述的2个方法外,还提供了一些便于外界监控器内部工作状态的方法:
- getReadLockCount():返回当前读锁被获取的次数,该次数不等于获取读锁的线程数
- getReadHoldCount():返回当前线程获取读锁的次数
- isWriteLocked():判断写锁是否被获取
- getWriteHoldCount():返回当前写锁被获取的次数
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
StudentInfoManagementSystem system = new StudentInfoManagementSystem();
Runnable readTask = () -> {
for(int i = 0; i <5 ;i++){
system.getStudents();
}
};
Runnable writeTask = () -> {
system.addStudent("Tom");
};
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 创建多个读任务读取学生信息
for (int i = 0; i < 10; i++) {
if (i % 4 ==0){
executor.execute(writeTask);
} else {
executor.execute(readTask);
}
}
// 关闭线程池
executor.shutdown();
}
}
class StudentInfoManagementSystem {
private volatile List<String> studentList = new ArrayList<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void addStudent(String studentName) {
lock.writeLock().lock();
try {
// 写入学生信息
studentList.add(studentName);
System.out.println("Added student: " + studentName);
} finally {
lock.writeLock().unlock();
}
}
public void getStudents() {
lock.readLock().lock();
try {
System.out.println("Read students: " + studentList);
} finally {
lock.readLock().unlock();
}
}
}
3. 总结
1. 在计算机科学中,锁(Lock)是一种同步原语:一种在有许多执行线程时强制限制对资源的访问的机制
- 公平锁/非公平锁
- 悲观锁/乐观锁
- 可重入锁/不可重入锁
- 共享锁/排他锁
2. Lock 接口是Java 5版本在并发工具包中新增的内容,主要用来实现锁功能,其实现类提供了各种类型的锁,应用场景非常丰富
- ReentrantLock
- ReadWriteLock