Java多线程学习笔记:ReentrantLock,Condition,ReentrantReadWriteLock

ReentrantLock

ReentrantLock和synchronized关键字一样可以用来实现线程之间的同步互斥,但是在功能是比synchronized关键字更强大而且更灵活。

ReentrantLock 整体结构如下图:

常用接口分析

构造器

  • ReentrantLock()创建一个 ReentrantLock的实例。
  • ReentrantLock(boolean fair)创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例

公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先lock的线程不一定先获得锁。

常用方法

  • getHoldCount()查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
  • getQueueLength()返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
  • getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
  • hasWaiters(Condition condition)查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
  • hasQueuedThread(Thread thread)查询给定线程是否等待获取此锁
  • hasQueuedThreads()是否有线程等待此锁
  • isFair()该锁是否公平锁
  • isHeldByCurrentThread()当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
  • isLock()此锁是否有任意线程占用
  • lockInterruptibly()如果当前线程未被中断,获取锁
  • tryLock()尝试获得锁,仅在调用时锁未被线程占用,获得锁
  • tryLock(long timeout TimeUnit unit)如果锁在给定等待时间内没有被另一个线程保持,则获取该锁

注意

每个lock()的调用都必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。

代码

public class ReentrantLockTest implements Runnable{

    private static ReentrantLock lock = new ReentrantLock();
    private static int index = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            lock.lock();
            try {
                index++;
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 这里循环10次只是为了结果更明显,并无特殊意义
        for (int i = 0; i < 10; i++) {
            ReentrantLockTest test = new ReentrantLockTest();
            Thread t1 = new Thread(test);
            Thread t2 = new Thread(test);

            t1.start();
            t2.start();
            // join()是等待线程结束
            t1.join();
            t2.join();
            System.out.println(index);
            index = 0;
        }
    }
}

运行结果

作为对比,我们把lock删除

代码,没用锁

for (int i = 0; i < 10000; i++) {
    lock.lock();
    try {
        index++;
    } finally {
        lock.unlock();
    }
}

改成

for (int i = 0; i < 10000; i++) {
    index++;
}

运行结果

公平锁

Lock锁分为:公平锁 和 非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。

所谓公平锁,就是按照时间先后顺序,使先等待的线程先得到锁,而且,公平锁不会产生饥饿锁,也就是只要排队等待,最终能等待到获取锁的机会。使用重入锁(默认是非公平锁)创建公平锁:

CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。

ReentrantLock通过在构造方法中传入true就是公平锁,传入false,就是非公平锁。

代码

public static void main(String[] args) {
    final Service service = new Service(true);  // true为公平锁,false为非公平锁

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("线程" + Thread.currentThread().getName()
                    + "运行了");
            service.serviceMethod();
        }
    };

    Thread[] threadArray = new Thread[10];
    for (int i = 0; i < 10; i++) {
        threadArray[i] = new Thread(runnable);
    }
    for (int i = 0; i < 10; i++) {
        threadArray[i].start();
    }
}

static public class Service {
    private ReentrantLock lock;

    public Service(boolean fair) {
        lock = new ReentrantLock(fair);
    }

    public void serviceMethod() {
        lock.lock();
        try {
            System.out.println("ThreadName=" + Thread.currentThread().getName()
                    + "获得锁定");
        } finally {
            lock.unlock();
        }
    }
}

运行结果

公平锁的运行结果是有序的。

把Service的参数修改为false则为非公平锁

final Service service = new Service(false);//true为公平锁,false为非公平锁

运行结果

非公平锁的运行结果是无序的。

锁申请等待限时

可以使用 tryLock()或者tryLock(long timeout, TimeUtil unit) 方法进行一次限时的锁等待。

前者不带参数,这时线程尝试获取锁,如果获取到锁则继续执行,如果锁被其他线程持有,则立即返回 false ,也就是不会使当前线程等待,所以不会产生死锁。

