2020-02-01 2.2.1 可重入锁与不可重入锁。

本文是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);
    }
}
  1. 幸好 synchronized 语法使用的内置锁是可重入锁,因此调用子类的 calc方法不会死锁:
  • 调用子类的calc方法时,当前线程申请并获取了当前对象的内置锁。
  • 子类的calc方法调用父类的calc方法时,当前线程会再次尝试申请当前对象的内置锁,因为父类的calc方法也有 synchronized 修饰词。根据可重入锁的语义,这个申请立即成功。功能正常运行。
  1. 如果 synchronized 语法是不可重入锁,则调用子类的calc方法将直接死锁:
  • 调用子类的calc方法时,当前线程申请并获取了当前对象的内置锁。
  • 子类的calc方法调用父类的calc方法时,当前线程会再次尝试申请当前对象的内置锁,因为父类的calc方法也有 synchronized 修饰词。根据不可重入锁的语义,这个申请失败,当前线程被阻塞等待锁的主人释放锁。但这个锁本身,也是本当前线程获取的,因此当前线程阻塞后,将不可能释放锁。死锁形成了。
  1. 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();
        }
    }
}

你可能感兴趣的:(2020-02-01 2.2.1 可重入锁与不可重入锁。)