Java锁ReentrantLock

从Java 5之后,Java提供了Lock实现同步访问,需要说明的是Lock并不是用来替代synchronized的。

synchronized有以下不足

效率低:锁的释放情况少、不能设置锁的超时时间、不能中断正在试图获得锁的线程。
不够灵活:加锁、释放锁的时机单一,进入同步同步代码块获取锁,离开释放锁。

Lock可以提供更多高级功能。

ReentrantLock的基本使用

ReentrantLock直接翻译过来是可重入锁的意思,是Lock接口的实现类。

lock()获取锁,unlock()释放锁

class LockMustUnlock {
    /**
     * 创建可重入锁
     */
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        /**
         * lock()获取锁。如果锁已被其他线程持有,则进行等待。
         * Lock锁不会像synchronized一样在异常时自动释放锁。
         * 最佳实践是在finally的第一行释放锁,以保证发生异常时锁一定被释放。
         *
         * 最好是写了lock.lock()之后,直接写try finally释放锁,然后再写业务代码
         */
        lock.lock();
        try {
            System.out.println("创建锁,必须释放锁");
        } finally {
            lock.unlock();
        }
    }
}

tryLock()与tryLock(long time, TimeUnit unit)

class TryLock{
    private static Lock lock = new ReentrantLock();

    /**
     * tryLock()用法
     */
    private static Runnable runnable1 = () -> {
        System.out.println(Thread.currentThread().getName()+"运行");
        // 如果tryLock()返回true,获取到锁,才能执行释放锁操作
        if (lock.tryLock()){
            try {
                System.out.println(Thread.currentThread().getName()+"获取到锁");
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        System.out.println(Thread.currentThread().getName()+"结束运行");
    };

    /**
     * tryLock(long time, TimeUnit unit)用法
     */
    private static Runnable runnable2 = () -> {
        System.out.println(Thread.currentThread().getName()+"运行");
        try {
            /**
             * tryLock(long time, TimeUnit unit)
             * 如果在给定的等待时间内,其他线程没有持有该锁,并且当前线程没有被中断,则获取该锁。
             * 由于有超时时间,可以避免死锁
             */
            if (lock.tryLock(1, TimeUnit.SECONDS)){
                try {
                    System.out.println(Thread.currentThread().getName()+"获取到锁");
                    TimeUnit.SECONDS.sleep(2);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }else {
                System.out.println(Thread.currentThread().getName()+"未获取到锁");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束运行");
    };

    public static void main(String[] args) {
        //new Thread(runnable1).start();
        //new Thread(runnable1).start();

        new Thread(runnable2).start();
        new Thread(runnable2).start();
    }
}

lockInterruptibly() 线程等待锁期间可响应中断

class LockInterruptiblyDemo{
    private static Lock lock = new ReentrantLock();

    private static Runnable runnable = () -> {
        System.out.println(Thread.currentThread().getName()+"运行");
        try {
            /**
             * 线程等待锁期间可以响应中断
             */
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName()+"获取到锁");
                while (true){
                }
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"等待锁期间被中断");
            e.printStackTrace();
        }
    };

    public static void main(String[] args) throws Exception{
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        TimeUnit.MILLISECONDS.sleep(10);
        thread2.start();
        TimeUnit.MILLISECONDS.sleep(10);
        // thread2此时等待锁,处于BLOCKED状态,设置中断标记位
        thread2.interrupt();
    }
}

ReentrantLock 可重入性质

/**
 * 可重入锁:线程持有锁期间,该线程可以反复获取锁
 */
class ReentrantLockRecursion{
    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource(){
        lock.lock();
        try {
            /**
             * getHoldCount()当前线程持锁次数
             */
            if (lock.getHoldCount() < 5){
                System.out.println("递归前重入次数:" + lock.getHoldCount());
                accessResource();
                System.out.println("递归后重入次数:" + lock.getHoldCount());
            }
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        accessResource();
    }
}

ReentrantReadWriteLock 读写锁

读写锁规则:

    1、多个线程只申请读锁,可以申请到。
    2、一个线程持有读锁,其他线程申请写锁,申请锁的线程会阻塞,直到读锁被释放。
    3、一个线程持有写锁,其他线程申请读锁或写锁,申请锁的线程会阻塞,直到写锁释放。

总结:读锁允许同一时刻被多个线程持有,写锁同一时刻只能被一个线程持有;读锁与写锁互斥,同一时刻不能同时被持有。

从排他性、共享性的角度分类。写锁属于排他锁(又称独占锁),同一时刻只能被一个线程持有。写锁属于共享锁,可以被多个线程同时持有。

class ReadWriteLockDemo{

    // 创建读写锁
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    // 返回读锁
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    // 返回写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    /**
     * 读取资源用读锁
     */
    private static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到读锁");
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readLock.unlock();
        }
    }

    /**
     * 修改资源用写锁
     */
    private static void write(){
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到写锁");
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws Exception{
        // 多个线程可同时读取数据
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();

        TimeUnit.MILLISECONDS.sleep(10);

        /**
         * 同一时刻只有一个线程能写入数据
         * 并且数据正在被读取时也无法写入数据,因为读锁与写锁也是互斥的
         */
        new Thread(() -> write()).start();
        new Thread(() -> write()).start();

    }
}

公平锁与非公平锁

公平锁:多线程下,线程按照请求锁的顺序得到锁,先请求锁的线程先得到锁。

非公平锁:多线程下,线程不按照请求锁的顺序得到锁。

ReentrantReadWriteLock可创建公平锁,也可创建非公平锁,其内部维护一个先进先出的队列,此队列保存因等待锁而阻塞的线程。

class FairLockDemo{
    /**
     * 创建公平的读写锁。
     * ReentrantReadWriteLock 默认是非公平的
     */
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到读锁");
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readLock.unlock();
        }
    }

    private static void write(){
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到写锁");
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }

    /**
     * 公平锁,先请求锁的线程先得到锁
     */
    public static void main(String[] args) throws Exception{
        // 还没有线程持有锁,线程1能获取到读锁
        new Thread(() -> read(), "线程1").start();
        TimeUnit.MILLISECONDS.sleep(10);

        // 线程1持有读锁,线程2请求读取,线程2也能直接获取读锁不需要阻塞
        new Thread(() -> read(), "线程2").start();
        TimeUnit.MILLISECONDS.sleep(10);

        // 线程3请求写锁,但读锁被其他线程持有,线程3阻塞并进入队列排队
        new Thread(() -> write(), "线程3").start();
        TimeUnit.MILLISECONDS.sleep(10);

        /**
         * 线程4请求请求读锁,此时即便线程1、2持有的是读锁,
         * 但由于线程3在排队,为了体现公平性,线程4不能早于线程3执行,所以线程4也会被放进队列中
         */
        new Thread(() -> read(), "线程4").start();
        TimeUnit.MILLISECONDS.sleep(10);

        new Thread(() -> write(), "线程5").start();
    }
}

new ReentrantReadWriteLock(true); 在构造函数中创建一个公平锁对象new FairSync(),FairSync部分源码:

/**
 * ReentrantReadWriteLock.FairSync 公平锁部分源码
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    /**
     * 锁被释放的瞬间刚好有一个线程请求写锁,当前的写线程是否需要阻塞
     * 如果当前线程之前有一个排队的线程,则返回true,需要阻塞;如果当前线程位于队列的开头或队列为空,则返回false。
     */
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }

    /**
     * 读线程与写线程的规则一样
     */
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

