重入锁简单理解就是对同一个线程而言,它可以重复的获取锁。例如这个线程可以连续获取两次锁,但是释放锁的次数也一定要是两次。下面是一个简单例子:
public class ReenterLock {
private static ReentrantLock lock = new ReentrantLock();
private static int i = 0;
// 循环1000000次
private static Runnable runnable = () -> IntStream.range(0, 1000000).forEach((j) -> {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
});
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
// 利用join,等thread1,thread2结束后,main线程才继续运行,并打印 i
thread1.join();
thread2.join();
// 利用lock保护的 i,最终结果为 2000000,如果不加,则值肯定小于此数值
System.out.println(i);
}
}
从上面的代码可以看到,相比于synchronized
,开发者必须手动指定锁的位置和什么时候释放锁,这样必然增加了灵活性。
线程中断响应
如果线程阻塞于synchronized
,那么要么获取到锁,继续执行,要么一直等待。重入锁提供了另一种可能,就是中断线程。下面的例子是利用两个线程构建一个死锁,然后中断其中一个线程,使另一个线程获取锁的例子:
public class ReenterLockInterrupt {
private static ReentrantLock lock = new ReentrantLock();
private static Runnable runnable = () -> {
try {
// 利用 lockInterruptibly 申请锁,这是可以进中断申请的申请锁操作
lock.lockInterruptibly();
// 睡眠20秒,在睡眠结束之前,main方法里要中断thread2的获取锁操作
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
String threadName = Thread.currentThread().getName();
// 中断后抛出异常,最后要释放锁
// 如果是线程1则释放锁,因为线程2就没拿到锁,所以不用释放
if ("Thread-1".equals(threadName)) lock.unlock();
System.out.println(threadName+" 停止");
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(runnable, "thread-1");
Thread thread2 = new Thread(runnable, "thread-2");
thread1.start();
// 让主线程停一下,让thread1获取锁后再启动thread2
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 这里什么也不做
}
thread2.start();
thread2.interrupt();
}
}
thread-1拿到锁之后,线程即持有锁并等待20秒,然后thread-2启动,并没有拿到锁,这时候中断thread-2线程,线程2退出。
有限时间的等待锁
顾名思义,简单理解就是在指定的时间内如果拿不到锁,则不再等待锁。当持有锁的线程出问题导致长时间持有锁的时候,你不可能让其他线程永远等待其释放锁。下面是一个例子:
public class ReenterTryLock {
private static ReentrantLock reenterLock = new ReentrantLock();
private static Runnable runnable = () -> {
try {
// tryLock()方法会返回一个布尔值,获取锁成功则为true
if (reenterLock.tryLock(3, TimeUnit.SECONDS)) {
Thread.sleep(5000);
} else {
System.out.println(Thread.currentThread().getName() + "获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 最后,如果当前前程在持有锁,则释放锁
if (reenterLock.isHeldByCurrentThread()) {
System.out.println(Thread.currentThread().getName() + "释放锁了");
reenterLock.unlock();
}
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(runnable, "thread-1");
Thread thread2 = new Thread(runnable, "thread-2");
thread1.start();
thread2.start();
}
}
这里使用tryLock()
第一个获取锁的线程,会停止5秒。而获取锁的设置为3秒获取不到锁则放弃,所以第二个去尝试获取锁的线程是获取不到锁而被迫停止的。如果tryLock()
方法不传入任何参数,那么获取锁的线程不会等待锁,则立即返回false。
公平锁与非公平锁
当一个线程释放锁时,其他等待的线程则有机会获取锁,如果是公平锁,则分先来后到的获取锁,如果是非公平锁则谁抢到锁算谁的,这就相当于排队买东西和不排队买东西是一个道理。Java的synchronized
关键字就是非公平锁。
那么重入锁ReentrantLock()
是公平锁还是非公平锁?
重入锁ReentrantLock()
是可以设置公平性的,可以参考其构造方法:
// 通过传入一个布尔值来设置公平锁,为true则是公平锁,false则为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
构建一个公平锁需要维护一个有序队列,如果实际需求用不到公平锁则不需要使用公平锁。下面用一个例子来演示公平锁与非公平锁的区别:
public class ReenterTryLockFair {
// 分别设置公平锁和非公平锁,分析打印结果
private static ReentrantLock lock = new ReentrantLock(true);
private static Runnable runnable = () -> {
while (true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 获取了锁");
} finally {
lock.unlock();
}
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(runnable, "thread---1");
Thread thread2 = new Thread(runnable, "thread---2");
Thread thread3 = new Thread(runnable, "thread---3");
thread1.start();
thread2.start();
thread3.start();
}
}
当设置为true
即公平锁的时候,可以看到打印非常规律,截取一段儿打印结果:
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
thread---1 获取了锁
thread---2 获取了锁
thread---3 获取了锁
可以看到,都是thread–1,thread–2,thread–3,无限循环下去,如果设置的为非公平锁,打印结果就混乱没有规律了:
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---3 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---2 获取了锁
thread---1 获取了锁
Condition
同jdk中的等待/通知机制类似,只不过Condition是用在重入锁这里的。有了Condition,线程就可以在合适的时间等待,在合适的时间继续执行。
Condition接口包含以下方法:
// 让当前线程等待,并释放锁
void await() throws InterruptedException;
// 和await类似,但在等待过程中不会相应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒等待中的线程
void signal();
// 唤醒等待中的所有线程
void signalAll();
下面是一个简单示例:
public class ReenterLockCondition {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static Runnable runnable = () -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "进入等待。。");
condition.await();
System.out.println(Thread.currentThread().getName() + "继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(runnable, "thread--1");
thread.start();
Thread.sleep(2000);
lock.lock();
condition.signal();
System.out.println("主线程发出信号");
lock.unlock();
}
}
thread–1启动,拿到锁,然后进入等待并且释放锁,2秒后,主线程拿到锁,然后发出信号并释放锁,最后,thread–1继续执行。下面是打印结果:
thread--1进入等待。。
主线程发出信号
thread--1继续执行