ReentrantLock 实现的前提就是 AbstractQueuedSynchronizer,简称 AQS,是
java.util.concurrent 的核心,CountDownLatch、FutureTask、Semaphore、
ReentrantLock 等都有一个内部类是这个抽象类的子类。由于 AQS 是基于 FIFO
队列的实现,因此必然存在一个个节点,Node 就是一个节点,Node 有两种模
式:共享模式和独占模式。ReentrantLock 是基于 AQS 的,AQS 是 Java 并发包
中众多同步组件的构建基础,它通过一个 int 类型的状态变量 state 和一个 FIFO
队列来完成共享资源的获取,线程的排队等待等。AQS 是个底层框架,采用模
板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤
醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使
用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共
享变量 state 的一些简单的获取释放的操作)。
使用Retrantlock多线程操作变量简单示例:
public class ReentrantLockUse extends Thread {
public static int sum = 0;
public static ReentrantLock lock = new ReentrantLock();
public ReentrantLockUse(@NonNull String name) {
super.setName(name);
}
@Override
public void run() {
for (int i=0;i<1000;i++) {
lock.lock();
try {
Log.i(TAG, getName() + ": " + sum);
sum++;
} finally {
lock.unlock();
}
}
}
}
开启三个线程操作sum:
ReentrantLockUse("thread1").start()
ReentrantLockUse("thread2").start()
ReentrantLockUse("thread3").start()
最后的结果会是 2000;如果去掉锁,那么输出结果是一个小于2000的不确定的数,这个应该知道因为可能多个线程同步执行某个步骤。
Reentrantlock实现单例
实现线程同步锁synchronized 功能:
public class Singleton {
private static Singleton instance;
private static Lock lock = new ReentrantLock();
public static Singleton getInstance() {
lock.lock();
try {
if (instance == null) {
instance = new Singleton();
}
} finally {
lock.unlock();
}
return instance;
}
}
ReentrantLock与synchronized对比的优点:
1.可重入性
ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
2.锁的实现方式
synchronized加锁解锁的过程是隐式的,用户不用手动操作,系统去调度实现,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
3.公平性
ReentrantLock提供了公平锁和非公平锁两种API,开发人员完全可以根据应用场景选择锁的公平性;
synchronized是作为Java关键字是依赖于JVM实现,Java团队应该是优先考虑性能问题,因此synchronized是非公平锁。
即相比于synchronized,ReentrantLock在功能上更加丰富,它具有可重入、可中断、可限时、公平锁等特点。
可重入性:
public static void relock() {
ReentrantLock lock = new ReentrantLock();
for (int i=0;i<3;i++) {
lock.lock();
}
for (int i=0;i<3;i++) {
lock.unlock();
}
}
上面的代码通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出。由于ReentrantLock是重入锁,所以可以反复得到相同的一把锁,它有一个与锁相关的获取计数器state,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放(重入锁)。
可中断
与synchronized不同的是,ReentrantLock对中断是有响应的.synchronized一旦尝试获取锁就会一直等待直到获取到锁。使用ReentrantLock可通过中断来避免死锁。
可超时放弃
定时操作的例如:错误日志、定时过期缓存清理的操作,遇到优先级更高的操作占用资源时,暂时放弃本次操作下次再处理,可以起到让出CPU,提升用户体验;且超时不能获得锁,就返回false,不会永久等待构成死锁;
使用lock.tryLock(long timeout, TimeUnit unit)来实现可限时锁,参数为时间和单位。
public static void tryLock() {
ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(10000, TimeUnit.SECONDS)) {
//TO DO
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
可实现公平锁
公平锁: 是指多个线程竞争同一资源时[等待同一个锁时],获取资源的顺序是按照申请锁的先后顺序的;公平锁保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁,有点像早年买火车票一样排队早的人先买到火车票;
基本特点: 线程执行会严格按照顺序执行,等待锁的线程不会饿死,但 整体效率相对比较低;
非公平锁: 是指多个线程竞争同一资源时,获取资源的顺序是不确定的,一般是抢占式的;非公平锁相对公平锁是增加了获取资源的不确定性,但是整体效率得以提升;
基本特点: 整体效率高,线程等待时间片具有不确定性;
ReentrantLock继承于Lock类,在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁。
ReentrantLock lock = new ReentrantLock(true); //公平锁
ReentrantLock lock = new ReentrantLock(); //非公平锁
参考
Java中ReentrantLock的使用
ReentrantLock(重入锁)功能详解和应用演示