显示锁和AQS

一.什么是原子操作?如何实现原子操作?

原子操作即为操作的最小单元,比如i=1,这样一个简单的赋值操作便是原子操作。再例如i=i+1就不是一个原子操作,因为这个语句包括了读取i,i+1,将结果写入内存。这三个操作单元。保证原子操作即保证这三个单元在操作中具有原子性,也就是保证这个操作单元在操作中,不能因线程竞争等情况而被打断。如果操作单元被打断,那么计算结果会发生变化,也就无法保证线程的安全。

那么如何实现原子操作?大家也许会第一时间想到synchronized,它的本质是阻塞锁,执行持有锁的线程,阻塞其它线程的执行。这样在多且小的原子操作中会面临几个问题。(1)被阻塞的线程优先级很高怎么办?(2)拿到锁的原子操作不释放锁怎么办?(3)因为多,可能造成大量的竞争,消耗cpu的性能而且可能带来死锁和线程安全问题。那么CAS是一个很好的选择。

二.CAS

CAS,Compare And Swap,即比较并交换。
CAS原理:利用计算机处理器都有的CAS指令,进行循环,直到成功为止

CAS工作:CAS会有三个操作数,分别是内存值V(Java中可以理解为地址),旧的预期值A(在计算前保存内存的值),以及新的值B(新的运算出来的值).首先会将就预期值和内存值V进行比较,相同才会更新内存值为新值B,否则会一直进行循环。直到相同后更新为止。但是无论是否更新了V的值,都会返回V的旧值,这个处理过程是一个原子操作,由硬件来保证。

CAS面临的问题:
(1)循环时间太长
如果CAS一直不成功呢?这种情况绝对有可能发生,如果CAS自旋长时间地不成功,则会给CPU带来非常大的开销。
(2)只能保证一个共享变量原子操作
看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了。
(3)ABA问题
CAS中存在这样一种场景:如果一个变量V初次读取的时候是A值,如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS检查就会误认为它从来没有被改变过,但是实质上它已经发生了改变,这就是CAS操作的"ABA"问题。就像你倒了一杯水放在桌子上后去上了个厕所,同学给你喝了然后又给你接了一杯,这样你会误认为这杯就是你刚才接好的。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

在这里插入图片描述

更新基本类型AtomicInteger.getAndIncrement()与atomicInteger.incrementAndGet()极易产生理解偏差。一个是先获取值后再进行+1,一个是先+1后再获取。

public class TestAutomicIntege {
    //初始化一个10的变量
    public static AtomicInteger atomicInteger=new AtomicInteger(10);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.incrementAndGet());
    }
}

输出结果
10
12

更新引用类型中,AtomicMarkableReference、AtomicstampedReference。一个是判断变量是否被更新过返回boolean,一个是判断更新了多少次。

例:

public class TestAutomicReference {
    public static void main(String[] args) throws InterruptedException {
        //第一个参数是reference(引用,强引用,弱引用等) ,第二个参数是初始化版本号
        AtomicStampedReference asr=new AtomicStampedReference("sjw",0);
        //先记录下更新前的时间版本戳
        int oldStamp=asr.getStamp();
        //记录下更新前的引用,以便作为期望引用
        Object oldReference= asr.getReference();

        //进行一个正确的数据更新(期望值和期望版本号不错误)
        Thread rightReference=new Thread(new Runnable() {
            @Override
            public void run() {
                //在更新方法.compareAndSet方法中第一个参数是期望引用对象,第二个是要更新到的值
                //第三个是期望版本号,第四个是想要更新到的值
                System.out.println("当前版本戳:"+oldStamp+"   当前引用:"+oldReference
                                   +"结果:"+asr.compareAndSet(oldReference,oldReference
                                   +"ringht",oldStamp,oldStamp+1));
            }
        });

       //进行一个线程,让其当前期望值与期望值不一致
        Thread errorReference=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前版本戳:"+asr.getStamp()+"    当前引用:"+asr.getReference()
                                +"结果:"+asr.compareAndSet(oldReference,oldReference+"error",
                                 oldStamp,oldStamp+1));

            }
        });

        rightReference.start();
        rightReference.join();
        errorReference.start();
        errorReference.join();
    }
}

输出结果:
当前版本戳:0 当前引用:sjw结果:true
当前版本戳:1 当前引用:sjwringht结果:false

AtomicInterArray变更属于在内部封装了个新的数组变更,不会更改最初原始数组的数据
例:

public class TestAtomicArray {
    public static void main(String[] args) {

        int[] arrys = {1, 2, 6, 9, 12};
        //实例化的时候把数组放入
        AtomicIntegerArray ata = new AtomicIntegerArray(arrys);
        //给索引为0的数组变化为3
        System.out.println(ata.getAndSet(0,3));
        //输出
        System.out.println(ata.get(0));
        System.out.println(arrys[0]);

    }
}

