文章目录
题记:了解ReentrantLock必须提及的AQS和Lock
ReentrantLock,谈到它又不能的不说AQS,AQS的全称是AbstractQueuedSynchronizer
,这个类也是在java.util.concurrent.locks下面,提供了一个FIFO的队列,可以用于构建锁的基础框架,内部通过原子变量state来表示锁的状态,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁,ReentrantLock是一个重入锁,表现在state上,如果持有锁的线程重复获取锁时,它会将state状态进行递增,也就是获得一个信号量,当释放锁时,同时也是释放了信号量,信号量跟随减少,如果上一个线程还没有完成任务,则会进行入队等待操作。
ReentrantLock类实现了Lock接口和Serializable接口。
java.util.concurrent.locks包下Lock接口的方法介绍:
一、ReentrantLock可重入(lock)
/***
* ReentrantLock可重入
*/
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock1{
static ReentrantLock lock = new ReentrantLock();
//方法lock1
public static void lock1() {
try {
lock.lock();
log.dubug("---------lock1执行---------");
lock2();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//方法lock2
public static void lock2() {
try {
lock.lock();
log.dubug("---------lock2执行---------");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//测试
public static void main (string[] args){
lock1();
}
}
控制台打印结果:
---------lock1执行---------
---------lock2执行---------
1)
重入锁
实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程(此时的lock1)如果再次请求这个锁(lock2),就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
2)synchronized也是可重入锁
二、ReentrantLock可中断(lockInterruptibly)
/***
* ReentrantLock可中断
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock2{
static ReentrantLock lock = new ReentrantLock();
public static void main (string[] args)throws InterruptedException {
//主线程main执行,创建线程t2
new Thread (()->
try {
lock.lock();
log.dubug("---------t2线程获取锁,然后去睡5秒---------");
TimeUnit.SECONDS.sleep(timeout:5);
log.dubug("---------t2线程睡5秒后醒来了---------");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},name:"t2").start();
//主线程main睡2秒,然后创建线程t1
TimeUnit.SECONDS.sleep(timeout:2); //此处主线程睡眠主要为了确保t2先拿到锁,实现效果
//创建线程t1
Thread t1=new Thread (()->
try {
lock.lockInterruptibly();//(此处t1尝试加锁失败,睡眠,因为等待上面的t2线程释放锁)
log.dubug("---------t1获取到锁---------");
} catch (InterruptedException e) {
log.dubug("---------t1被主线程打断了,没有获取到锁---------");
e.printStackTrace();
return;
} finally {
lock.unlock();
}
},name:"t1");
t1.start();
//主线程睡完2秒后醒来,打断t1
log.dubug("---------主线程睡完2秒后醒来,打断t1---------");
t1.interrupt();//打断t1,所以t1不再等待t2释放锁了
}
}
控制台打印结果:
---------t2线程获取锁,然后去睡5秒---------
---------主线程睡完2秒后醒来,打断t1---------
---------t1被主线程打断了,没有获取到锁---------
java. lang. InterruptedException <3 internal ca1ls>
at com. TestReentrantLock2.lambda$main$1(TestReentrantLock2. java:31)
at java. lang. Thread. run(Thread. jiava:748)
Exception in thread "t1" java. lang. IllegalMonitorStateException <3 internal ca1ls>
at com. TestReentrantLock2.lambda$main$1(TestReentrantLock2. java:38)
at java.lang. Thread.run(Thread. iava:748)
---------t2线程睡5秒后醒来了---------
小结:1)java.lang.InterruptedException被中止异常
当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。
2)lockInterruptibly
通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
三、ReentrantLock超时(tryLock)
/***
* ReentrantLock超时
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock3{
static ReentrantLock lock = new ReentrantLock();
public static void main (string[] args)throws InterruptedException {
//创建线程t1
Thread t1=new Thread (()->
try {
log.dubug("---------t1启动--------");
if(!lock.tryLock(timeout:2,TimeUnit.SECONDS){//tryLock尝试获取锁,此处可以带参数,设置超时时间,等待2秒,拿不到就返回,若设置成4秒,就可以拿到锁了。不带参数的话只尝试一次,拿不到就返回了。
log.dubug("---------t1获取不到锁,返回---------");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
log.dubug("---------t1获取到锁了---------");
}finally {
lock.unlock();
}
},name:"t1");
//主线程先获取锁
lock.lock();
log.dubug("---------主线程获取锁---------");
//启动t1
t1.start();
//然后主线程睡了3秒
try {
log.dubug("---------主线程开始睡眠3秒--------");
TimeUnit.SECONDS.sleep(timeout:3);
}finally {
lock.unlock();
}
}
}
控制台打印结果:
---------主线程获取锁---------
---------主线程开始睡眠3秒--------
---------t1启动--------
---------t1获取不到锁,返回---------
四、ReentrantLock锁绑定多个条件(newCondition、await、signal)
ReentrantLock锁可以绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
/***
* ReentrantLock锁可以绑定多个条件
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class TestReentrantLock4{
static ReentrantLock lock = new ReentrantLock();
//创建两个回家条件:需要钱、需要票
static Condition moneyCondition=lock.newCondition();
static Condition ticketCondition=lock.newCondition();
Boolean havaMoney=false;//是否有钱
Boolean haveTicket=false;//是否有票
public static void main (string[] args)throws InterruptedException {
//线程farmer1
new Thread (()->
log.dubug("--------第一个农民想回家--------");
while(!havaMoney){
try {
lock.lock();
log.dubug("---------第一个农民没有钱,回不去家了,等钱---------");
moneyCondition.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
},name:"farmer1").start();
//线程farmer2
new Thread (()->
log.dubug("--------第二个农民想回家--------");
while(!haveTicket){
try {
lock.lock();
log.dubug("---------第二个农民没有票,回不去家了,等票---------");
ticketCondition.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
},name:"farmer2").start();
//主线程12306睡一会
Thread.sleep(millis:1000);
try{
lock.lock();
havaMoney=true;
log.dubug("---------第一个农民有钱了,唤醒他可以回家了---------");
moneyCondition.signal();//唤醒等钱的farm1线程执行回家。
haveTicket=true;
log.dubug("---------第二个农民有票了,唤醒他可以回家了---------");
ticketCondition.signal();//唤醒等票的farm2线程执行回家。
}catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},name:"12306").start();
}
}
精确条件精确唤醒,控制台打印结果:
--------第一个农民想回家--------
---------第一个农民没有钱,回不去家了,等钱---------
--------第二个农民想回家--------
---------第二个农民没有票,回不去家了,等票---------
---------第一个农民有钱了,唤醒他可以回家了---------
---------第二个农民有票了,唤醒他可以回家了---------
小结:1)ReentrantLock锁可以绑定多个条件,synchronized不可以;
2)Condition中,用await
()替换wait(),用signal
()替换notify(),用signalAll()替换notifyAll(),Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition
()方法。
五、ReentrantLock实现公平锁
公平锁
是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁
则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
ReentrantLock内部提供了两种AQS的实现,一种公平模式,一种是非公平模式,如果没有特别指定在构造器中,默认是非公平的模式
。但可以通过带布尔值的构造函数要求使用公平锁。如下所示:
//源码分析:无参的构造函数
public ReentrantLock() {
sync = new NonfairSync();
}
当调用有参构造函数时,指定使用哪种模式来进行操作,参数为布尔类型,如果指定为false的话代表非公平模式,如果指定为true的话代表的是公平模式,如下所示:
//源码分析:有参的构造函数
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁和非公平锁继承关系:
公平锁和非公平锁内部实现:
通过源码分析可知,如果创建的是无参的ReentrantLock的对象,内部实际调用的ock(),其实是调用了NonfairSync对象的lock方法。如果创建的是有参的ReentrantLock的对象,内部实际调用的lock(),其实是调用的是fairSync对象的lock方法。如下所示:
//源码:
public void lock() {
sync.lock();
}
/**
* 非公平模式锁源码
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 执行锁动作,先进行修改状态,如果锁被占用则进行请求申请锁,申请锁失败则将线程放到队列中
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 继承自AQS的tryAcquire方法,尝试获取锁操作,这个方法会被AQS的acquire调用
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
非公平源码分析:
1.调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程,申请失败则将线程放到队列中
2.调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
以上两步都属于插队现象,非公平,可以提高系统吞吐量。
/**
* 公平模式锁源码
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -300089789090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平源码分析:
1.调用lock()方法时,不进行CAS尝试
2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state,通过判断该线程是否是队列的头结点,从而保证公平性.
此处只是简单描述,深层次代码调用可以自行分析理解,此处不过多展开。
小结:synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。