后者带有参数,表示在指定时长内获取到锁则继续执行,如果等待指定时长后还没有获取到锁则返回false。

代码

public class TryLockTest implements Runnable {

    private  static ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) {    // 等待1s
                Thread.sleep(2000); // 休眠2s
            } else {
                System.out.println(Thread.currentThread().getName() + "获取锁失败!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        TryLockTest test = new TryLockTest();
        Thread t0 = new Thread(test);
        Thread t1 = new Thread(test);
        t0.start();
        t1.start();
    }
}

运行结果

上述示例中,t0先获取到锁,并休眠2秒,这时t1开始等待,等待1秒后依然没有获取到锁,就不再继续等待,t1获取锁是失败,符合预期结果。

可重入性

当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。
具体概念就是:自己可以再次获取自己的内部锁。
Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。

Synchronized的重入代码

public class SysncronizedTest {
    public void method1() {
        synchronized (SysncronizedTest.class) {
            System.out.println("方法1获得了锁");
            method2();
        }
    }

    public void method2() {
        synchronized (SysncronizedTest.class) {
            System.out.println("方法2也获得了锁");
        }
    }

    public static void main(String[] args) {
        new SysncronizedTest().method1();
    }
}

运行结果

ReentrantLock的重入代码

public class ReentrantLockTest {
    private Lock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            System.out.println("方法1获得了锁");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public void method2() {
        lock.lock();
        try {
            System.out.println("方法2也获得了锁");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new ReentrantLockTest().method1();
    }
}

运行结果

上面便是ReentrantLock的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。

Condition

synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,ReentrantLock同样可以,但是需要借助Condition,且Condition有更好的灵活性,具体体现在:

  1. 一个Lock里面可以创建多个Condition实例,实现多路通知
  2. notify()方法进行通知时,被通知的线程时Java虚拟机随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知,这是非常重要的

常用方法分析

  • await()造成当前线程在接到信号或被中断之前一直处于等待状态。使当前线程处于等待状态,释放与Condtion绑定的lock锁,直到 singal()方法被调用后,被唤醒(若中断,就game over了),唤醒后,该线程会再次获取与条件绑定的 lock锁
  • awaitUninterruptibly()造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。相比较await()而言,不响应中断
  • awaitNanos(long nanosTimeout)在wait()的返回条件基础上增加了超时响应,返回值表示当前剩余的时间,< 0 ,则表示超时
  • await(long time, TimeUnit unit)同上,只是时间参数不同而已
  • awaitUntil(Date deadline)同上,只是时间参数不同而已
  • signal()唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  • signalAll()唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

代码

public static void main(String[] args) {
    final ReentrantLock reentrantLock = new ReentrantLock();
    final Condition condition = reentrantLock.newCondition();

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "在等待被唤醒");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "恢复执行了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    }).start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "抢到了锁");
                condition.signal(); // 释放锁
                System.out.println(Thread.currentThread().getName() + "唤醒其他等待的线程");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    }).start();
}

运行结果

Condition与监视器方法比较

在没有 Lock 之前,我们使用 synchronized 来控制同步,配合 Object 的 wait()、notify() 等一系列方法可以实现等待 / 通知模式

在 Java SE 5 后,Java 提供了 Lock 接口,相对于 synchronized 而言,Lock 提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活。下图是 Condition 与 Object 的监视器方法的对比(摘自《Java并发编程的艺术》):

在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。

synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程

tryLock和lock和lockInterruptibly的区别

  1. tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回false
  2. lock能获得锁就返回true,不能的话一直等待获得锁
  3. lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常

ReentrantLock 与 synchronized 的区别

  1. 与 synchronized 相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
  2. ReentrantLock 还提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock 更加适合。
  3. ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized 则一旦进入锁请求要么成功要么阻塞,所以相比 synchronized 而言,ReentrantLock会不容易产生死锁些。
  4. ReentrantLock 支持更加灵活的同步代码块,使用 synchronized 时,只能在同一个 synchronized 块结构中获取和释放。注意,ReentrantLock 的锁释放一定要在 finally 中处理,否则可能会产生严重的后果。
  5. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  6. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  7. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  8. 在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。
  9. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  10. Lock可以提高多个线程进行读操作的效率。

ReentrantLock 与 synchronized性能比较

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

ReentrantReadWriteLock(读写锁)

我们刚刚接触到的ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。

读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。

多个读锁之间不互斥读锁与写锁互斥写锁与写锁互斥只要出现写操作的过程就是互斥的。)。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

ReadWriteLock是JDK5开始提供的读写分离锁。读写分离开有效的帮助减少锁的竞争,以提升系统性能。用锁分离的机制避免多个读操作线程之间的等待。

如果在一个系统中读的操作次数远远大于写操作,那么读写锁就可以发挥明显的作用,提升系统性能

ReentrantReadWriteLock的特性

  1. 公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量上来看还是非公平优于公平
  2. 重进入:该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁也能够同时获取读锁
  3. 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级称为读锁

代码

public class ReadWriteLockTest {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = reentrantReadWriteLock.readLock();
    private static Lock writeLock = reentrantReadWriteLock.writeLock();

    public static void main(String[] args) {
        ReadThreadTest readThreadTest = new ReadThreadTest(readLock);
        WriteThreadTest writeThreadTest = new WriteThreadTest(writeLock);
        for (int i = 0; i < 20; i++) {
            new Thread(readThreadTest).start();
        }
        for (int i = 0; i < 10; i++) {
            new Thread(writeThreadTest).start();
        }
    }

    private static class ReadThreadTest extends Thread {
        private Lock lock;

        public ReadThreadTest(Lock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                ReadWriteLockTest.handleRead(lock);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class WriteThreadTest extends Thread {

        private Lock lock;

        public WriteThreadTest(Lock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                ReadWriteLockTest.handleWrite(lock);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
   
    public static void handleRead(Lock lock) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000); // 模拟读操作
            System.out.println("读操作");
        } finally {
            lock.unlock();
        }
    }

    public static void handleWrite(Lock lock) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000); // 模拟写操作
            System.out.println("写操作");
        } finally {
            lock.unlock();
        }
    }
}

运行结果

可以看到,这里读操作虽然比写操作数量多,但是读操作几乎是瞬间完成(因为读操作之间不阻塞),而写操作却一个一个的执行。

ReadThreadTest readThreadTest = new ReadThreadTest(readLock);
WriteThreadTest writeThreadTest = new WriteThreadTest(writeLock);

改成

ReadThreadTest readThreadTest = new ReadThreadTest(lock);
WriteThreadTest writeThreadTest = new WriteThreadTest(lock);

运行结果

读和写操作都是阻塞进行,效率低下

小结

读写锁还是很实用的,因为一般场景下,数据的并发操作都是读多于写,在这种情况下,读写锁能够提供比排它锁更好的并发性。

总结

  • Lock类也可以实现线程同步,而Lock获得锁需要执行lock方法,释放锁需要执行unLock方法
  • Lock类可以创建Condition对象,Condition对象用来是线程等待和唤醒线程,需要注意的是Condition对象的唤醒的是用同一个Condition执行await方法的线程,所以也就可以实现唤醒指定类的线程
  • Lock类分公平锁和不公平锁,公平锁是按照加锁顺序来的,非公平锁是不按顺序的,也就是说先执行lock方法的锁不一定先获得锁
  • Lock类有读锁和写锁,读读共享,写写互斥,读写互斥

参考

https://www.cnblogs.com/xrq730/p/4855155.html

https://blog.csdn.net/Somhu/article/details/78874634

https://www.jianshu.com/p/96c89e6e7e90

https://blog.csdn.net/qq_34337272/article/details/79714196

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值