1、前言
java.util.concurrent.locks为锁定和等待条件提供一个框架的接口和类,说白了就是锁所在的包。
2、什么是Lock
Lock是一种锁机制,比同步块(synchronized block)更加灵活,同时也更加复杂的线程同步机制。在JDK1.5就已存在Lock接口了。
其中有三个实现类:
- ReentrantLock:可重入锁
- ReentrantReadWriteLock.ReadLock:读锁
- ReentrantReadWriteLock.WriteLock:写锁
3、Lock的API
Lock接口只提供了6个方法:
其中lock()和unlock()是配对的。lock()是加锁,而unlock()是解锁。lockInterruptibly()与lock()类似,区别在于lock()如果无法获取到锁,线程一直被阻塞,直到锁释放。而lockInterruptibly()允许线程被中断,并抛出java.lang.InterruptedException。
tryLock()只有在调用时才可以获得锁。如果可用,则获取锁定,并返回true,反之返回false。相应的还有重载方法tryLock(long time, TimeUnit unit)表示在给定的等待时间内空闲,则可以获取锁。如果到了超时时间,还没获取到就放弃获取。
4、ReentrantLock的基本使用
Lock是一个接口,官方文档其实也给了如何使用的说明:
- 声明Lock对象。Lock lock = new XXXLock();
- 方法执行加锁lock.lock();
- 在方法块执行完毕后,需要释放锁:lock.unlock();
由于LocK是一个接口,需要使用具体的实现。典型的实现如ReentrantLock。示例代码如下:
public class ReentrantLockDemo {
public static void main(String[] args) {
Phones phones = new Phones();
new Thread(() -> {
for(int i = 0; i< 50; i++) {
phones.sale();
}
}, "销售员小王").start();
new Thread(() -> {
for(int i = 0; i< 50; i++) {
phones.sale();
}
}, "销售员小红").start();
}
}
class Phones {
// 库存10部手机
private int total = 50;
// 1、 声明锁的实例
ReentrantLock lock = new ReentrantLock();
public void sale(){
try {
// 2、加锁,代码规约检测会提示你加载try的第一行
lock.lock();
if(total > 0){
System.out.println(Thread.currentThread().getName() + "卖出了一部手机,当前库存剩余:" + (--total));
}
} finally {
// 3、释放锁
lock.unlock();
}
}
}
执行结果:
ReentrantLock是个可重入锁,其中可以执行公平锁和非公平锁。如new ReentrantLock(true),默认是非公平锁(false)。
4.1、公平锁和非公平锁
- 公平锁:每个线程获取锁的顺序按照先后顺序获取。关键字眼:先到先得。
- 优点:能保证所有的线程都得到资源,不会产生线程饥饿现象。
- 缺点:吞吐量低,除了第一个线程以外,其余的都处于排队阻塞的状态,cpu需要每次唤醒线程,开销较大。
- 非公平锁:多个线程同时尝试获取,哪个线程优先获取到锁取决于系统分配策略。关键字眼:无需排队。
- 优点:吞吐量高,cpu无需唤醒所有的线程,开销低。
-
- 缺点:会产生线程饥饿现象,可能后到的线程先获取到锁,而前面的线程永远都获取不到
5、读写锁ReadWriteLock
ReadWriteLock是一个读写锁接口。什么是读写锁呢?看下官方文档说明:
ReadWriteLock分为一个读锁和一个写锁。读锁可以被多个线程持有,而写锁只能被一个线程持有。典型的实现类有ReentrantReadWriteLock。
读锁:就是我们常说的共享锁。
写锁:就是常说的独占锁。
示例代码:
public class ReadWriteLock {
public static void main(String[] args) {
MyMap map = new MyMap();
// 写操作
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> map.put(String.valueOf(finalI), String.valueOf(finalI))).start();
}
// 读操作
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> map.get(String.valueOf(finalI))).start();
}
}
}
// 模拟公共资源类
class MyMap extends HashMap<String, String> {
@Override
public String get(Object key) {
System.out.println(Thread.currentThread().getName() + "获取key:" + key);
return super.get(key);
}
@Override
public String put(String key, String value) {
System.out.println(Thread.currentThread().getName() + "写入key:" + key);
String put = super.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入key:" + key + "完成");
return put;
}
}
如果不加入任何的锁限制,我们直到结果肯定是很随机的。在写入操作时会被其他读线程插队。
加入读写锁后:
public class ReadWriteLock {
public static void main(String[] args) {
MyMap map = new MyMap();
// 写操作
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> map.put(String.valueOf(finalI), String.valueOf(finalI))).start();
}
// 读操作
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> map.get(String.valueOf(finalI))).start();
}
}
}
// 模拟公共资源类
class MyMap extends HashMap<String, String> {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
@Override
public String get(Object key) {
try{
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "获取key:" + key);
return super.get(key);
} finally {
rwLock.readLock().unlock();
}
}
@Override
public String put(String key, String value) {
try {
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "写入key:" + key);
String put = super.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入key:" + key + "完成");
return put;
} finally {
rwLock.writeLock().unlock();
}
}
}
我们可以看到在写入的时候,并不会有读的线程插队操作。
6、小结
关于Lock锁大概就讲这些,主要讲了ReentrantLock和ReadWriteLock的基本使用,也是通常比较常用的。其中Locks中还有一个接口Condition,这个等后面讲生产者和消费者的时候在细说。