一、ReentrantLock
ReentrantLock是Lock的一个实现类,它的底层基于AQS实现,是一种可重入互斥锁,用于替代synchronized,但功能比synchronized更强大。
1、ReentrantLock的使用:
private final ReentrantLock lock = new ReentrantLock();
public void m() {
try {
lock.lock();
// ... method body
} finally {
lock.unlock()
}
}
注:使用synchronized遇到异常时,会自动释放锁,但ReentrantLock必须手动释放锁。
2、ReentrantLock提供的额外功能:
① tryLock:
可以使用lock.tryLock()进行尝试锁定,无论锁定与否,方法都将继续执行,是否需要阻塞可以交由开发者自己决定,返回值为true时表示锁定成功,false表示锁定失败。
也可以通过调用tryLock(long timeout, TimeUnit unit),在指定时间内驱尝试获得锁
② lockInterruptibly:
可以使用lockInterruptibly进行一个“可以被打断等待的”加锁,也就是如果在线程A中调用了lock.lockInterruptibly()方法,但此时锁已经被其它线程拿到,A就会等待锁的释放并再次去争抢锁,而这时调用A的interrupt()方法,就会换醒A,让它去处理InterruptedException,打断A的等待。
注意:如果使用lock.lock方法获取锁,线程不会被中断。
③ 将ReentrantLock指定为公平锁:
ReentrantLock默认是非公平锁,但可以通过new ReentrantLock(true)将ReentrantLock设定为公平锁。
公平锁和非公平锁:公平锁是指新来的线程首先会去检查队列中是否有等待获取锁的线程,若有,它会排在最后。非公平锁是指新来的线程不会先去检查队列,而是会立即去尝试获取锁,如果没获取到锁再去队列中排队等待(队列中的线程通过顺序获取锁)
④ 支持Condition:
在ReentrantLock中使用Condition类似于在synchronized中wait和notify,但Condition能够做到比wait和notify更为精准,它可以精准的唤醒某一类线程。
public class MyContainer {
private final int MAX = 10; //最多10个元素
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put() {
try {
lock.lock();
while(count == MAX) {
producer.await(); // 若容器满了,生产者进入wait状态
}
count++;
System.out.println(Thread.currentThread().getName() + "生产了一个元素,剩余元素个数为:" + count);
consumer.signalAll(); // 将所有消费者从wait状态中唤醒,通知它们进行消费,如果调用consumer.signal则会随机通知一个consumer
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void remove() {
try {
lock.lock();
while(count == 0) {
consumer.await(); // 若容器空了,消费者进入wait状态
}
count--;
System.out.println(Thread.currentThread().getName() + "消费了一个元素,剩余元素个数为:" + count);
producer.signalAll(); // 将所有生产者唤醒,通知它们进行生产,如果调用producer.signal则会随机通知一个producer
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
MyContainer container = new MyContainer();
// 启动10个消费者线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) container.remove();
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动2个生产者线程
for(int i = 0; i < 2; i++) {
new Thread(()->{
for(int j = 0; j < 25; j++) container.put();
}, "p" + i).start();
}
}
}
二、ReentrantReadWriteLock
ReentrantReadWriteLock是ReadWriteLock的实现类,是一种读写锁。
读写锁的本质其实就是共享锁和排它锁。
共享锁:这把锁在某一时刻可以被多个线程同时拥有
排它锁:也称为独占锁,这把锁在某一时刻只能被一个线程拥有
读写锁中的读锁是一把共享锁,写锁是排它锁,且读锁与写锁互斥(某一个线程不能同时拥有锁对象的读锁和写锁)。读写锁通常用于对数据的读写操作,想要对数据进行读取时,可以让该线程去申请锁对象的读锁,让线程只能读取数据但不能修改数据。想要对数据进行修改时,需要线程去申请到锁对象的写锁。当某一个线程申请到读锁时,其它想要读取数据的线程可以继续申请并拥有读锁,这保证了数据可以同时被多个线程读取,由于读锁与写锁互斥,想要修改数据的线程在读锁完全释放之前,无法获得写锁,也就不能对数据进行修改。当某一个线程申请到写锁,在对数据进行修改时,其它线程无法获得写锁和读锁,保证线程在修改数据期间,其它线程不会对该数据进行修改或读取。
1、ReentrantReadWriteLock的使用:
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public void get() {
try {
r.lock();
// read data
} finally {
r.unlock();
}
}
public void modify() {
try {
w.lock();
// modify data
} finally {
w.unlock();
}
}
2、ReentrantReadWriteLock提供的额外功能:
① 读锁和写锁均为可重入锁:
当线程已经获取到读锁时,可继续获得读锁,增加读状态
当线程已经获取到写锁时,也继续获得写锁,增加写状态
② 将ReentrantReadWriteLock指定为公平锁:
ReentrantReadWriteLock默认是非公平锁,但可以通过new ReentrantReadWriteLock(true)将ReentrantReadWriteLock设定为公平锁。
③ 锁降级
当线程拥有写锁时,此时可以立马获取读锁,再释放写锁,完成从写锁到读锁的降级过程,其原理是写锁为排它锁,当线程持有写锁时,能够保证其它线程没有拥有读锁或写锁。作用是修改完后需要立马读取数据时,避免其它线程获取到写锁,对数据进行修改,读到错误数据。
注:
ReentrantReadWriteLock不支持锁升级,也就是线程不可以在拥有读锁时去再次获取到写锁,原因为当线程拥有读锁时,其它线程也可能拥有读锁。
读锁和写锁均支持tryLock、lockInterruptibly、Condition
三、StampedLock
StampedLock邮戳锁,在jdk1.8引入,是一把比ReentrantReadWriteLock性能更好的读写锁。它具有三种模式去控制读/写访问:写锁、读锁、乐观读锁。获取锁时返回的stamp类似于版本的概念。
1、StampedLock的使用:
public class StampedLockTest {
private StampedLock stampedLock = new StampedLock();
public void modify() {
long stamp = stampedLock.writeLock();
try {
// modify data
} finally {
stampedLock.unlockWrite(stamp);
}
}
public void get() {
long stamp = stampedLock.readLock();
try {
// read data
} finally {
stampedLock.unlockRead(stamp);
}
}
public void optGet() {
long stamp = stampedLock.tryOptimisticRead();
// read data
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock();
try {
// read data again
} finally {
stampedLock.unlockRead(stamp);
}
}
}
}
① 写锁:与ReentrantReadWriteLock中的写锁基本相同,为独享锁,解锁时需要传入获得锁时得到的stamp。
② 读锁:与ReentrantReadWriteLock中的读锁基本相同,为共享锁,解锁时需要传入获得锁时得到的stamp。
③ 乐观读锁:可通过tryOptimisticRead获取乐观读锁,乐观读锁不会阻塞写锁的获取,当线程持有乐观读锁时,其它线程也可获取写锁去修改数据,在读多写少的情况下可以使用乐观读锁,避免需要修改的线程由于一直拿不到写锁,进入饥饿状态。
如果线程持有乐观读锁读取数据后,可以使用validate方法查看在读取数据期间是否有其它线程获取过写锁,若返回false,则表示验证不通过,其它线程已经获取过写锁,对数据进行过修改。
2、StampedLock提供的额外功能:
① 可通过readLockInterruptibly获取“可中断读锁”。
② 可通过writeLockInterruptibly获取“可中断写锁”。
③ 读锁可以通过tryConvertToWriteLock升级为写锁。
④ 写锁可以通过tryConvertToReadLock降级为读锁。
⑤ 读锁和写锁可通过tryConvertToOptimisticRead降级为乐观读锁。
⑤ 可通过tryReadLock尝试获取读锁,若返回值为0,则获取失败。
⑥ 可通过tryWriteLock尝试获取写锁,若返回值为0,则获取失败。
3、注:StampedLock不支持可重入