前言
ReeterLock和synchronized具有相同的内存语义;
0.与sysnchronized相比,重入锁具有显示的操作过程,开发人员必须指定何时加锁,何时释放锁。因此,重入锁更加灵活;
1.与synchronized相比,ReentrantLock功能更加强大;
2.ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合;
3.ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
4.ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
5.ReentrantLock支持中断处理,且性能较synchronized会好些。
重入锁
public class ReeterLock implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0; //临界区资源
public void run() {
for(int j=0;j<10000000;j++) {
//使用重入锁保护临界区资源,确保多线程对i操作的安全性
lock.lock();
try {
i++;
} finally {
lock.unlock(); //退出临界区的时候,必修记得释放锁。否则其它线程就不能访问临界区了
}
}
}
public static void main(String[] args) throws InterruptedException {
ReeterLock r1=new ReeterLock();
Thread t1=new Thread(r1);
Thread t2=new Thread(r1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
为什么会叫重入锁呢?Re-Entrant-Lock?因为这个锁是可以反复进入的,这个的反复仅仅局限于一个线程。
lock.lock(); //1
lock.lock(); //2
try{
i++;
}finally{
lock.unlock(); //1
lock.unlock(); //2
}
在上述的情况下,一个线程连续两次获得同一把锁,这是允许的。如果不允许发生这种操作,那么同一线程在获得第二次锁的时候,就会和自己产生死锁。
但需要注意的是,如果同一个线程多次获得同一把锁,那么释放锁的时候,也必须释放相同的次数。当然,如果释放的次数多了,就会抛出java.lang.IllegalMonitorStateException异常。反之,如果释放少了,那么这个线程还持有这该锁,其他线程无法进入该临界区;
获取锁
//非公平锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
//lock方法
public void lock() {
sync.lock();
}
Sync为ReentrantLock的一个内部类,它继承自AQS(AbstractQueuedSynchronizer);它有两个子类:公平锁FairSync和非公平锁NonfairSync;
ReentrantLock里面大部分的功能都是委托给Sync及其子类来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,它是非公平锁的默认实现;
下面是非公平锁的lock方法:
final void lock() {
//尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//获取失败,调用AQS的acquire(int arg)方法
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法首先调用tryAcquire(int arg)方法,在AQS中讲述过,tryAcquire(int arg)需要自定义同步组件自己实现;
非公平锁的tryAcquire(int arg)是这样实现的:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//state == 0,表示没有该锁处于空闲状态
if (c == 0) {
//获取锁成功,设置为当前线程所有
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); //CAS操作
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//锁不处于空闲状态
//判断持有锁的线程是否current线程
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
释放锁
//ReentrantLock提供的unlock方法
public void unlock() {
sync.release(1);
}
//unlock内部使用Sync的release(int arg)释放锁,release(int arg)是在AQS中定义的:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
与获取同步状态的tryAcquire(int arg)方法一样,释放同步状态的tryRelease(int arg)同样是需要自定义同步组件自己实现;
下面是非公平锁自己实现的tryRelease方法;
protected final boolean tryRelease(int releases) {
//减掉releases
int c = getState() - releases;
//如果释放的不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//state == 0 表示已经释放完全了,其他线程可以获取同步状态了
if (c == 0) {
free = true; //表示锁释放成功
setExclusiveOwnerThread(null); //将这个锁持有线程设置为null
}
setState(c);
return free;
}
重入锁的中断响应
对于synchronized来说,如果一个线程在等待锁,那么结果就只有两种情况,第一获得这把锁继续执行,第二保持等待。而使用重入锁,则有第三种情况,即线程可以被中断。就是在等到锁的过程中,程序可以根据需要取消对锁的请求。
中断提供了类似的机制。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,取消对锁的请求。这对解决死锁是很有帮助的。
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) {
//申请锁,但与lock()不同的是
//在等待锁的过程中,它允许被中断(Thread.interrupt())
//只是会抛出异常
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (Exception e) {}
lock2.lockInterruptibly();
}else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (Exception e) {}
lock1.lockInterruptibly();
}
} catch (Exception e) {
e.printStackTrace();
}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和t2启动后,t1先占用lock1,再占用lock2;
//t2则先占用lock2,再占用lock1;这样很容易形成t1和t2的互相等待,即死锁;
t1.start();
t2.start();
Thread.sleep(100); //主线程休眠,t1和t2处于死锁状态
//中断其中一个线程
t2.interrupt(); //t2线程被中断,t2会放弃对lock1的申请,同时释放lock2;t1顺利执行完;
}
}
锁申请等待限时
除了“重入锁的中断响应”外,要避免死锁,还有一种方法,那就是限时等待。给定一个等待时间,如果还没有获得想要的锁,那么线程就自动放弃申请。
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
//tryLock()最多等待5秒,超过5秒还没得到锁,就返回false
//tryLock()也可以不带参数,这种情况下不等待
if (lock.tryLock(5, TimeUnit.SECONDS)) {
Thread.sleep(6000); //占用锁6秒,所以另外一个线程请求锁将失败
}else {
System.out.println("get lock failed");
}
}catch (Exception e) {
e.printStackTrace();
}finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock r1 = new TimeLock();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
}
}
公平锁
在大多数情况下,锁的分配是非公平的;
而公平的锁,它会按照时间的先后顺序。保证先来先得。公平锁的一个特点是:它不会产生饥饿现象,只要你排队,最终还是可以获得锁的;
对于非公平锁而言,一个线程会倾向于再次获得已经持有的锁。
要实现公平锁则系统必须要维护一个有序队列,因此公平锁的实现成本比较高,性能也比较差,在多线程访问的情况下,公平锁的吞吐量较;
因此默认情况下,锁是非公平的。
当然,如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的;而重入锁允许我们对其公平性进行设置。//构造函数:当fair为true时,表示锁是公平的
public ReetrantLock(boolean fair)
公平锁的tryAcquire(int arg)实现如下:
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;
}
比较非公平锁和公平锁获取锁的过程,会发现两者唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors(),定义如下:
//主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回true,否则返回false
public final boolean hasQueuedPredecessors() {
Node t = tail; //尾节点
Node h = head; //头节点
Node s;
//头节点 != 尾节点
//同步队列第一个节点不为null
//当前线程是同步队列第一个节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
总结
ReetrantLock的几个重要的方法如下:
- lock():申请锁,如果锁已经被占用,则等待;
- lockInterruptibly():申请锁,并且在获得锁之前,响应中断响应;
- tryLock():申请锁,成功则返回true,不等待;
- tryLock(long time,TimeUnit unit):申请锁,并等待time时间;
- unlock():是否锁;
实现重入锁,从代码层面得从三个方面看:
- 原子状态:原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被其他线程持有;
- 等待队列:所有没有请求到锁的线程,都会进入等待队列等待。是否锁后,系统会唤醒一个线程去持有锁;
- 阻塞原语park()和unpark():用来挂起和恢复线程。没有得到锁的线程会被挂起;关于这点,可以参考线程阻塞工具类LockSupport类;