重入锁可以完全替代synchronized关键字。在JDK 6.0以前,重入锁的性能远远好于synchronized,但是在JDK 6.0以后,JDK在synchronized上做了大量的优化,两者的性能差距并不大。
重入锁
下面是一个重入锁简单的例子:
package dgb.test.concurrent;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Dongguabai
* @date 2018/9/3 11:10
*/
public class ReentrantLockTest implements Runnable {
private static final ReentrantLock lock = new ReentrantLock();
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLockTest test = new ReentrantLockTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
}
}
可以看出与synchronized相比,重入锁有着显示的操作过程,必须手动指定何时加锁何时释放锁,非常灵活,要注意的是,在退出临界区时一定要记得释放锁。
重入锁中重入的意思就是同一个线程可以连续多次获取同一把锁,因为如果不允许的话,同一个线程在第二次获得锁的时候会和自己产生死锁,程序会卡死在第二次获取锁的过程中。如果同一个线程多次获得重入锁,那么在释放锁的时候,也必须释放相同的次数,如果释放的次数多,会产生IllegalMonitorStateException异常;如果释放的次数少了,那么相当于这个线程还持有这个锁。
几个重要的API
- lock():获得锁,如果锁已经被占用,则等待
- lockInterruptibly():如果当前线程未被中断,获取锁(在等待锁的过程中可以响应中断)
- lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常
- tryLock():尝试获得锁,如果成功,返回true,失败则返回false。该方法不等待,会立即返回(可以和for(;;)结合一起用)
- tryLock(long timeout,TimeUnit unit):尝试获得锁,如果成功,返回true,失败则返回false。会等待给定时间
- unlock():释放锁
- getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
- getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
- hasQueuedThreads():是否有线程等待此锁
- isFair():该锁是否公平锁
- isHeldByCurrentThread():当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
- isLock():此锁是否有任意线程占用
- getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
- hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
重入锁的实现主要集中在Java层面。重入锁主要包含三个要素:
- 原子状态。原子状态时使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有;
- 等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作;
- 是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。
公平锁
重入锁主要是使用java.util.concurrent.locks.ReentrantLock类。有两个构造方法:
Lock lock=new ReentrantLock(true);//公平锁
Lock lock=new ReentrantLock(false);//非公平锁
公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先lock的线程不一定先获得锁。在大多数情况下,锁的申请都是非公平的,如果我们使用synchronized关键字进行锁的控制,那么产生的锁就是非公平锁。
但是实现公平锁系统就必须要维护一个有序队列,因此公平锁的成本相对比较高,性能也相对低下。
下面是一个公平锁简单的例子:
package dgb.test.concurrent;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Dongguabai
* @date 2018/9/3 11:46
*/
public class ReentrantLockTest2 implements Runnable {
private static final ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
lock.lock();
System.out.println("线程【" + Thread.currentThread().getName() + "】获得锁-----");
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockTest2 reentrantLockTest2 = new ReentrantLockTest2();
new Thread(reentrantLockTest2,"thread-1").start();
new Thread(reentrantLockTest2,"thread-2").start();
}
}
输出结果:
可以看出两个线程是交替获得锁的,几乎不会发生同一个线程连续多次获得锁的可能。从而公平性得到了保证。
中断响应
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它能获得这把锁继续执行,要么就保持等待。重入锁提供了一种中断的功能,就是在等待锁的过程中,程序可以根据需要取消对锁的请求。比如你跟朋友越好出去玩,等了一会朋友还没来,这时候接了个电话让你去做别的事情,这时候你就可以不用继续等待了。中断提供了一套类似的机制,如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,可以停止工作了。这种情况对于处理死锁是有一定的帮助的。
先看下面一段代码:
package dgb.test.concurrent;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Dongguabai
* @date 2018/9/3 17:33
*/
public class ReentrantLockTest4 implements Runnable {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
private int lock;
public ReentrantLockTest4(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock1");
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock2");
lock2.lockInterruptibly();
}else {
System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock2");
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
System.out.println("线程【"+Thread.currentThread().getName()+"】开始获取锁lock1");
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("线程【"+Thread.currentThread().getName()+"】进入finally");
if (lock1.isHeldByCurrentThread()){
System.out.println("线程【"+Thread.currentThread().getName()+"】开始释放锁lock1");
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()){
System.out.println("线程【"+Thread.currentThread().getName()+"】开始释放锁lock2");
lock2.unlock();
}
System.out.println("线程【"+Thread.currentThread().getName()+"】退出-------");
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockTest4 test1 = new ReentrantLockTest4(1);
ReentrantLockTest4 test2 = new ReentrantLockTest4(2);
Thread t1 = new Thread(test1);
Thread t2 = new Thread(test2);
t1.start();
t2.start();
}
}
很明显,构成了死锁:
这时候可以中断一个线程:
输出结果:
可以看到两个线程双双退出,只有一个线程完成了工作,另一个线程直接放弃任务直接退出了。
锁申请等待时间
避免死锁还有另一种方式,就是限时等待。比如你跟朋友出去玩,然后你等朋友等了一个小时朋友还没来,那就不等了。通常,我们无法判断一个线程为什么一直拿不到锁。可能是因为死锁,可能是因为饥饿。如果给定一个时间限制,如果超过了这个时间就让线程自动放弃,这样是有实际意义的。我们可以使用tryLock()方法。
下面是一个简单的示例:
package dgb.test.concurrent;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Dongguabai
* @date 2018/9/3 17:33
*/
public class ReentrantLockTest5 implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ReentrantLockTest5 reentrantLockTest5 = new ReentrantLockTest5();
Thread t1 = new Thread(reentrantLockTest5,"thread1");
Thread t2 = new Thread(reentrantLockTest5,"thread2");
t1.start();
t2.start();
}
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
//持有6秒
TimeUnit.SECONDS.sleep(6);
} else {
System.out.println("线程【" + Thread.currentThread().getName() + "】加锁失败!!!!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程【" + Thread.currentThread().getName() + "】进入finally");
if (lock.isHeldByCurrentThread()) {
System.out.println("线程【" + Thread.currentThread().getName() + "】开始释放锁lock1");
}
}
}
}
Condition条件
Condition条件和Object#wait()、Object#notify()的作用大致相同,但是Object#wait()和Object#notify()需要与synchronized关键字一起合作使用,而Condition是与重入锁相关联的。
ReentrantLock实现了Lock接口,在Lock接口中有这样一个方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition实例我们可以让线程在合适的时候等待,或者在某一个特定的时刻得到通知,继续执行。
Condition接口的基本方法如下:
- void await() throws InterruptedException
使当前线程进入等待状态,同时释放当前锁。当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待,这和Object#wait()很像;
- void awaitUninterruptibly()
当前线程进入等待状态,直到被通知,对中断不做响应;
- long awaitNanos(long nanosTimeout) throws InterruptedException
- boolean await(long time, TimeUnit unit) throws InterruptedException
- boolean awaitUntil(Date deadline) throws InterruptedException
当前线程进入等待状态直到将来的指定时间被通知,如果没有到指定时间被通知返回true,否则,到达指定时间,返回false;
- void signal()
唤醒一个等待在Condition上的线程;
- void signalAll()
唤醒等待在Condition上所有的线程;
和Object#wait()、Object#notify()类似,当线程使用Condition#await()的时候,要求线程持有相关的重入锁,当线程调用Condition#await()后,这个线程需要释放这把锁
下面简单演示一下Condition的用法:
package dgb.test.concurrent;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Dongguabai
* @date 2018/9/3 16:07
*/
public class ReentrantLockTest3 implements Runnable {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
@Override
public void run() {
System.out.println("线程【" + Thread.currentThread().getName() + "】开始执行---------------");
try {
lock.lock();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("线程【" + Thread.currentThread().getName() + "】解锁---------------");
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("线程【" + Thread.currentThread().getName() + "】开始执行---------------");
ReentrantLockTest3 reentrantLockTest3 = new ReentrantLockTest3();
Thread thread = new Thread(reentrantLockTest3);
thread.start();
Thread.sleep(5000);
System.out.println("线程【" + Thread.currentThread().getName() + "】发出通知");
lock.lock();
condition.signal();
lock.unlock();
}
}
执行流程分析如下: