重入锁是synchronized、Object.wait()和Object.notify()方法的替代品(或者说是增强版),在JDK5.0的早期版本,重入锁的性能远远好于synchronized,但从JDK6.0开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大。但ReentrantLock也有一些synchronized没法实现的特性。
重入锁,为啥要叫重入锁呢,很多文章是这么解释的:
同一个线程连续两次获得同一把锁,这是允许的,否则同一个线程将在第二次获得锁的时候会和自己死锁。道理我都懂,但是这个例子也太抽象了,看了也不知道用处啊,还是看下面这个例子吧:
public class ReentrantTest implements Runnable {
ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId()+" doSomething()");
System.out.println("进入第一个lock,当前的lock数:"+lock.getHoldCount());
doAnotherThing();
} finally {
lock.unlock();
System.out.println("第一个lock释放,当前的lock数:"+lock.getHoldCount());
}
}
private void doAnotherThing() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId()+" doAnotherThing()");
System.out.println("进入第二个lock,当前的lock数:"+lock.getHoldCount());
} finally {
lock.unlock();
System.out.println("第二个lock释放,当前的lock数:"+lock.getHoldCount());
}
}
@Override
public void run() {
doSomething();
}
public static void main(String[] args) {
ReentrantTest rt = new ReentrantTest();
ExecutorService es = Executors.newFixedThreadPool(10);
for(int i = 0;i<5;i++) {
es.submit(rt);
}
es.shutdown();
}
}
这样一看不就明白了重入的意义。
注意:同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数。如果释放多了,会抛出java.lang.IllegalMonitorStateException异常;反之,释放少了,那这个线程还占用这把锁,别的线程就没法进入临界区。
重入的实现源码:
final boolean nonfairTryAcquire(int acquires) { //非公平版本
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //资源已经被占用,且当前占用资源的线程再次请求资源,即重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
ReentrantLock的几个重要方法:
1、lock():获得锁,如果锁已经被占用,则等待。
2、lockInterruptibly():获得锁,但优先响应中断。
3、tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
4、tryLock(long time,TimeUnit unit):在给定时间内尝试获得锁。
5、unlock():释放锁。
在大多数情况下,锁的申请都是非公平的。比如,线程1首先请求了锁A,线程2再请求了锁A,当锁A释放了之后,是线程1拿到锁还是线程2拿到锁是不一定的。系统只会在这个锁的等待队列中随机挑选一个。重入锁允许我们对公平性进行设置。
public ReentrantLock() { //默认为非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) { //通过boolean类型参数true设置成公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
之前我们谈到AQS
同步类在实现时,一般将自定义同步器(Sync)定义为内部类。而同步类就依赖Sync实现一系列接口供外部使用。而ReentrantLock中对Sync进一步扩展,实现了NonfairSync(非公平锁)和FairSync(公平锁)。
非公平锁就不说了,平时也一直在用,下面看看公平锁的使用:
public class FairLockTest implements Runnable{
ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while(true){
try{
fairLock.lock();
System.out.println(Thread.currentThread().getId()+" 获得锁");
}finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairLockTest ft = new FairLockTest();
ExecutorService es = Executors.newFixedThreadPool(3);
for(int i = 0;i<3;i++) {
es.submit(ft);
}
}
}
运行结果:
可以看到挺公平的,你一下我一下的。。。
这两种都是由tryLock方法实现,两者都能避免死锁的发生。只不过轮询锁用的是tryLock(),而定时锁用的是tryLock(long timeout, TimeUnit unit)。
轮询锁:
tryLock:当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,直接返回true。如果被其他线程占用,则不会等待,直接返回false。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看到tryLock也就是调用了非公平版本的获取资源的方法,获取不到直接返回false。
再看下lock()方法(就看非公平版本的lock()吧):
final void lock() {
if (compareAndSetState(0, 1)) //直接抢占
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
可以看到lock()方法资源被占用的情况下调用的是acquire(1),之前在AQS那篇文章里说过,acquire(1)在AQS里是这样实现的(不清楚的可以看下这里: http://blog.csdn.net/qq_31957747/article/details/74910939):
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
也就是他会先尝试获取资源,获取不到,再加入等待队列,等待前驱唤醒自己。
而tryLock()则少了等待的过程,获取不到,不会等待,直接不干了。
while(true)配上tryLock(),就是一个轮询,顾名思义,一遍遍的循环尝试获取资源,失败继续循环,这就跟我们的CAS有点像了。
例子如下:
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock){
this.lock = lock;
}
@Override
public void run() {
if(this.lock == 1){
while (true) {
if (lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My Job Done");
return ;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
}else {
while (true) {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My Job Done");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
new Thread(r1).start();
new Thread(r2).start();
}
}
这段程序用两个线程一个占用lock1锁,一个占用lock2锁,然后又相互请求获取对方的锁,如果是用lock()的话,则就相互等待,产生死锁。而tryLock()这种轮询的方式,让他不停的尝试获取,最终两个线程都能成功退出,避免了死锁(可能轮询的时间有点长,不过最终两个线程是能正常执行结束的)。
运行结果:
定时锁:
tryLock(long timeout, TimeUnit unit)接收两个参数,第一个表示等待时间,第二个表示计时单位。当前线程会尝试获得锁,当前锁没有被线程占用,则获取成功返回true。被其他线程占用,则等待参数设置的时间,超过这个时间则不等待,立即返回false。
看看tryLock(long timeout, TimeUnit unit)的源码:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
再看tryAcquireNanos这个方法:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
long lastTime = System.nanoTime(); //获取当前系统的时间数值
final Node node = addWaiter(Node.EXCLUSIVE); //将当前线程节点标记为独占模式,放入队尾
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//前驱线程节点为head节点,且刚好释放资源
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
if (nanosTimeout <= 0) //剩余等待时间<=0,退出等待
return false;
if (shouldParkAfterFailedAcquire(p, node) && //满足等待条件,那就等待喽
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();//等待结束再次获取当前系统时间数值
nanosTimeout -= now - lastTime; //剩余等待时间减少
lastTime = now;
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
意思还是很明确。
来个例子:
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try{
System.out.println(Thread.currentThread().getName()+"正在运行。并尝试在5秒内获得锁");
if(lock.tryLock(5, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"获取锁,并睡眠6秒");
Thread.sleep(6000);
}else{
System.out.println(Thread.currentThread().getName()+"等待超过5秒。获得锁失败");
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock tl = new TimeLock();
new Thread(tl).start();
new Thread(tl).start();
}
}
一个线程占用了锁,然后sleep6秒,sleep是不会释放锁的,所以另一个线程等待5秒后拿不到锁,在放弃等待了,方法返回false。
运行结果:
lockInterruptibly()方法在获得锁的同时保持对中断的响应。看下源码:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
继续跟踪acquireInterruptibly()方法。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
再看doAcquireInterruptibly()方法:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //跟之前AQS那个非中断的系列的方法相比,不同就是这里throw了中断异常。
}
} finally {
if (failed)
cancelAcquire(node);
}
}
差不多就可以看出为什么会响应中断了,下面看个例子:
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,方便构造死锁
* @param lock
*/
public IntLock(int lock){
this.lock = lock;
}
@Override
public void run() {
try{
if(lock == 1) {
lock1.lockInterruptibly();
Thread.sleep(500);
lock2.lockInterruptibly();
}else{
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
}
System.out.println(Thread.currentThread().getId()+":线程执行完任务");
}catch (InterruptedException e) {
System.out.println(Thread.currentThread().getId()+":线程被中断");
}finally {
if(lock1.isHeldByCurrentThread())
lock1.unlock();
if(lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getId()+":线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
//中断其中一个线程
t2.interrupt();
}
}
也是两个线程相互等待锁的问题,这时候中断了其中一个线程,另一个线程也就能顺利的拿到锁并执行结束。
运行结果:可以看到,中断后两个线程双双退出。但真正完成任务的只有一个,另一个则放弃任务直接退出,释放资源。
Condition接口提供的基本方法如下:
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time,TimeUnit unit)throws InterruptedException;
void signal();
void signalAll();
await()方法会使当前线程等待,同时释放当前锁,当其他线程使用signal()或signalAll()方法的时候,线程会重新获得锁并继续执行。或者当线程被中断的时,也能跳出等待。跟Object.wait()方法很像。
awaitUninterruptibly()跟await方法基本相同,区别仅仅在于它不会在等待过程中响应中断。
signal()方法用于唤醒一个在等待中的线程。相对的signalAll()方法会唤醒所有在等待中的线程,跟Object.notify()很像。
用Condition来改造以前讲的notify()和wait()的生产者消费者的例子:
//仓库类
public class Store2 {
private ReentrantLock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
private final int MAX_SIZE; //仓库的最大容量
private int count; //当前的仓库数量
public Store2(int n){
MAX_SIZE = n;
count = 0;
}
//往仓库加货物的方法
public void add(){ //使用了wait()或notify()方法,需要加上syncronized关键字
try {
lock.lock();
while (count >= MAX_SIZE) { //否则可能会抛出java.lang.IllegalMonitorStateException
System.out.println("已经满了");
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
//打印当前仓库的货物数量
System.out.println(Thread.currentThread().toString() + " put" + count);
//仓库中已经有东西可以取了,则通知所有的消费者线程来拿
notEmpty.signal();
}finally {
lock.unlock();
}
}
//从仓库拿走货物的方法
public void remove(){
try {
lock.lock();
while (count <= 0) {
System.out.println("空了");
try {
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印当前仓库的货物数量
System.out.println(Thread.currentThread().toString() + " get" + count);
count--;
//仓库还没装满,通知生产者添加货物
notFull.signal();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
Store2 s = new Store2(5); //创建容量为5的仓库
//创建两个生产者和两个消费者
Thread pro = new Producer2(s);
Thread con = new Consumer2(s);
Thread pro2 = new Producer2(s);
Thread con2 = new Consumer2(s);
pro.setName("producer");
con.setName("consumer");
pro2.setName("producer2");
con2.setName("consumer2");
//启动各个线程
pro.start();
pro2.start();
con.start();
con2.start();
}
}
class Producer2 extends Thread{ //生产者线程类
private Store2 s;
public Producer2(Store2 s){
this.s = s;
}
public void run(){ //线程方法
while(true){ //永久循环
s.add(); //往仓库加货物
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
class Consumer2 extends Thread{ //消费者线程类
private Store2 s;
public Consumer2(Store2 s){
this.s = s;
}
public void run(){ //线程方法
while(true){ //永久循环
s.remove(); //往仓库取走货物
try{
Thread.sleep(1500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}