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,这个等后面讲生产者和消费者的时候在细说。