假设这样的场景:线程1获取到读锁,线程2请求写锁,读锁写锁互斥,线程2阻塞并进入等待队列;线程1释放锁,此时刚好线程3请求锁(读锁、写锁皆可),由于把线程2从阻塞状态唤醒是需要消耗CPU资源的,为了提高效率,可以让当前请求锁的线程3得到锁,避免了“唤醒线程2,设置线程3为阻塞”的资源消耗。可以认为线程3没去队列中排队,直接获取锁运行了。

这种做法虽然提高了效率,但也有弊端。假如每次释放锁的时候,都刚好有线程请求锁,则等待队列中的线程会因得不到锁长时间等待,这种现象有个名字,叫“线程饥饿”。

为了既能最大程度利用计算机资源,又能避免线程饥饿,ReentrantReadWriteLock对非公平锁定义了两个规则

1、锁释放的瞬间,线程请求写锁,写线程可不进入等待队列,直接得到写锁。

2、锁释放的瞬间,线程请求读锁,当等待队列的头结点不是排他锁的时候,线程可不进入等待队列,直接得到锁。

new ReentrantReadWriteLock(); 在构造函数中创建一个非公平锁对象new NonfairSync(),NonfairSync部分源码:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    /**
     * 锁被释放的瞬间刚好有一个线程请求写锁,此线程可不阻塞
     */
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }

    /**
     * 锁被释放的瞬间,线程请求读锁
     * 如果等待队列头结点不是排他锁(对于读写锁来说,读锁不是排他锁),此线程可不阻塞。
     */
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

