重入锁可以完全替代synchronized关键字。事实上,在JDK1.5之前,重入锁的性能远远好于synchronized,但从JDK1.6后在synchronized上做了大量的优化,使得两者的性能差距并不大。
重入锁的类路径是java.util.concurrent.locks.ReentrantLock。顾名思义,Re-entrant-Lock就是可以重复进入的锁。当然,这里所说的重复是局限于同一条线程的。下面的代码演示了重入锁的简单应用。
public class ReentrantLockExample {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
lock.lock();
lock.lock();
try {
i++;
} finally {
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample r1 = new ReentrantLock();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
上述代码中,使用重入锁确保多线程对临界区资源i操作的安全性。也可以看出,与synchronized相比,重入锁有着显示的操作过程,开发者必须手动指定获取锁和释放锁的时机。正是因为这样,重入锁可以更灵活的控制程序逻辑。代码中连续进行了两次lock()操作,对于重入锁来说是允许这种操作的,这也是“重入”两个字的来源。但需要注意,加了多少次锁,当操作结束后也需要执行相应次数的释放操作。如果释放次数超过加锁次数,就会抛出一个java.lang.IllegalMonitorStateException异常;反之,如果释放锁的次数少于加锁次数,就相当于锁仍被当前线程持有,其他线程无法进入临界区。
除了更为灵活,重入锁还提供了一些高级功能:
-
中断响应
对于synchronized来说,如果一个线程在等待,那么只会有两种情况,要么获得锁进入临界区,要么就保持等待。而重入锁提供了另一种可能:在等待过程中,程序可以根据需要取消对锁的请求。这就可以对处理死锁有一定的帮助。
下面代码中就产生了一个死锁,但得益于锁中断,我们可以轻易地解决这个问题:public class IntLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); private int order; /** * 控制加锁顺序,方便构造死锁 */ public IntLock(int order) { this.order = order; } @Override public void run() { try { if (order == 1) { lock1.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) {} lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) {} lock1.lockInterruptibly(); } } catch (InterruptedException 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.start();t2.start(); Thread.sleep(1000); t2.interrupt(); }程序启动后,t1先占用lock1,再请求lock2;t2先占用lock2,再请求lock1。代码中对锁的请求使用了lockInterruptibly,这是一个可以响应中断的锁申请操作,即在等待锁的过程中可以响应中断。 当主线程处于休眠时,这两个线程处于死锁状态,主线程结束休眠后中断了t2,所以t2会放弃对lock1的申请,并在
finally块中释放了lock2,然后t1就可以顺利获得lock2并继续执行下去。 -
锁申请等待限时
除了等待外部通知,显示等待也是避免死锁的一种方法。一般情况下我们无法判断一个线程为什么会长期处于等待状态——可能是因为死锁,也可能是因为产生了饥饿。如果可以给定一个等待时间,超时后让线程自动放弃,在这种情况下也是很有帮助的。public class TimeLock implements Runnable { public static ReentrantLock lock = new ReentrantLock(); @Override public void run() { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { Thread.sleep(6000); } else { System.out.println("get lock failed"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if(lock.isHeldByCurrentThreads()) { lock.unlock(); } } } public static void main(String[] args) { TimeLock lock = new TimeLock(); Thread t1 = new Thread(lock); Thread t2 = new Thread(lock); t1.start(); t2.start(); } }上述代码演示了显示等待的使用。
tryLock()方法接收两个参数,一个表示等待时长,一个表示计时单位,代码中将单位设置为秒,即线程在这次锁请求中,最多等待5秒。如果5秒内获取到锁就会返回true,否则返回false。本例中持有锁的线程会将其占用6秒,所以另一个线程无法在5秒内获取到锁,对锁的请求会失败。tryLock()方法有一个不带任何参数的重载,这个重载方法的作用是尝试获取锁,如果锁已被其他线程占用,当前线程并不会进行等待,而是立刻返回false。 -
公平锁
大多数情况下,锁的申请都是非公平的。也就是说,先申请锁的线程不一定先获得锁。而公平锁则不是这样,它会按顺序保证先来后到。公平锁的一大特点就是不会产生饥饿,只要线程在等待队列中,最终肯定可以得到资源。synchronized的锁方式就是非公平的,而重入锁可以设置其是否公平。它有一个构造函数:public ReentrantLock(boolean fair)当参数fair为
true时表示锁是公平的。公平锁虽然看起来很优美,但往往性能相较于非公平锁来说更为 低下,因为非公平锁的分配机制更倾向于继续分配给上次获得锁的线程,而公平锁由于其公平性,往往会 引起频繁的线程切换——一项非常消耗系统资源的操作。 下面的代码可以很好地突出公平锁的特点”public class FairLock implements Runnable { public static ReentrantLock fairLock = new ReentrantLock(); @Override public void run() { while(true) { try { fairLock.lock(); System.out.println(Thread.currentThread().getName() + " 获得锁"); } finally { fairLock.unlock(); } } } public static void main(String[] args) throws InterruptedException{ FairLock fair = new FairLock(); Thread t1 = new Thread(fair, "Thread_t1"); Thread t2 = new Thread(fair, "Thread_t2"); t1.start();t2.start(); } }在公平锁的情况下,通常会得到如下输出:
Thread_t1 获得锁 Thread_t2 获得锁 Thread_t1 获得锁 Thread_t2 获得锁 Thread_t1 获得锁 Thread_t2 获得锁 Thread_t1 获得锁 Thread_t2 获得锁可以看出,两个线程基本上是交替获得锁的,几乎不会发生同一个线程连续多次获取锁的情况,公平性得到了保证。如果使用非公平锁,情况则大大不同,输出会是下面这样:
Thread_t1 获得锁 Thread_t1 获得锁 Thread_t1 获得锁 Thread_t1 获得锁 Thread_t1 获得锁 Thread_t2 获得锁 Thread_t2 获得锁 Thread_t2 获得锁 Thread_t2 获得锁这是由于系统调度会倾向于已经获取过锁的线程,这种方式性能很高,但没有什么公平性可言。
重入锁的实现
1. 原子状态
原子状态使用CAS(Comparea and Swap)操作来存储当前锁的状态,判断锁是否已被别的线程持有。
2. 等待队列
所有没有请求到锁的线程,会进入等待队列进行等待。持有线程释放锁后,系统就会从等待队列唤醒一个线程,继续工作。
3. 阻塞元语
park()和unpark()方法分别对应挂起和恢复线程。没有得到锁的线程将会被挂起。
参考文献
- 《Java高并发程序设计实战》.电子工业出版社.葛一鸣、郭超编著
- dzone.com/articles/wh…