输出结果:
1
3
1

三、Lock

1.Lock锁和synchronized关键字的区别?

(1).Lock是一个类,而synchronized是一个关键字
(2).synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
(3).synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
(4).用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
(6).Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。如果说没有需要获取锁可以被中断,需要可以超时的获取锁,以及实现尝试获取锁这三种情况,那么建议使用synchornized

2.什么是公平锁和非公平锁?

公平锁:根据请求的时间的顺序,进行获取锁。
非公平锁:允许线程进行不按请求的顺序去获取锁,可以抢先获取锁。

例如:线程A获取锁执行后,因为B比A的请求时间晚了些,所以在A执行中,B的状态变成了阻塞,当A执行完释放锁后,阻塞转变需要时间,在转变的时候线程C发送了请求,这时它是没有阻塞状态的,所以抢先B获取了锁。所以非公平锁比公平锁在效率上更好,因为阻塞转变成可执行状态是需要时间的。
在这里插入图片描述

3.什么是可重入锁?

当一个锁允许一个获取锁的线程调用其子过程(递归等),即可代表这个锁是可重入的。

synchronized就是常见的可重入锁,调用其子过程时候,依旧可以进入获取锁,在Lock接口中,其实现类ReentrantLock也是可重入锁,前提是设置了允许可重入,如果没有设置则代表不可重入。
在这里插入图片描述

4.ReadWriteLock接口下的实现类ReentrantReadWriteLock是一个读写锁,什么是读写锁?什么时候用?

首先说一下排他锁,ReentrantLock和synchronized都是排他锁,同一时刻只允许一条线程获得锁进行操作,但是读写锁不同,它允许读的线程同时可以有多条获得锁进行访问,但是写锁访问的时候,所有读线程和写线程都会被阻塞在读多写少的环境下使用,可以有效的提高性能。

在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁
可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。

读写是如何在一个变量内,确定state呢?一个state整形变量是32个字节,它会分为32位字节,读占前16位,写状态占后16位。
在这里插入图片描述

测试读写锁和syn关键字在读写操作中的时间效率

读写锁锁定读写.class

public class Bean {
    private int age;
    private ReadWriteLock lock=new ReentrantReadWriteLock();
    //获取读锁
    private final Lock readLock=lock.readLock();
    //获取写锁
    private final Lock writeLock=lock.writeLock();

    //读操作
    public int getAge() {
        readLock.lock();
        try {
            return age;
        }finally {
            //释放锁
            readLock.unlock();
        }
    }

    //写操作
    public void setAge(int age) {
        writeLock.lock();
        try {
            this.age = age;
        }finally {
            writeLock.unlock();
        }
    }
}

syn关键字修饰读写线程.class

public class Bean {
    private int age;

    //读操作
    public synchronized int getAge() {
        return age;
    }

    //写操作
    public synchronized void setAge(int age) {
       this.age=age;
    }
}

运行读线程和写线程.class