下面是一段演示抢锁的代码,可使用公平锁、非公平锁运行,查看两者的差异

class NonFairLockDemo{
    /**
     * 公平锁
     */
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);

    /**
     * 非公平锁
     */
    //private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到读锁");
            TimeUnit.MILLISECONDS.sleep(10);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readLock.unlock();
        }
    }

    private static void write(){
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到写锁");
            TimeUnit.MILLISECONDS.sleep(30);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        // 抢锁的线程
        Thread thread[] = new Thread[1000];
        for (int i = 0; i < 1000; i++) {
            // 写线程抢锁
            //thread[i] = new Thread(() -> write(), "新的写线程" + i);

            // 读线程抢锁
            thread[i] = new Thread(() -> read(), "新的读线程" + i);
        }

        // 排队的读锁线程
        Thread threadRead[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadRead[i] = new Thread(() -> read(), "排队的读线程" + i);
        }

        // 第一个运行的线程是写线程
        new Thread(() -> write(), "写线程一  ").start();
        TimeUnit.MILLISECONDS.sleep(5);

        // 读线程将在队列中排队
        for (int i = 0; i < 10; i++) {
            threadRead[i].start();
        }

        // 大量抢锁线程启动,在“写线程一”释放锁的瞬间有线程抢锁。
        TimeUnit.MILLISECONDS.sleep(10);
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                thread[i].start();
            }
        }).start();
    }
}

使用公平锁,抢锁线程是读线程,线程按照请求锁的顺序得到锁。使用非公平锁,抢锁线程是读线程,部分线程能抢锁,控制台打印如下,标红的线程比排队的线程更早打印,抢锁成功。

写线程一  得到写锁
新的读线程192得到读锁
新的读线程193得到读锁

排队的读线程1得到读锁
排队的读线程0得到读锁
新的读线程197得到读锁
新的读线程195得到读锁
新的读线程201得到读锁

使用公平锁,抢锁线程是写线程,线程按照请求锁的顺序得到锁。使用非公平锁,抢锁线程是写线程,部分线程能抢锁,控制台打印如下,标红的线程比排队的线程更早打印,抢锁成功。

写线程一  得到写锁
新的写线程95得到写锁
新的写线程996得到写锁

排队的读线程3得到读锁
排队的读线程7得到读锁
排队的读线程1得到读锁

还有一点要注意,ReentrantLock的API中提到tryLock()方法不支持公平性设置,官方说明如下:

Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.

还要注意,未定时的tryLock()方法不支持公平性设置。 如果锁定可用,即使其他线程正在等待,它将成功。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值