zoukankan      html  css  js  c++  java
  • 深入理解java:2.3.2. 并发编程concurrent包 之重入锁/读写锁/条件锁

    重入锁
    Java中的重入锁(即ReentrantLock)   与JVM内置锁(即synchronized)一样,是一种排它锁。

    ReentrantLock提供了多样化的同步,比如有时间限制的同步(定时锁),可以被Interrupt的同步,即中断锁 (synchronized的同步是不能Interrupt的)等。

    在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,

    但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态

    Atomic原子操作,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。

    激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。

    但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
    所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。

    synchronized是在JVM层面上实现的,在代码执行时出现异常,JVM会自动释放锁定。

    但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

    try{
      renentrantLock.lock();
      // 用户操作
    } finally {
      renentrantLock.unlock();
    }

    ReentrantLock获取锁定与三种方式:
        a)  lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

        b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

        c) tryLock(long timeout,TimeUnit unit),   如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

        d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到获得锁定,或者当前线程被别的线程中断

    重入锁可定义为公平锁或非公平锁,默认实现为非公平锁

    公平锁是指多个线程获取锁被阻塞的情况下,锁变为可用时,最先申请锁的线程获得锁

    在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

    可通过在重入锁(RenentrantLock)的构造方法中传入true构建公平锁,如Lock lock = new RenentrantLock(true)


    非公平锁是指多个线程等待锁的情况下,锁变为可用状态时,哪个线程获得锁是随机的

    synchonized相当于非公平锁。可通过在重入锁的构造方法中传入false或者使用无参构造方法构建非公平锁。

    读写锁
    锁可以保证原子性和可见性。

    而原子性更多是针对写操作而言。对于读多写少的场景,一个读操作无须阻塞其它读操作,只需要保证读和写  或者 写与写  不同时发生即可。

    此时,如果使用重入锁(即排它锁),对性能影响较大。Java中的读写锁(ReadWriteLock)就是为这种读多写少的场景而创造的。

    实际上,ReadWriteLock接口并非继承自Lock接口,ReentrantReadWriteLock也只实现了ReadWriteLock接口而未实现Lock接口。

    ReadLock和WriteLock,是ReentrantReadWriteLock类的静态内部类,它们实现了Lock接口。


    一个ReentrantReadWriteLock实例包含一个ReentrantReadWriteLock.ReadLock实例和一个ReentrantReadWriteLock.WriteLock实例。

    通过readLock()和writeLock()方法可分别获得读锁实例和写锁实例,并通过Lock接口提供的获取锁方法获得对应的锁。


    读写锁的锁定规则如下:
    获得读锁后,其它线程可获得读锁而不能获取写锁
    获得写锁后,其它线程既不能获得读锁也不能获得写锁

    public class ReadWriteLockDemo {


      public static void main(String[] args) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();


        new Thread(() -> {
          readWriteLock.readLock().lock();
          try {
            System.out.println(new Date() + " Thread 1 started with read lock");
            try {
              Thread.sleep(2000);
            } catch (Exception ex) {
            }
            System.out.println(new Date() + " Thread 1 ended");
          } finally {
            readWriteLock.readLock().unlock();
          }
        }).start();


        new Thread(() -> {
          readWriteLock.readLock().lock();
          try {
            System.out.println(new Date() + " Thread 2 started with read lock");
            try {
              Thread.sleep(2000);
            } catch (Exception ex) {
            }
            System.out.println(new Date() + " Thread 2 ended");
          } finally {
            readWriteLock.readLock().unlock();
          }
        }).start();


        new Thread(() -> {
          Lock lock = readWriteLock.writeLock();
          lock.lock();
          try {
            System.out.println(new Date() + " Thread 3 started with write lock");
            try {
              Thread.sleep(2000);
            } catch (Exception ex) {
              ex.printStackTrace();
            }
            System.out.println(new Date() + " Thread 3 ended");
          } finally {
            lock.unlock();
          }
        }).start();
      }
    }
    执行结果如下
    Sat Jun 18 21:33:46 CST 2016  Thread 1 started with read lock
    Sat Jun 18 21:33:46 CST 2016  Thread 2 started with read lock
    Sat Jun 18 21:33:48 CST 2016  Thread 2 ended
    Sat Jun 18 21:33:48 CST 2016  Thread 1 ended
    Sat Jun 18 21:33:48 CST 2016  Thread 3 started with write lock
    Sat Jun 18 21:33:50 CST 2016  Thread 3 ended
    从上面的执行结果可见,thread 1和thread 2都只需获得读锁,因此它们可以并行执行。

    而thread 3因为需要获取写锁,必须等到thread 1和thread 2释放锁后才能获得锁。

    条件锁
    条件锁只是一个帮助用户理解的概念,实际上并没有条件锁这种锁。

    对于每个重入锁,都可以通过newCondition()方法绑定若干个条件对象。

    重入锁可以创建若干个条件对象,signal()和signalAll()方法只能唤醒相同条件对象的等待。
    一个重入锁上可以生成多个条件变量,不同线程可以等待不同的条件,从而实现更加细粒度的的线程间通信。

    Condition是个接口,基本的方法就是await()和signal()方法;

    Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
    调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

    Conditon中的await()对应Object的wait();
    Condition中的signal()对应Object的notify();
    Condition中的signalAll()对应Object的notifyAll()。

    原理

    ReentrantLock实现的前提就是AbstractQueuedSynchronizer,抽象队列同步器,简称AQS,是java.util.concurrent的核心。

    ReentrantLock中有一个抽象类Sync继承了AQS。

    我们知道Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。

    而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。

    但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

    1. 线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。
    2. 线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。
    3. 锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
    4. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。
    5. signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。


    所以:
    发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。

    只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。

    可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,

    Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。

    signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,

    而signalAll就是唤醒所有非CANCELLED节点线程,本质是将节点从Condition队列中取出来一个还是所有节点放到AQS的等待队列。

    尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

    阻塞当前的线程:调用JNI的 unsafe.park(false, 0L)

    操作队列时,调用JNI的unsafe.compareAndSwapInt(),循环CAS。

    compareAndSetHead(Node update) 利用CAS设置头Node
    compareAndSetTail(Node expect, Node update) 利用CAS设置尾Node
    compareAndSetWaitStatus(Node node, int expect, int update) 利用CAS设置某个Node中的等待状态
  • 相关阅读:
    绪论
    Linux回到上次目录
    松下伺服电机控制器参数设置
    更新github上的文件
    pytorch
    从本地上传文件到github
    Linux常用命令
    使用colab训练神经网络
    深度学习模型训练过程
    anaconda安装ubuntu20.4中
  • 原文地址:https://www.cnblogs.com/my376908915/p/6758681.html
Copyright © 2011-2022 走看看