本文是Java线程安全和并发编程知识总结的一部分。
2.2.1 可重入锁与不可重入锁。
JDK中提供的锁,基本都是提供可重入锁的实现为主。
2.2.1.1 定义
可重入锁定义:
- 一个线程再次申请自己正在持久的锁,会立即成功。
- 可重入锁的锁操作粒度为“线程0”,而不是“操作”。
- 每个对象的内置锁,都是可重入锁。
不可重入锁的定义:
- 一个锁被一个线程获取后,只要尚未释放,就不能被再次获取,无论是否是当前线程。
- 不可重入锁的锁操作粒度是“操作”,而不是“线程”。
- 正常情况下,我们一般不需要使用不可重入锁。
这是一个说明两者区别的例子:
/**
* @author xx
* 2020年1月31日 下午4:08:45
*/
public class Sample9 {
/**
* 由 synchronized 提供线程安全保护
* 2020年1月31日 下午2:50:17 xx添加此方法
* @param a 基本类型参数
* @param b 基本类型参数
* @return
*/
public synchronized void calc(int a, int b) {
// 执行业务
}
}
/**
* @author xx
* 2020年1月31日 下午4:08:45
*/
public class ChildSample9 extends Sample9 {
/**
* 若 synchronized 是不可重入锁,则这个方法将死锁。因为子类的 calc 方法已获取了内部锁,调用父类的calc方法时,
* 将在当前线程尝试再次尝试获得内部锁,按不可重入锁的定义,当前线程将被阻塞,因此死锁了。
* 2020年1月31日 下午2:50:17 xx添加此方法
* @param a 基本类型参数
* @param b 基本类型参数
* @return
*/
public synchronized void calc(int a, int b) {
// 子类特有的业务操作
super.calc(a, b);
}
}
- 幸好 synchronized 语法使用的内置锁是可重入锁,因此调用子类的 calc方法不会死锁:
- 调用子类的calc方法时,当前线程申请并获取了当前对象的内置锁。
- 子类的calc方法调用父类的calc方法时,当前线程会再次尝试申请当前对象的内置锁,因为父类的calc方法也有 synchronized 修饰词。根据可重入锁的语义,这个申请立即成功。功能正常运行。
- 如果 synchronized 语法是不可重入锁,则调用子类的calc方法将直接死锁:
- 调用子类的calc方法时,当前线程申请并获取了当前对象的内置锁。
- 子类的calc方法调用父类的calc方法时,当前线程会再次尝试申请当前对象的内置锁,因为父类的calc方法也有 synchronized 修饰词。根据不可重入锁的语义,这个申请失败,当前线程被阻塞等待锁的主人释放锁。但这个锁本身,也是本当前线程获取的,因此当前线程阻塞后,将不可能释放锁。死锁形成了。
- JDK默认提供的锁都是可重入锁;可以用二值信号量
模拟不可重入锁。
直接使用可重入锁对象 ReentrantLock 的版本如下:
/**
* @author xx
* 2020年1月31日 下午4:08:45
*/
public class Sample9 {
/**
* JDK提供的可重入锁实现
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 由 synchronized 提供线程安全保护
* 2020年1月31日 下午2:50:17 xx添加此方法
* @param a 基本类型参数
* @param b 基本类型参数
* @return
*/
public void calc(int a, int b) {
this.lock.lock();
try {
// 执行业务
} finally {
this.lock.unlock();
}
}
}
其效果和使用 synchronized 关键词相同。
2.2.1.2 ReentrantLock方法的主要功能
该对象同时支持公平锁和不公平锁,通过构造函数中传递参数来指定,默认是非公平锁。
该对象提供如下方法:
- lock: 申请锁;获取成功,立即返回;申请失败,阻塞直到获取锁。不响应中断。
- lockInterruptibly: 申请锁;获取成功,立即返回;申请失败,阻塞直到获取到锁或被中断。
- tryLock: 已非公平锁方式立即申请锁;就是如果现在锁可用,则成功;如果现在锁不可用,则立即失败。不响应中断。
- tryLock(long timeout, TimeUnit unit):以近似公平锁的方式在指定时间内申请锁;也就是如果现在锁可用,则立即成功;如果现在锁不可用,在指定时间内以公平锁策略竞争锁。并且响应中断。
- unlock: 释放锁。
- newCondition: 创建基于当前锁对象的条件对象,用于条件锁。
。
前面说过,synchronized 关键词实际上使用内置锁。内置锁是不响应中断的,类似 ReentrantLock 的lock() 方法。
对响应中断的含义,通过下面这个参考自网络其他文章的例子来说明:
/**
* @author xx
* 2020年2月9日 下午9:03:05
*/
public class Reader extends Thread {
private Buffer buff;
public Reader(Buffer buff) {
this.buff = buff;
}
@Override
public void run() {
try {
buff.read();
} catch (InterruptedException e) {
System.out.println("我不读了");
}
System.out.println("读结束");
}
}
/**
* @author xx
* 2020年2月9日 下午9:02:38
*/
public class Writer extends Thread {
private Buffer buff;
public Writer(Buffer buff) {
this.buff = buff;
}
@Override
public void run() {
buff.write();
}
}
/**
* 使用 sychronized 关键词,线程进入同步代码块以后不会响应中断。
* @author xx
* 2020年2月9日 下午9:02:23
*/
public class UninterruptableBuffer implements Buffer {
/**
* 我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,
* 也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,它都不来响应下,看来真的要等死了。
* 2020年2月9日 下午9:15:37 xx添加此方法
* @param args
*/
public static void main(String[] args) {
UninterruptableBuffer buff = new UninterruptableBuffer();
final Writer writer = new Writer(buff);
final Reader reader = new Reader(buff);
writer.start();
reader.start();
new Thread(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
for (;;) {
//等5秒钟去中断读
if (System.currentTimeMillis()
- start > 5000) {
System.out.println("不等了,尝试中断");
reader.interrupt(); //尝试中断读线程
break;
}
}
}
}).start();
}
@Override
public void write() {
synchronized (this) {
long startTime = System.currentTimeMillis();
System.out.println("开始往这个buff写入数据…");
for (;;)// 模拟要处理很长时间
{
if (System.currentTimeMillis()
- startTime > Integer.MAX_VALUE) {
break;
}
}
System.out.println("终于写完了");
}
}
@Override
public void read() {
synchronized (this) {
System.out.println("从这个buff读数据");
}
}
}
/**
* 使用 ReentrantLock.lockInterruptible,线程进入同步代码块以后会响应中断。
* @author xx
* 2020年2月9日 下午9:02:23
*/
public class InterruptableBuffer implements Buffer {
private ReentrantLock lock = new ReentrantLock();
/**
* ReentrantLock给了一种机制让我们来响应中断,让“读”能伸能屈,勇敢放弃对这个锁的等待。
* 2020年2月9日 下午9:14:08 xx添加此方法
* @param args
*/
public static void main(String[] args) {
InterruptableBuffer buff = new InterruptableBuffer();
final Writer writer = new Writer(buff);
final Reader reader = new Reader(buff);
writer.start();
reader.start();
new Thread(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
for (;;) {
//等5秒钟去中断读
if (System.currentTimeMillis()
- start > 5000) {
System.out.println("不等了,尝试中断");
reader.interrupt(); //尝试中断读线程
break;
}
}
}
}).start();
}
@Override
public void write() {
this.lock.lock();
try {
long startTime = System.currentTimeMillis();
System.out.println("开始往这个buff写入数据…");
for (;;)// 模拟要处理很长时间
{
if (System.currentTimeMillis()
- startTime > Integer.MAX_VALUE) {
break;
}
}
} finally {
this.lock.unlock();
}
System.out.println("终于写完了");
}
@Override
public void read() throws InterruptedException {
// 本实现使用 lockInterruptibly 方法,可以响应中断。
// 如果使用 lock 方法,那么也不响应中断了。
this.lock.lockInterruptibly();
try {
System.out.println("从这个buff读数据");
} finally {
this.lock.unlock();
}
}
}