public class TestReadAnaWrite {
    //读线程
    static class Read extends Thread{
        public void run(){
            try {
                sleep(60);
                new Bean().getAge();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //写线程
    static class Write extends Thread{
        public int age;
        public Write(int age){
            this.age=age;
        }
        public void run(){
            try {
                sleep(60);
                new Bean().setAge(age);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
           long start=System.currentTimeMillis();
        //读写比例10比1
            for (int i=0;i<200;i++){
            //记录开始时间
            new Read().start();
            for (int j=0;j<10;j++){
                new Write(j).start();
            }
            //记录结束时间
        }
        long end=System.currentTimeMillis();
        System.out.println("总共用时:  "+(end-start));
    }


}

读写锁用时
在这里插入图片描述

syn用时
在这里插入图片描述
在数据量越大效果会越明显。

5.Lock书写的范式?

采用 try{ } finally { }的格式。因为这样可以确保锁肯定会被释放

writeLock.lock();
        try {
            this.age = age;
        }finally {
            writeLock.unlock();
        }
四、配合Lock的condition接口

切记锁都是对同一对象而言的,如果new出一个对象去上锁,另外new出一个对象去开锁,这种开锁是无效的

condition接口中的方法,为Lock提供了线程的辅助功能,相当于wait()和notify()对于synchronized的作用。

(1)await() ,线程等待。
(2)awaitUninterruptibly(),线程等待不可被打断
(3)signal(),相当于notify()的功能。
(4)signalAll(),相当于notifyAll()的功能。

Lock+condition实现等待通知。
例:当改变数值大于100,启动signal,查看被await()的线程变化情况。

public class Express {
    //定义一个显示锁
    public Lock Kmlock=new ReentrantLock();
    public Lock SiteLock=new ReentrantLock();
    //定义两个condition接口,一个辅助KM线程,一个辅助Site线程
    public Condition Kmcondition=Kmlock.newCondition();
    public Condition Sitecondition=SiteLock.newCondition();
    public String site="chengdu";
    public int Km=100;

    //改变KM的方法
    public void changeKm() throws InterruptedException {
        Kmlock.lock();
        try {
            this.Km=101;
            //唤醒此锁的Kmcondition对应的等待线程
            Kmcondition.signal();
        }finally {
            Kmlock.unlock();
        }
    }

    public void changeSite(String site) throws InterruptedException {
        SiteLock.lock();
        try {
            this.site=site;
            //唤醒此锁的sitecondition对应的等待线程
            Sitecondition.signal();

        }finally {
            SiteLock.unlock();
        }
    }

    public void waitSite() throws InterruptedException {
        SiteLock.lock();
        try {
            while (site=="shanghai"||site.equals("shanghai")) {
                Sitecondition.await();
                System.out.println("地点线程被唤醒,启动...");
            }
        }finally {
            SiteLock.unlock();
        }
    }

    public void waitKm() throws InterruptedException {
        Kmlock.lock();
        try {
            while (this.Km<=100){
                Kmcondition.await();
                System.out.println("路程线程等待被唤醒,启动...");
            }
        }finally {
            Kmlock.unlock();
        }
    }


}

启动线程.class

public class Test {
    //切记锁都是对同一对象而言的,如果new出一个对象去上锁,另外new出一个对象去开锁,这种开锁是无效的
    public static Express express=new Express();
    public static void main(String[] args) throws InterruptedException {
        //启动三个修改路程命令线程
        for (int i=0;i<3;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        express.waitKm();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        //启动三个修改地点命令线程
        for (int i=0;i<3;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                       express.waitSite();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        Thread.sleep(100);
        express.changeKm();
    }
}

输出结果:
在这里插入图片描述

在syn关键字搭配wait()和notfiy()使用的时候,尽量用notifyAll()而不是notify(),但在显示锁中,尽可能使用sinal()而不是signalAll()。因为wait()相对于的是类,很可能这个方法间会互相影响,导致通知信号丢失(没有到达预期位置)。但是在显示锁中sinal会明确指定针对的是哪个锁,甚至一个锁可以new出很多个condication从而保证一个锁的不同的线程也可以精确的指定休眠和唤醒状态。所以没有这种顾虑。

五.LockSupport

LockSupport 工具可以帮助我们阻塞或唤醒一个线程,也是构建同步组件的基础工具。
AQS中 对于节点的阻塞和唤醒就是通过LockSupport的park和unpark实现的。

park():堆线程产生阻塞作用
unpark():对阻塞线程进行唤醒操作

六.AQS(AbstractQueuedSynchronizer)

AQS使用的设计模式是模板方法模式。
模板方法模式:由父类定义一个所谓的框架方法,这个方法会设置方法的运行流程。但是这些方法的具体实现交给子类进行实现。

下面笔者给大家举一个例子来介绍模板方法设计模式:加入一个公司想要实现一个邮件发送的程序,项目经理把抽象的流程抽象类写了出来,里面包含了这些方法的具体流程,但是实现的任务交给了项目组成员。如下sendMessage()就是一个框架方法。

public abstract class AbstractModel {
    //首先定义一个写邮件的功能
    public  abstract void write();
    //定义一个内容
    public abstract void context();
    //定义一个发送功能
    public abstract void send();
    //定义一个输出当前日期的功能
    public void date(){
        System.out.println(new Date());
    }

    //定义一个框架方法
    public  void sendMessage(){
        write();
        context();
        send();
        date();
    }
}

首先介绍一下独占锁和共享锁,独占锁也就是悲观锁,在某一时间段只可以被一个线程锁占有,典型的有synchronized 关键字,和ReentrantLock锁,共享锁,简而言之就是可以被多个线程去共享,典型的有ReentrantReadWriteLock的读锁。
独占锁和共享锁,在java源码中是如何实现的呢?独占锁state初始是0,当有线程获取的时候变为1,表示已经被获取,共享锁的state初始并不是0,而是一个可以被共享的量,比如是10,每个线程去获取它就会在初始值减去1,当0的时候,共享锁共享完毕,其它线程就会被阻塞。

AQS中的模板方法

独占式:
acquire()
acquireInterruptibly()
tryAcquireNanos()

共享式:
acquireShared()
acquireSharedInterruptibly()
tryAcquireSharedNanos()

独占式的释放方法:
release()

共享式的释放方法:
releaseShared()

需要实现的方法
独占式:
tryAcquire()

独占式释放:
tryRelease()

共享式:
tryAcquireShared()

共享式释放:
tryReleaseShared()

状态设置的方法(状态可以理解是否获得锁)
状态为1,代表锁被占用,状态为0代表锁没有被占用。
设置状态:
setState()

获取状态:
getState()

保证状态的原子性操作:
compareAndSetState()

7.自己实现一个类似ReentrantLock锁

首先我们先观察一下,ReentrantLock是怎么实现的,它首先继承于Lock,然后回创建一个内部类去继承AQS。
在这里插入图片描述

ReentrantLock是独占式锁,如果要实现独占锁,首先要实现独占锁的方法acquire()、tryRelease()、isHeldExclusively() (判断当前锁是否被占用)

实现自己的锁.class

public class MyLock implements Lock {
      static class syn extends AbstractQueuedSynchronizer{
        //覆写AQS判断是否占用的方法
        @Override
        protected boolean isHeldExclusively() {
            //状态1代表已占用,返回1
            return getState()==1;
        }

        @Override
        protected boolean tryAcquire(int arg) {
            //运用CAS去改变状态,独占锁获取前必须是不被占用的状态,所以期望值是0
            if (compareAndSetState(0,1)){
                //传入当前占入的线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            //如果已被占用,返回的是false
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            //说明当前线程没有被占用,也就无法释放
            if (getState()==0){
                throw new UnsupportedOperationException();
            }
            //让当前线程占用线程变为Null
            setExclusiveOwnerThread(null);
            //让当前状态变为0,那为什么设置的时候用CAS,而释放的时候不用呢?
            //因为拿锁的时候是众多线程竞争,但是释放的时候,只有拿到锁的才能释放,不存在竞争关系
            setState(0);
            return true;
        }

        //实现一个 Condition
        public Condition newCondition() {
            return new ConditionObject();
        }
    }
    private final syn syn=new syn();
    @Override
    public void lock() {
        //参数1代表独占式锁
        syn.tryAcquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        syn.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return syn.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return syn.tryAcquireNanos(1,time);
    }

    @Override
    public void unlock() {
        syn.release(1);
    }

    @Override
    public Condition newCondition() {
        return syn.newCondition();
    }
}

8.AQS的内部结构

在这里插入图片描述

首先AQS内部就是一个同步队列,数据结构底层是用双向链表进行储存的,在AQS的源码中,Node(节点)被重点写入。

在这里插入图片描述
在这里插入图片描述
CANCELLED:线程等待时间超时或者被中断,需要从队列中移除
SIGNAL:后续节点的等待状态,当前节点可以去通知后面的节点去运行
CONDITION :当前节点处于等待队列
PROPAGATE:共享状态,表示当前节点的状态要向后方节点传播

在这里插入图片描述
waitstatus:表示等待状态,前面所介绍的变量根据情况,赋值于waitstatus
prev:表示当前节点的前驱节点
next:表示当前节点的后继节点

同步队列的更新操作
在这里插入图片描述

当1号线程获取锁的时候,所有的等待线程会组成一个同步队列等待线程1去释放锁,当再有新的线程想要获取锁的时候,就会被加入到等待队列,增加尾节点。
当1号线程释放锁后,头节点2会被唤醒,让其脱离同步队列去获取锁。
增加尾节点才用CAS,因为获取锁是竞争关系,CAS会保证安全。但是设置首节点,因为只有这一个需要去获取,所以不需要CAS。
在这里插入图片描述

AQS流程
获取同步状态就相当于获取锁
前置节点在被获取锁前,也会同时去唤醒后面的节点,让其准备进入头节点位置。
在这里插入图片描述

9.Condition与AQS的联系

Condition本质上也是去维护一个队列,这个队列配合AQS队列去实现线程的等待和唤醒,但Condition维护的等待队列与AQS同步队列不同的是,它是单链表。

await方法只有获取锁后才可以调用,说明头节点如果调用了await方法就会被放置Condition的等待队列中,当期使用signal方法后,又会将其从等待队列拿出,放入同步队列的末尾。
在这里插入图片描述

为什么在syn关键字中,最好用notifyAll()而不是notify(),在condition中要用signal()而不是signalAll()?
因为在syn中,它也是存在等待队列的,但是只有一个等待队列,你无法保证你唤醒的线程就是你想要的线程(可能是实现其它功能的线程),而在condition中有多个队列,你可以指定哪个条件(也就是功能)唤醒,这样就无需用singnalAll(),用singnal()性能更好。
在这里插入图片描述

10.非公平锁和公平锁是如何设计的?

在公平锁中,会有一个hasQeuedPredeceessors()方法去判断同步队列前方是否还有节点,如果有,就会等前方节点去获取。
在这里插入图片描述

在这里插入图片描述

而非公平锁不会判断前方是否还有节点,只要判断当前锁是否被释放,释放后,直接去CAS获取锁,然后其它线程就会被阻塞。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值