-
本章的代码在>
https://gitee.com/kiteff/java_multithreading.git
的
reentlock 包
下
什么是可重入锁
假设现在有一个线程A 他获取了锁,然后他再一次去尝试获取锁,如果能成功,就说这个锁是可以重入的,在java中
ReentrantLock.class
就是一个可重入的锁,关于这个类的源码解析,将在后面的章节讲到,现在我们看一下一个可重入锁的基本使用。
使用ReentrantLock
首先我们写一个简单的例子,这个例子是线程不安全的:
public class NoteSafe {
private static Integer n = 5;
public static void main(String[] args) {
//新建一个线程池
ExecutorService exe = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
//在线程池中启动 50个线程
for (int i = 0; i < 50; i++) {
exe.execute(() -> {
try {
if (n > 0) {
Thread.sleep(500);
n--;
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
exe.shutdown();
while (!exe.isTerminated()){
}
System.out.println(n);
}
}
上面的程序的逻辑就是 ,我们设置了一个参数
n
,然后同时启动50个线程去并发访问这个n,去执行 -1 的操作。
最后查看结果,当然结果肯定就是 一个负数了。
现在我们使用可重入锁来改造这个不安全的类
通过一个创建一个ReentrantLock ,调用他的
lock()
方法,将资源上锁,使用
unlock()
方法进行解锁。
public class NoteSafe {
private static Integer n = 5;
public static void main(String[] args) {
ExecutorService exe = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
ReentrantLock reentrantLock = new ReentrantLock();
for (int i = 0; i < 50; i++) {
exe.execute(() -> {
try {
reentrantLock.lock();
if (n > 0) {
Thread.sleep(500);
n--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
});
}
exe.shutdown();
while (!exe.isTerminated()){
}
System.out.println(n);
}
}
这里我们创建了一个 可重入的锁,然后利用lock 锁住,运行的结果一定是0了
可以看到和synchronized 不一样的是
ReentrantLock
需要我们自己手动的上锁和解锁,斜体样式所以在使用的时候应该把解锁也就是
unlock
放入到
finally
的代码块里去,以免出现死锁这点很重要
自己去实现一个可重入锁
通过上面的这个例子我们已经掌握了,可重入锁的使用方法,他的特点就是手动的上锁和解锁。
下面我们将自己去实现一个可重入的锁。
实现Lock 接口
首先我们看一下java中有哪些类实现了这个接口。
可以看到 我们一些常见的可重入的锁,读写锁还有我们上面用到的ReentrantLock 都实现了这个接口。那么我们也只要去实现这个接口就行。
然后去重写 他的
lock()
和
unlock()
方法。
具体代码实现
public class DemoReentryLock implements Lock {
/**
* 用来标记是否已经有线程获取了锁
* 是的话 值为true 否则为 false
*/
private boolean isHold = false;
/**
* 用来记录当前获取锁的线程
*/
private Thread nowHoldThread = null;
/**
* 记录锁重入的次数
*/
private int holdNum = 0;
@Override
public synchronized void lock() {
/**
* 判断是否已经有线程获取了锁,
* 如果已经有人获取了锁,再判断当前操作锁的线程是不是获取了锁的线程
*/
if (isHold && Thread.currentThread() != nowHoldThread) {
//说明当前操作锁的线程,不是获取锁的线程,将当前线程挂起
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没有人获取锁,或者被挂起的线程被唤醒了,将获取锁的线程,变成当前操作锁的线程
nowHoldThread = Thread.currentThread();
//将重入的次数加一,同时设置 标志位位true,表示锁已经被获取
holdNum++;
isHold = true;
}
@Override
public synchronized void unlock() {
//解锁的时候,首先判断当前操作的线程是不是获取了锁的线程
if (Thread.currentThread() == nowHoldThread) {
//将记录锁重入的次数 -1 ,在判断其值是不是0,是0说明重入的次数已经全部出来了,就可以通知其他线程来获取锁
holdNum--;
if (holdNum == 0) {
isHold = false;
notify();
}
}
}
//这里省略了其他要重写的方法,为了节省空间
}
将我们自己实现的锁去替代上一个例子中的,ReentrantLock ,重新运行程序,结果一直是0 ,说明这个锁已经生效
验证我们写的锁是否能重入
到上面为止,我们已经成功实现了一个锁,至于他是不是可重入的,还需要程序验证一下,也很简单
我们只要新建一个线程,在里面上锁后,不要解锁直接再上锁一次,看看会不会报错就可以
public class ReentDemo {
public static void main(String[] args) {
DemoReentryLock demoReentryLock = new DemoReentryLock();
new Thread(() -> {
System.out.println("我开始上锁了");
demoReentryLock.lock();
demoReentryLock.lock();
demoReentryLock.unlock();
demoReentryLock.unlock();
System.out.println("已经全部解锁");
}).start();
}
}
运行后程序正常输出,说明我们的锁是可以重入的。