• 【Java并发】- 4.对Lock接口及其关联接口Condition的解析


    1.对Lock类的简单解析

    lock接口是java1.5开始添加入Java的,它是一个接口。其作用是替代synchronized关键字实现线程安全。

    synchronized关键字实现线程安全是通过JVM底层的Monitor锁来实现,但是在多线程环境下,如果线程进入阻塞状态就会涉及系统用户态和内核态的切换,这种切换极大的消耗量性能,所以从Java1.5开始引入Lock来解决synchronized上述的问题。

    Lock的锁是由java代码实现的,其运行在用户态下不涉及用户态与内核态的切换,可以比synchronized直接加锁的性能高。且Lock获取锁的方式也比synchronized更加多样化,更加灵活。

    我们来简单讲解一下lock接口的方法

    void lock();

    void lock();
    

    作用:获取锁,如果获取不到锁,线程就无法被调度,会陷入睡眠状态,直到获取到锁为止。类似于Object类的wait方法。

    void lockInterruptibly()

    void lockInterruptibly() throws InterruptedException;
    

    作用:也是获取锁,如果没有获取到锁,那么线程就会陷入等待,但是该方法有两种情况被唤醒

    • 线程获取到了锁,那么就会唤醒当前线程。
    • 如果其他线程中断了当前线程,而且这个中断操作是被允许,那么线程也就不会继续睡眠而是抛出InterruptedException异常。

    boolean tryLock();

    boolean tryLock();
    

    这种获取锁的方式在Lock中用的较多。

    如果能获取到锁,那么就会获取到锁并立即返回一个true。如果不能获取到锁那么这个方法也会立即返回一个false,不会像lock方法一样陷入睡眠状态。

    官方推荐的该方法的用法是:

     Lock lock = ...;
     if (lock.tryLock()) {
       try {
         // manipulate protected state
       } finally {
         lock.unlock();
       }
     } else {
       // perform alternative actions
     }
    

    这种写法可以确保如果能获取到锁,那么会正常解锁;如果获取不到锁,也不会去尝试解锁。

    boolean tryLock(long time, TimeUnit unit)

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    

    在等待的时间范围内,如果线程没有被中断,且锁是可以被获取的,那么就获取到锁。

    如果在等待时间内没有获取到锁,那么线程就会陷入睡眠状态,在一下三种情况下会被唤醒。

    • 1.在等待时间获取到了锁,那么线程被唤醒,方法返回true
    • 2.其他线程中断了这个线程,且这个中断操作是被允许的。线程会被唤醒被抛出一个异常。
    • 3.等待时间结束但是没有获取到锁,返回false

    void unlock();

    void unlock();
    

    就是解锁没有什么好说的。

    Condition newCondition();

    Condition newCondition();
    

    获取一个Condition对象,该对象的功能类似于Object类中的wait和notify方法。不过与之前说的synchronized与Lock的不同一样,Condition实现的await方法和singal方法使用起来也比Object类的wait和notify方法更加灵活。

    到处为止Lock中的方法就讲完了,下面说一下Lock的实现:

    Lock接口的主要实现

    在这里插入图片描述
    其实Lock还有一个接口也实现了同Lock类似的功能,不过因为该接口中有一些特殊实现不能直接实现或继承Lock所以单独写出来了一个接口ReadWriteLock
    在这里插入图片描述
    不过这篇文章只是讲解Lock的入门知识,所以不细讲上述的实现。

    下面写一个ReetrantLock锁使用的例子。多线程打印a++不出现重复值。

    public class MyTest1 {
    
        private Lock lock = new ReentrantLock();
        private int count;
    
        public void method1(){
            lock.lock();
            try {
                System.out.println("method1 print:"+count++);
            }finally {
                lock.unlock();
            }
        }
    
        public void method2(){
            lock.lock();
            try {
                System.out.println("method2 print:"+count++);
            }finally {
                lock.unlock();
            }
        }
    
    
        public static void main(String[] args) {
            MyTest1 test1 = new MyTest1();
    
            IntStream.range(0,10).forEach(i -> new Thread(()->{
                test1.method1();
            }).start());
            IntStream.range(0,10).forEach(i -> new Thread(()->{
                test1.method1();
            }).start());
        }
    }
    

    2.Condition类的解析

    传统上,我们可以通过synchronized关键字 + wait + notify/notifyAll 来实现多个线程之间的协调与通信,整个过程都是由JVM来帮助我们实现的;开发者无需(也是无法)了解底层的实现细节

    从JDK 5开始,并发包提供了Lock,Condition(await与signal/signalAll)来实现多个线程之间的协调与通信,整个过程都是由开发者来控制的,而且相比于传统方式,更加灵活,功能也更加强大

    Thread.sleep与await(或是Object的wait方法)的本质区别:sleep方法本质上不会释放锁,而await会释放锁,并且在signal后,还需要重新获得锁才能继续执行(该行为与Object的wait方法完全一致)

    Condition接口与Lock接口的关系

    上面说过Condition与Lock就与synchronized关键字 + wait + notify/notifyAll 的作用相同。而执行wait方法的线程就会进入一个等待集合,Condition就是实现了与等待集合(WaitSet)相似的作用。

    所以上面的Lock规定了获取Condition对象方式。因为Lock是锁对象,而Condition对象必须和Lock对象进行绑定才能使用。即一个Condition对象必须要与一个Lock对象配合使用,而Lock对象不必须生成Condition对象

    故Lock与synchronized的一点不同也体现出来了。Synchronized的中锁对象的等待集合是由c++实现的,一个锁只能有一个Waitset,但是Condition是由Lock实现的Lock对象可以生成多个Condition,来存放不同类型的等待集合,这也是Lock比较Synchronized灵活的一个方面。

    Java文档中给出了使用Condition的规范参考:

    class BoundedBuffer {
         final Lock lock = new ReentrantLock();
         final Condition notFull  = lock.newCondition(); 
         final Condition notEmpty = lock.newCondition(); 
      
         final Object[] items = new Object[100];
         int putptr, takeptr, count;
      
         public void put(Object x) throws InterruptedException {
           lock.lock();
           try {
             while (count == items.length)
               notFull.await();
             items[putptr] = x;
             if (++putptr == items.length) putptr = 0;
             ++count;
             notEmpty.signal();
           } finally {
             lock.unlock();
           }
         }
      
         public Object take() throws InterruptedException {
           lock.lock();
           try {
             while (count == 0)
               notEmpty.await();
             Object x = items[takeptr];
             if (++takeptr == items.length) takeptr = 0;
             --count;
             notFull.signal();
             return x;
           } finally {
             lock.unlock();
           }
         }
       }
    

    不过我们实现该功能的情况常常不多,因为如果要在项目中使用,Condition通常是依据AQS中的ConditionObject实现类使用,不必我们开发者动手编写相关代码。

    下面介绍一下Condition中的方法

    void await() ;

    void await() throws InterruptedException;
    

    将线程陷入等待状态,并释放锁对象,进入等待的线程在一下四种情况会被唤醒:

    • 1.其他线程执行了signal方法且被唤醒的线程恰好是该线程
    • 2.其他线程执行了signalAll方法。
    • 3.陷入睡眠的线程,被其他线程中断,这不是正常的唤醒,被唤醒的线程抛出InterruptedException异常。
    • 4.假唤醒发生,线程被唤醒。

    如果线程是被正常唤醒,那么只有等到线程再次获取锁,await方法才会返回。

    如果线程的状态被标记为Interrupted或线程被其他线程中断则线程会被异常唤醒并抛出InterruptedException异常

    void awaitUninterruptibly();

    void awaitUninterruptibly();
    

    该方法作用与await处该方法不会对中断做出响应意外其他都相同。这里不做详细说明。

    long awaitNanos(long nanosTimeout)

    long awaitNanos(long nanosTimeout) throws InterruptedException;
    

    这个方法和await方法使用也大致相同,不过与await方法不同的是该方法会在一个纳秒的时间内等待,如果超时线程也会被唤醒。

    在正常被signal唤醒时:该方法的返回值=nanosTimeout - 睡眠使用的时间
    如果返回值<=0,说明超时了,线程在给定等待时间内未能被唤醒

    官方给出的该方法的使用范例如下:

     boolean aMethod(long timeout, TimeUnit unit) {
       long nanos = unit.toNanos(timeout);
       lock.lock();
       try {
         while (!conditionBeingWaitedFor()) {
           if (nanos <= 0L)
             return false;
           nanos = theCondition.awaitNanos(nanos);
         }
         // ...
       } finally {
         lock.unlock();
       }
     }
    

    这里采用传入一个纳秒值作为等待时间是为了在线程被正常唤醒时可以明确其等待使用了多长时间的一个较为准确的值。当然也可以自行定义await方法等待时间的单位,这个方法就是下一个要讲的方法。

    boolean await(long time, TimeUnit unit)

    boolean await(long time, TimeUnit unit) throws InterruptedException;
    
    • time:等待的时常
    • unit:等待的时间单位
      该方法与前一个方法使用类似,只是时间的准确度上有所不同。也不展开讲解。

    boolean awaitUntil(Date deadline)

    boolean awaitUntil(Date deadline) throws InterruptedException;
    

    该方法也是最开始await方法的一个变种,该方法要求给出一个准确的日期,

    • 如果在给出的日期前线程被唤醒,该方法反回true,
    • 如果在给定日期后线程被唤醒,那么方法返回false

    官方给出的范例如下:

    boolean aMethod(Date deadline) {
       boolean stillWaiting = true;
       lock.lock();
       try {
         while (!conditionBeingWaitedFor()) {
           if (!stillWaiting)
             return false;
           stillWaiting = theCondition.awaitUntil(deadline);
         }
         // ...
       } finally {
         lock.unlock();
       }
     }
    

    void signal();

    void signal();
    

    该方法唤醒一个正在等待的线程,该线程获取到锁才能从Await方法返回。

    void signalAll();

    void signalAll();
    

    唤醒所以等待的线程,被唤醒的所有线程也是只有在获取到锁后才能从await方法返回。

    关于Condition接口的方法就讲的这里。

    3.Condition的实现ConditionObject
    Condition接口虽然在AbstractQueuedLongSynchronizer和AbstractQueuedSynchronizer类中都被实现,不过二者实现的方式一直,所有其实在Java在Condition接口只有一种实现方式。

    具体Condition接口的实现ConditionObject类的解析后续完成

    使用Lock和Condition解决生产者消费者问题

    public class MyTest2 {
    
        public static void main(String[] args) {
            BoundedContainer container = new BoundedContainer();
    
            IntStream.range(0,10).forEach(i -> new Thread(()->{
                container.put("wf");
            }).start());//这是对Java8新特性的使用
    
            IntStream.range(0,10).forEach(i -> new Thread(()->{
                container.take();
            }).start());
        }
    }
    
    class BoundedContainer{
    
        private String[] list = new String[10];
        private Lock lock = new ReentrantLock();
        private Condition notEmptyCondition = lock.newCondition();
        private Condition notFullCondition = lock.newCondition();
        private int counter;//数组中元素个数
        private int putIndex;
        private int takeIndex;
    
        public void put(String s){
            try {
                lock.lock();
                while (counter == list.length){
                    try {
                        notFullCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list[putIndex] = s;
                putIndex = ++putIndex == list.length ? 0:putIndex;//索引越界处理
                counter++;
                System.out.println("put method:" + Arrays.toString(list));
                notEmptyCondition.signal();
            }finally {
                lock.unlock();
            }
        }
    
        public String take(){
            try {
                lock.lock();
                while (counter == 0){
                    try {
                        notEmptyCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                String s = list[takeIndex];
                list[takeIndex] = null;
                takeIndex = ++takeIndex == list.length ? 0:takeIndex;
                counter--;
                System.out.println("take method:" + Arrays.toString(list));
                notFullCondition.signal();
                return s;
            }finally {
                lock.unlock();
            }
        }
    }
    
  • 相关阅读:
    Goahead在linux环境下安装部署
    vim卡住怎么办
    Clickhouse 实现 row number功能
    JavaScript ES6 模块化
    MySQL012事务的四个基本特征是什么
    MySQL015简述mysql中索引类型有哪些,以及对数据库的性能的影响
    MySQL010MySQL执行计划怎么看
    JavaScript ES6 Promise
    MySQL011如何处理MySQL的慢查询
    MySQL009MySQL为什么需要主从复制和读写分离
  • 原文地址:https://www.cnblogs.com/wf614/p/13168593.html
Copyright © 2020-2023  润新知