美文网首页
锁的分类及锁接口和类(AQS)

锁的分类及锁接口和类(AQS)

作者: wuchao226 | 来源:发表于2021-04-25 17:21 被阅读0次

    Java 原生的锁——基于对象的锁,它一般是配合 synchronized 关键字来使用的。Java 在 java.util.concurrent.locks 包下,还为我们提供了几个关于锁的类和接口。它们有更强大的功能或更高的性能。

    synchronized 的不足之处

    • 如果临界区是只读操作,其实可以多线程一起执行,但使用 synchronized 的话,同一时间只能有一个线程执行
    • synchronized 无法知道线程有没有成功获取到锁
    • 使用 synchronized,如果临界区因为 IO 或者 sleep 方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待

    这些都是 locks 包下的锁可以解决的。

    锁的几种分类

    1、乐观锁与悲观锁

    乐观锁:
    乐观锁又称为“无锁”。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为 CAS 的技术来保证线程执行的安全性。

    由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁免疫死锁

    乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

    悲观锁:
    对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

    2、可重入锁和非可重入锁

    重入锁,就是支持重新进入的锁,即同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权,这个锁支持一个线程对资源重复加锁

    synchronized 关键字隐式的支持重进入。比如说,你在一个 synchronized 实例方法里面调用另一个本实例的 synchronized 实例方法,它可以重新进入这个锁,不会出现任何异常。ReentrantLock 在调用 lock()方法时,已经获取到锁的线程,能够再次调用 lock()方法获取锁而不被阻塞。ReentrantLock 的中文意思就是可重入锁。

    如果我们自己在继承AQS实现同步器的时候,没有考虑到占有锁的线程再次获取锁的场景,可能就会导致线程阻塞,那这个就是一个非可重入锁

    3、公平锁与非公平锁

    如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁, 也可以说锁获取是顺序的。

    ReentrantLock 提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高。

    非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿(有一些线程长时间得不到锁)的情况

    在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个锁已被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求 这个锁,那么 C 很可能会在 B 被完全唤醒之前获得、使用以及释放这个锁。这样 的情况是一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早地获得了锁,并且吞吐量也获得了提高。

    ReentrantLock 支持非公平锁和公平锁两种。

    4、读写锁和排它锁

    synchronized 用的锁和 ReentrantLock,都是排它锁。也就是说,这些锁在同一时刻只允许一个线程进行访问。

    读写锁在同一时刻可以允许多个读线程访问, 但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

    除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。

    在没有读写锁支持的(Java 5 之前)时候,如果需要完成上述工作就要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠 synchronized 关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程) 的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

    一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

    Java提供了 ReentrantReadWriteLock 类作为读写锁的默认实现,内部维护了两个锁:一个读锁,一个写锁。通过分离读锁和写锁,使得在读多写少的环境下,大大地提高了性能。

    ReentrantReadWriteLock 其实实现的是 ReadWriteLock 接口

    5、显式锁和隐式锁

    所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。

    隐式锁(Synchronized)是Java的关键字,当它用来修饰一个方法或一个代码块时,能够保证在同一时刻最多只有一个线程执行该代码。因为当调用 Synchronized 修饰的代码时,并不需要显示的加锁和解锁的过程,所以称之为隐式锁。
    显示锁(Lock)是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。

    在使用 lock 的时候,需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。获取锁方法:lock.lock()。释放锁:unlock方法。需要配合tyr/finaly语句块来完成。

    //隐式锁
    synchronized(锁对象){
        //任务代码
    }
    
    //显示锁
    Lock l = new ReentrantLock()
    @Override
    public void run(){
        l.lock;
        //任务代码
        try{
           
        }finaly{
            l.unlock;
        }
    }
    

    有关锁的一些接口和类

    Condition 接口

    任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上), 主要包括 wait()、wait(long timeout)、notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式。

    Lock接口中有一个方法是可以获得一个Condition:

    Condition newCondition();
    

    Condition 和 Object 的 wait/notify 基本相似。其中,Condition 的 await 方法对应的是 Object 的 wait 方法,而 Condition 的 signal/signalAll 方法则对应 Object 的 notify/notifyAll()。但 Condition 类似于 Object 的等待/通知机制的加强版。我们来看看主要的方法:

    方法名称 描述
    await() 当前线程进入等待状态直到被通知(signal)或者中断;当前线程进入运行状态并从 await() 方法返回的场景包括:(1)其他线程调用相同 Condition 对象的 signal/signalAll 方法,并且当前线程被唤醒;(2)其他线程调用 interrupt 方法中断当前线程;
    awaitUninterruptibly() 当前线程进入等待状态直到被通知,在此过程中对中断信号不敏感,不支持中断当前线程
    awaitNanos(long) 当前线程进入等待状态,直到被通知、中断或者超时。如果返回值小于等于0,可以认定就是超时了
    awaitUntil(Date) 当前线程进入等待状态,直到被通知、中断或者超时。如果没到指定时间被通知,则返回 true,否则返回 false
    signal() 唤醒一个等待在 Condition 上的线程,被唤醒的线程在方法返回前必须获得与 Condition 对象关联的锁
    signalAll() 唤醒所有等待在 Condition 上的线程,能够从 await() 等方法返回的线程必须先获得与 Condition 对象关联的锁
    Condition 使用范式
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
    
        private void conditionWait() throws InterruptedException {
            lock.lock();
            try {
                condition.wait();
            } finally {
                lock.unlock();
            }
        }
    
        private void conditionSignal() {
            lock.lock();
            try {
                condition.signal();
            } finally {
                lock.unlock();
            }
        }
    

    实例:

    public class ExpressCond {
        
        public final static String CITY = "ShangHai";
        private int km;/*快递运输里程数*/
        private String site;/*快递到达地点*/
        private Lock kmLock = new ReentrantLock();
        private Lock siteLock = new ReentrantLock();
        private Condition kmCond = kmLock.newCondition();
        private Condition siteCond = siteLock.newCondition();
    
        public ExpressCond() {
        }
    
        public ExpressCond(int km, String site) {
            this.km = km;
            this.site = site;
        }
    
        /* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
        public void changeKm() {
            kmLock.lock();
            try {
                this.km = 101;
                kmCond.signal();
                //kmCond.signalAll();
            } finally {
                kmLock.unlock();
            }
    
    
        }
    
        /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
        public void changeSite() {
            siteLock.lock();
            try {
                this.site = "BeiJing";
                siteCond.signal();//通知其他在锁上等待的线程
            } finally {
                siteLock.unlock();
            }
        }
    
        /*当快递的里程数大于100时更新数据库*/
        public void waitKm() {
            kmLock.lock();
            try {
                while (this.km < 100) {
                    try {
                        kmCond.await();
                        System.out.println("Check Site thread["
                                + Thread.currentThread().getId()
                                + "] is be notified");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                kmLock.unlock();
            }
    
    
            System.out.println("the Km is " + this.km + ",I will change db");
        }
    
        /*当快递到达目的地时通知用户*/
        public void waitSite() {
            siteLock.lock();
            try {
                while (this.site.equals(CITY)) {
                    try {
                        siteCond.await();//当前线程进行等待
                        System.out.println("check Site thread[" + Thread.currentThread().getName()
                                + "] is be notify");
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            } finally {
                siteLock.unlock();
            }
    
            System.out.println("the site is " + this.site + ",I will call user");
        }
    }
    

    ReentrantLock

    ReentrantLock 是一个非抽象类,它是 Lock 接口的 JDK 默认实现,实现了锁的基本功能。是一个”可重入“锁。内部有一个抽象类 Sync,是继承了 AQS,自己实现的一个同步器。ReentrantLock 内部有两个非抽象类 NonfairSyncFairSync,它们都继承了 Sync。分别是非公平同步器公平同步器的意思。这意味着 ReentrantLock 可以支持公平锁非公平锁

    通过看着两个同步器的源码可以发现,它们的实现都是”独占“的。都调用了 AOS的 setExclusiveOwnerThread 方法,所以 ReentrantLock 的锁的”独占“的,也就是说,它的锁都是排他锁,不能共享。

    在 ReentrantLock 的构造方法里,可以传入一个 boolean 类型的参数,来指定它是否是一个公平锁,默认情况下是非公平的。这个参数一旦实例化后就不能修改,只能通过 isFair() 方法来查看。

    实例:

    /**
     * 类说明:使用Lock的范例
     */
    public class LockCase {
        private Lock lock = new ReentrantLock();
        private int age = 10000;//初始10000
    
        private static class TestThread extends Thread {
    
            private LockCase lockCase;
    
            public TestThread(LockCase lockCase, String name) {
                super(name);
                this.lockCase = lockCase;
            }
    
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {//递增10000
                    lockCase.test();
                }
                System.out.println(Thread.currentThread().getName()
                        + " age =  " + lockCase.getAge());
            }
        }
    
        public void test() {
            lock.lock();
            try {
                age++;
            } finally {
                lock.unlock();
            }
        }
    
        public void test2() {
            lock.lock();
            try {
                age--;
            } finally {
                lock.unlock();
            }
        }
    
        public int getAge() {
            return age;
        }
    
        public static void main(String[] args) throws InterruptedException {
            LockCase lockCase = new LockCase();
            Thread endThread = new TestThread(lockCase, "endThread");
            endThread.start();
            for (int i = 0; i < 10000; i++) {//递减10000
                lockCase.test2();
            }
            System.out.println(Thread.currentThread().getName()
                    + " age =  " + lockCase.getAge());
        }
    }
    

    打印结果:

    endThread age =  17021
    main age =  10000
    

    ReentrantReadWriteLock

    它是 ReadWriteLock 接口的JDK默认实现。它与 ReentrantLock 的功能类似,同样是可重入的,支持非公平锁和公平锁。不同的是,它还支持读写锁

    之前提到锁(如 ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问, 但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升

    除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它 大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写 操作完成之后的更新需要对后续的读服务可见。

    在没有读写锁支持的(Java 5 之前)时候,如果需要完成上述工作就要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠 synchronized 关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程) 的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

    一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

    抽象类 AbstractQueuedSynchronizer

    队列同步器 AbstractQueuedSynchronizer(以下简称同步器或 AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。

    AQS 使用方式和其中的设计模式

    AQS 的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态,在 AQS 里由一个 int 型的 state 来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法 (getState()、setState(int newState)和 compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

    /**
     * The synchronization state.
     */
    private volatile int state;
    

    在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS 自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等)。

    同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。

    可以这样理解二者之间的关系:
    锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线 程并行访问),隐藏了实现细节;

    同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、 线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

    实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步 组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

    模板方法模式

    同步器的设计基于模板方法模式。模板方法模式的意图是,定义一个操作中 的算法的骨架,而将一些步骤的实现延迟到子类中。

    模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

    解决的问题

    • 提高代码复用性
      将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中
    • 实现了反向控制
      通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 & 符合“开闭原则”

    实例:
    步骤一:创建抽象模板结构(Abstract Class):炒菜的步骤

    public abstract class AbstractCake {
    
        // 模板方法,用来控制炒菜的流程 (炒菜的流程是一样的-复用)
        // 申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
        public final void cookProcess() {
            //第一步:倒油
            this.pourOil();
            //第二步:热油
            this.HeatOil();
            //第三步:倒蔬菜
            this.pourVegetable();
            //第四步:倒调味料
            this.pourSauce();
            //第五步:翻炒
            this.fry();
        }
    
        //定义结构里哪些方法是所有过程都是一样的可复用的,哪些是需要子类进行实现的
    
        //第一步:倒油是一样的,所以直接实现
        void pourOil() {
            System.out.println("倒油");
        }
    
        //第二步:热油是一样的,所以直接实现
        void HeatOil() {
            System.out.println("热油");
        }
    
        // 第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
        //所以声明为抽象方法,具体由子类实现
        abstract void pourVegetable();
    
        //第四步:倒调味料是不一样的(一个下辣椒,一个是下蒜蓉)
        //所以声明为抽象方法,具体由子类实现
        abstract void pourSauce();
    
        //第五步:翻炒是一样的,所以直接实现
        void fry() {
            System.out.println("炒啊炒啊炒到熟啊");
        }
    }
    

    步骤二:创建具体模板(Concrete Class),即”手撕包菜“和”蒜蓉炒菜心“的具体步骤

     //炒手撕包菜的类
     public class ConcreteClass_BaoCai extends AbstractCake{
        @Override
        void pourVegetable() {
            System.out.println("下锅的蔬菜是包菜");
        }
    
        @Override
        void pourSauce() {
            System.out.println("下锅的酱料是辣椒");
        }
    }
     //炒蒜蓉菜心的类
     public class ConcreteClass_CaiXin extends AbstractCake {
        @Override
        void pourVegetable() {
            System.out.println("下锅的蔬菜是菜心");
        }
    
        @Override
        void pourSauce() {
            System.out.println("下锅的酱料是蒜蓉");
        }
    }
    

    步骤3:客户端调用-炒菜了

    public static void main(String[] args) {
        //炒 - 手撕包菜
        AbstractCake cake=new ConcreteClass_BaoCai();
        cake.cookProcess();
        //炒 - 蒜蓉菜心
        AbstractCake cake1=new ConcreteClass_CaiXin();
        cake1.cookProcess();
    }
    

    结果输出:

    倒油
    热油
    下锅的蔬菜是包菜
    下锅的酱料是辣椒
    炒啊炒啊炒到熟啊
    倒油
    热油
    下锅的蔬菜是菜心
    下锅的酱料是蒜蓉
    炒啊炒啊炒到熟啊
    

    访问或修改同步状态的方法

    重写同步器指定的方法时,需要使用同步器提供的如下 3 个方法来访问或修改同步状态。

    • getState():获取当前同步状态。
    • setState(int newState):设置当前同步状态。
    • compareAndSetState(int expect, int update):使用 CAS 设置当前状态,该方 法能够保证状态设置的原子性。

    参考

    锁接口和类

    相关文章

      网友评论

          本文标题:锁的分类及锁接口和类(AQS)

          本文链接:https://www.haomeiwen.com/subject/lnlilltx.html