重入锁
一 :什么是重入锁
这里需要详细介绍一下synchronized,它有一些功能性的限制 :
- 它无法中断一个正在等候获得锁的线程
- 无法通过轮询得到锁
- 如果不想等下去,也就没法得到锁。
- 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行
ReentrantLock
类实现了 Lock
,它拥有与 synchronized
相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
基本原理:
它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。
二:重入锁的使用
public class Test implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
lock.lock();
try {
i++;
} finally {
// 手动释放锁
lock.unlock();
}
}
}
public static void main(String args[]) throws InterruptedException {
Test reenterLock = new Test();
Thread t1 = new Thread(reenterLock);
Thread t2 = new Thread(reenterLock);
t1.start();
t2.start();
// 阻塞main线程,等到t1和t2全部执行完
t1.join();
t2.join();
System.out.println(i);
}
}
运行结果:
2000000
通过重入锁保证了多线程对临界区资源i操作的安全性。重入锁不像synchronized,重入锁需要手动释放锁,所以灵活性会优于synchronized,但是一定要注意必须释放锁
,不然其他线程就永远无法访问临界区了。切记。
那么为什么叫重入锁呢,是因为这种锁可以重复进入,比如下面:
lock.lock();
lock.lock();
try {
i++;
} finally {
// 手动释放锁
lock.unlock();
lock.unlock();
}
一定要注意,拿锁与释放锁的次数必须一致。如果释放锁的次数少了,就是说,锁还没释放掉,那么其他线程无法进入,如果释放锁的次数多了,那么会抛出java.lang.IllegalMonitorStateException
异常。
三:重入锁的高级特性
1. 中断响应
之前我们使用了同步(synchronized),但是它有个明显的问题就是如果同步线程正在等待某个资源,如果得不到锁,那么它会一直等待(一直处在请求锁的队列中)。而我们现在有个需求就是中途中断对锁的请求,重入锁便能很好的做到这一点。(重入锁方式解决死锁的方式1)
我们先来构造一个死锁:
public class DeadLock {
public static void main(String[] args) {
DeadLockThread deadLock1=new DeadLockThread(true);
DeadLockThread deadLock2=new DeadLockThread(false);
Thread t1=new Thread(deadLock1,"Thread-1");
Thread t2=new Thread(deadLock2,"Thread-2");
t1.start();
t2.start();
}
}
class DeadLockThread implements Runnable{
boolean flag;
final static Object o1=new Object();
final static Object o2=new Object();
public DeadLockThread(boolean flag){
this.flag=flag;
}
public void run() {
if(flag){
synchronized(o1){
try{
Thread.sleep(100);
}
catch(InterruptedException e){
e.printStackTrace();
}
synchronized(o2){//要放在o1临界区内,因为要保持o1的锁,申请o2的锁
System.out.println("我没有死锁-1");
}
}
}
else{
synchronized(o2){
try{
Thread.sleep(100);
}
catch(InterruptedException e){
e.printStackTrace();
}
synchronized(o1){//此处类似,保持o2的锁,申请o1的锁
System.out.println("我没有死锁-2");
}
}
}
}
}
上述的是通过同步来构造的死锁,运行结果就是无法打印出任何信息,我们使用重入锁来改造,并让他可以响应中断
public class ReentrantLockSolveDeadLock {
public static void main(String[] args) {
DeadLockThread deadLock1=new DeadLockThread(true);
DeadLockThread deadLock2=new DeadLockThread(false);
Thread t1=new Thread(deadLock1,"Thread-1");
Thread t2=new Thread(deadLock2,"Thread-2");
t1.start();
t2.start();
t1.interrupt();
}
}
class DeadLockThread implements Runnable{
boolean flag;
final static ReentrantLock LOCK1 = new ReentrantLock();
final static ReentrantLock LOCK2 = new ReentrantLock();
public DeadLockThread(boolean flag){
this.flag=flag;
}
public void run() {
try {
if(flag){
LOCK1.lockInterruptibly();
Thread.sleep(100);
LOCK2.lockInterruptibly();
System.out.println("我没有死锁-1");
}
else{
LOCK2.lockInterruptibly();
Thread.sleep(100);
LOCK1.lockInterruptibly();
//此处类似,保持o2的锁,申请o1的锁
System.out.println("我没有死锁-2");
}
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+":进入了catch块,因为被中断了");
}finally {
if (LOCK1.isHeldByCurrentThread()){
LOCK1.unlock();
}
if (LOCK2.isHeldByCurrentThread()){
LOCK2.unlock();
}
System.out.println(Thread.currentThread().getName()+":线程退出");
}
}
}
运行结果:
Thread-1:进入了catch块,因为被中断了
Thread-1:线程退出
我没有死锁-2
Thread-2:线程退出
我们对t1线程进行了中断,所以t1直接响应中断,放弃对锁的请求,也就执行不到“我没有死锁-1”,直接进入了catch块里面,而一旦线程t1中断,那么会放弃对LOCK2的请求,同时释放LOCK1,那么线程t2就可以成功请求到LOCK1,执行未完成的操作。
2. 锁申请等待限时
重入锁解决死锁的方式2
其实简单来讲,就是给定一个等待时间,让线程去拿锁,超过了那个时间,线程自动放弃抢锁。
tryLock()带参数:
代码示例如下:
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName()+":成功获取锁");
// 让第一个进来的线程再睡4秒,那么第二个线程则需要等至少4秒
// 第二个线程必然失败
Thread.sleep(4000);
} else {
System.out.println(Thread.currentThread().getName()+":获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String args[]) {
TimeLock timeLock = new TimeLock();
Thread t1 = new Thread(timeLock,"Thread-1");
Thread t2 = new Thread(timeLock,"Thread-2");
t1.start();
t2.start();
}
}
tryLock()不带参数表示,则表示线程一旦去请求锁,如果没有被占用,则申请成功,如果,锁已经被占用,则立刻返回false,不会有线程等待,也就不会产生死锁,我们改造一下之前死锁的例子:
public class TryLockSolveDeadLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
boolean flag;
public TryLockSolveDeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
if (lock1.tryLock()) {
try {
Thread.sleep(100);
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + ":进入锁2,完成任务");
return;
} finally {
lock2.unlock();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
finally {
lock1.unlock();
}
}
}
} else {
while (true) {
if (lock2.tryLock()) {
try {
Thread.sleep(100);
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + ":进入锁1,完成任务");
return;
} finally {
lock1.unlock();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
finally {
lock2.unlock();
}
}
}
}
}
public static void main(String args[]) {
TryLockSolveDeadLock r1 = new TryLockSolveDeadLock(true);
TryLockSolveDeadLock r2 = new TryLockSolveDeadLock(false);
Thread thread1 = new Thread(r1,"Thread-1");
Thread thread2 = new Thread(r2, "Thread-2");
thread1.start();
thread2.start();
}
}
运行结果:
Thread-1:进入锁2,完成任务
Thread-2:进入锁1,完成任务
t1获得lock1,t2获得lock2,t1申请Lock2,t2申请lock1,完成了死锁的模拟,通过trylock()结合while循环,让线程不停的去尝试获取锁,既不会造成死锁,又不会使线程不停的等待,执行一定的时间,最终可以获取资源而不造成死锁。(提出一个问题,一直不停的trylock会不会影响性能)
3.公平锁
高并发程序设计就讲了一点,感觉不准确,需要参考其他资料
当线程1先请求了锁A,线程二后请求了锁A,那么谁将先拿到呢?答案是不一定,因为系统只会随机从等待队列取出一个线程。要想公平对待,先到先得,则必须使用公平锁,其设置如下:
public ReentrantLock(boolean fair)
示例:
public class FairLock implements Runnable {
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + "得到锁");
} finally {
fairLock.unlock();
}
}
}
public static void main(String args[]) {
FairLock r1 = new FairLock();
Thread thread1 = new Thread(r1, "Thread_t1");
Thread thread2 = new Thread(r1, "Thread_t2");
thread1.start();
thread2.start();
}
}
运行结果:
Thread_t1得到锁
Thread_t2得到锁
Thread_t1得到锁
Thread_t2得到锁
Thread_t1得到锁Thread_t2得到锁
Thread_t2得到锁
Thread_t2得到锁
Thread_t2得到锁
Thread_t2得到锁
Thread_t2得到锁
Thread_t1得到锁
大家可能会疑惑上面的代码两个线程既有交替的,又有连续执行的,网上很多文章都没有讲到,并且很多都讲的是会交替执行,然而并不是,这个例子主要参考「高并发程序设计」,当我们将公平锁设置为true的时候,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁定的许多线程中的一个可以连续多次获得它,而其他活动线程没有进展并且当前没有保持锁定,还要注意不定时tryLock()
方法不尊重公平性设置。即使其他线程正在等待,如果锁可用,它也会成功。
四:重入锁的搭档:Condition
之前学过object.wait()和object.notify(),这两个方法是跟synchronized搭配使用的,而condition则是跟重入锁相关联,其作用也就跟wait和notify类似。
其基本方法如下:
void await() throws InterruptedException;
void awaitUninterruptibly();
void signal();
void signalAll();
- await() :使当前线程等待,同时释放锁,当线程使用signal()或者signalAll()方法,会重新获得锁并执行,当线程中断,也能跳出等待,不会一直傻傻的等待。
- awaitUninterruptibly() : 与await类似,但是不会再等待中响应中断。
- signal() : 随机唤醒一个等待的线程。
- signalAll():唤醒所有等待的线程,抢不到锁的进入阻塞状态,抢到锁的则进入运行状态
实战:
public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String args[]) throws InterruptedException {
ReenterLockCondition reenterLockCondition = new ReenterLockCondition();
Thread thread1 = new Thread(reenterLockCondition);
thread1.start();
System.out.println("先让线程睡两秒,再去唤醒它");
Thread.sleep(2000);
lock.lock();
condition.signal();
lock.unlock();
}
}
运行结果:
先让线程睡两秒,再去唤醒它
Thread is going on
参考:
https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
高并发程序设计
并发编程的艺术