可重入概念
若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。
就是说某个线程已经获得某个锁并且这个锁还没释放,可以再次获取该锁而不会出现死锁,不管是可重入锁还是不可重入锁,锁多少次,就要释放多少次
synchronized也是可重入锁
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
一个简单的例子:用户名和密码保存在本地txt文件中,则登录验证方法和更新密码方法都应该被加synchronized,那么当更新密码的时候需要验证密码的合法性,所以需要调用验证方法,此时是可以调用的。
今天的重头ReentrantLock类(finally一定要手动释放锁)
// 演示可重入锁是什么意思
public class WhatReentrant2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}
使用场景
1.如果发现该操作已经在执行中则不再执行
a、用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。
以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)
private ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) { //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
try {
//操作
} finally {
lock.unlock();
}
}
2.如果发现该操作已经在执行,等待一个一个执行
这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与非公平锁,而synchronized永远是非公平锁
private ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
private ReentrantLock lock = new ReentrantLock(true); //公平锁
try {
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
//其他操作
} finally {
lock.unlock();
}
3.如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。
synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)
这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)
try {
lock.lockInterruptibly();
//操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
不公平锁与公平锁的区别
- 公平情况下,操作会排一个队按顺序执行,来保证执行顺序。谁等的时间最长,谁就先获取锁。(会消耗更多的时间来排队)
- 不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)
源码里同时有公平和非公平,都是使用Sync,也就是使用之前讲过的aqs,复写tryAcquire来实现的
非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁
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;
}
重写的tryAcquire()方法区别在这
hasQueuedPredecessors 作用是判断当前线程前面有没有在排队的线程,有则返回true,否则返回false;换句话讲就是判断当前线程能不能获得锁,如果能够获得锁则返回false。如果队列为空,或是当前线程位于队列头部会返回false,其他情况返回true。
线程执行可重入锁时抛异常,是否会释放锁
看到一个例子,试一试
@Slf4j
public static class SynchronizedExceptionRunnable implements Runnable {
private volatile boolean flag = true;
@Override
public void run() {
synchronized (this) {
if (flag) {
//让先启动的线程先执行异常方法methodB后,flag==false,并且抛出异常线程停止,直接释放锁,不会执行后面的代码;
methodB();
} else {
//后启动的线程再获取锁,进入if-else,再获取锁执行methodA
methodA();
}
log.info("{}:if-else end!",Thread.currentThread().getName());
}
}
public synchronized void methodA(){
log.info("ThreadName:{}----methodA", Thread.currentThread().getName());
}
public synchronized void methodB() {
flag = false;
log.warn("ThreadName:{}----methodB will throw a exception!",Thread.currentThread().getName());
//如果把下面这行抛异常的代码注释掉,会执行下面的线程睡眠5秒和最后的日志代码
//如果不注释,会抛出异常,不在执行后面的代码,并且释放锁,methodA方法就会执行
int a = 1/0;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("ThreadName:{}----methodB End!",Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExceptionRunnable runnable = new SynchronizedExceptionRunnable();
Thread thread1 = new Thread(runnable,"杯子");
Thread thread2 = new Thread(runnable,"人");
thread1.start();
thread2.start();
}
报错返回
可以看到只跑了杯子这条打印前的代码,报错后面的代码不会跑
注释掉报错再跑
杯子会往下跑,并且sleep霸占线程5秒,之后才会跑人
总结:如果锁的计数器为2,执行过程中抛出异常,锁的计数器直接置为0,线程会直接停止并且会直接释放锁
参考: