江老师的CAS与AQS学习笔记

CAS,compareAndSwap,见名知意,比较和交换。

CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。

比如对于ActomicInteger类型的原子类操作,都是基于汇编语言实现的原子操作,因为汇编语言都是按顺序一条一条执行的。对于多核cpu可能会产生误解,程序是由很多指令组成的,多个cpu会同时开启多个流水线执行,但是在单个的流水线上,cpu会将程序指令加以标记,按正确的指令顺序进行执行。

我们知道JUC(并发编程类包java.util.concurrent)下的atomic类都是通过CAS来实现的,下面就以AtomicInteger为例来阐述CAS的实现。如下:
请添加图片描述
上图分别是AtomicInteger和Integer的自增方法反编译后的class文件,这里AtomicInteger的自增只有一条语句,对应的调用如下:
请添加图片描述
我们可以看到,这里调用了Unsafe类的方法,这是一个后门类。

Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。

请添加图片描述
请添加图片描述
如上对于ActomicInteger的自增操作,底层汇编语句只有这样一条指令(前面是对指令进行加锁操作),自然是线程安全的,个人猜测即使是多条汇编语句,也不会乱吧。

一个简单的例子,对于非原子性的i++这类操作,并不是简单的一步操作,而对于ActomicInteger的incrementAndGet方法来说,其底层就是调用了一句汇编语言,所以是原子性的。

CAS缺点

CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。

对于什么是ABA问题,这里就不做过多的阐述了,可以看下这个链接https://blog.csdn.net/qq_32998153/article/details/79529704

用代码来表示如下:

public class CAS2 {
    private static volatile Integer m = 0;
    private static AtomicInteger atomicI = new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println(atomicI.compareAndSet(100,110));
        });
        t1.start();
        Thread t2 = new Thread(()->{
            System.out.println(atomicI.compareAndSet(110,100));
        });
        t2.start();
        Thread t3 = new Thread(()->{
            System.out.println(atomicI.compareAndSet(100,120));
        });
        t3.start();
    }
}

结果如下:
请添加图片描述
表面上看输出结果没什么问题,但是在一定的业务场景下就会产生ABA问题,所以针对于特定的场景,我们要避免ABA问题发生,可以用AtomicStampedReference相关的类给变量加个时间戳,在比较和交换的时候会同时比较时间戳,如果时间戳不相同,则更新不成功。代码如下 :

public class CAS2 {
    private static volatile Integer m = 0;
    private static AtomicInteger atomicI = new AtomicInteger(100);
    private static AtomicStampedReference as1 = new AtomicStampedReference(100,1);
    //这里有个坑,对于上面的as1来说,默认是int类型,使用compareAndSet方法时如果数值超过127,每次返回的结果都为false,所以最好使用下面的Integer泛型
    private static AtomicStampedReference<Integer> as2 = new AtomicStampedReference(100,1);

    public static void main(String[] args) throws InterruptedException {
        Thread tf1 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(as1.compareAndSet(100,110,as1.getStamp(),as1.getStamp()+1));
        });
        tf1.start();

        Thread tf2 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(as1.compareAndSet(110,100,as1.getStamp(),as1.getStamp()+1));
        });
        tf2.start();

        Thread tf3 = new Thread(()->{
            int stamp = as1.getStamp();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(as1.compareAndSet(100,120,stamp,stamp+1));
        });
        tf3.start();
    }
}

结果如下:
在这里插入图片描述
如上所示,每更新成功一次,时间戳就+1,最后一次更新拿到的时间戳是最初始的时间戳,所以更新失败。同时需要注意的是,AtomicStampedReference和 AtomicStampedReference类型的变量是有一定的区别的,区别如代码所示。

AQS:同步发生器

对于java程序来说,多个线程的执行会映射到内核线程,由cpu调用去执行,两者通过操作系统进行转换,操作系统内核线程接管用户线程(程序),最后通过cpu去调用。

基本思想:

通过内置的FIFO(先进先出)同步队列来完成线程争夺资源的管理工作。

CLH同步队列:

一种算法,AQS的核心。
请添加图片描述
一个node节点就代表一个线程,线程在等待的时候会进行自旋。

我们进入AbstractQueuedSynchronizer这个类(同步队列发生器),这里用volatile修饰了一个waitStatus属性,即上图的state,当state值为0,可以获取锁。
请添加图片描述

对于CAS和AQS来说,其作用是为了减小Synchronized锁的粒度制定的解决方法。

现在我们用AQS写一个自定义的锁,我们可以将AQS子类定义为非公共内部帮助器类(私有的内部类继承AQS),写锁的时候提供一个帮助器,提供获取锁和释放锁的功能、模板。
请添加图片描述请添加图片描述
acquire(int arg) 以独占模式获取对象,忽略中断

acquireShared(int arg) 以共享模式获取对象,忽略中断

tryAcquire(arg) 试图在独占模式下获取对象状态

tryAcquireShared(int arg) 试图在共享模式下获取对象状态

release(int arg) 以独占模式释放对象

releaseShared(int arg) 以共享模式释放对象

利用以上一些基本方法实现自定义锁代码,我们需要实现Lock接口,其基本方法如下:
请添加图片描述
实现代码如下:

public class MyLock implements Lock {

    private Helper helper=new Helper();

    private class Helper extends AbstractQueuedSynchronizer{
        //获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            int state=getState();
            if(state==0){
                //利用CAS原理修改state
                if(compareAndSetState(0,arg)){
                    //设置当前线程占有资源
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }else if(getExclusiveOwnerThread()==Thread.currentThread()){   //可重入锁
                setState(getState()+arg);
                return true;
            }
            return false;
        }

        //释放锁
        @Override
        protected boolean tryRelease(int arg) {
            int state=getState()-arg;
            boolean flag=false;
            //判断释放后是否为0
            if(state==0){
                setExclusiveOwnerThread(null);
                setState(state);
                return true;
            }
            setState(state);
            return false;
        }

        public Condition newConditionObjecct(){
            return new ConditionObject();
        }
    }
    @Override
    public void lock() {
        helper.acquire(1);
    }

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

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

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

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

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

使用上面的锁:

public class MyLockDemo1 {
    private MyLock lock=new MyLock();
    private int m=0;
    public int next() throws InterruptedException {
        Thread.sleep(2000);
        return m++;
    }

    public static void main(String[] args) {
        MyLockDemo1 demo=new MyLockDemo1();
        Thread[] th=new Thread[20];
        for (int i = 0; i < 20; i++) {
            th[i]=new Thread(()->{
                try {
                    System.out.println(demo.next());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            th[i].start();
        }
    }
}

首先我们不加锁,执行以上代码,结果如下所示:
请添加图片描述
可以看到,在多线程的情况下,不加锁会出现重复的数据,下面让我们加上自定义的锁:

public class MyLockDemo {
    private MyLock lock=new MyLock();
    private int m=0;
    public int next() throws InterruptedException {
        lock.lock();
        try {
        	//这里睡眠一段时间是为了保证输出的顺序性,可以不加
            Thread.sleep(500);
            return m++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyLockDemo demo=new MyLockDemo();
        Thread[] th=new Thread[20];
        for (int i = 0; i < 20; i++) {
            th[i]=new Thread(()->{
                try {
                    System.out.println(demo.next());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            th[i].start();
        }
    }
}

输出结果如下:
请添加图片描述
不存在重复性的问题,即保证了线程的顺序执行,这是通过AQS来实现的。

另外,我们自定义的锁也保证了锁的可重入性,即当一个线程已经拿到锁的情况下,当前线程再次进入程序,去获取同一把锁。这里我们做了这样一个判断

            if(state==0){
                //利用CAS原理修改state
                if(compareAndSetState(0,arg)){
                    //设置当前线程占有资源
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }else if(getExclusiveOwnerThread()==Thread.currentThread()){   //可重入锁
                setState(getState()+arg);
                return true;
            }

我们写个例子来验证自定义锁的可重入性:

public class MyLockDemo2 {
    private MyLock lock=new MyLock();
    public void a(){
        lock.lock();
        System.out.println("a");
        b();
        lock.unlock();
    }
    public void b(){
        lock.lock();
        System.out.println("b");
        lock.unlock();
    }

    public static void main(String[] args) {
        MyLockDemo2 demo=new MyLockDemo2();
        new Thread(()->{
            demo.a();
        }).start();
    }
}

如上所示,a方法获取锁,调用b方法,b方法又要去获取同一把锁,如果不满足可重入性,那么b方法是无法执行的,代码执行结果如下:
请添加图片描述
说明我们自定义的锁是满足可重入性的,同时,这里的锁我们完全可以用ReentrantLock锁,同样满足可重入性,所以实际开发中很少需要我们去自定义锁的情况。

并发工具类:

1、CountDownLatch

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有框架服务之后执行。

简单的说,CountDownLatch允许在多个线程执行完以后再执行其他代码,如果用传统的方法,需要让线程按顺序执行或者让线程之外的代码执行之前先要判断其他所有线程有没有全部执行完,无论用什么方法,这会让代码变得复杂或者代码效率更低,CountDownLatch就很好的解决了这个问题。

让我们直接看一个简单的案例:

public class FightQueryDemo {

    private static List<String> company = Arrays.asList("东方航空","南方航空","海南航空");
    //机票
    private static List<String> fightList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        String origin = "北京";
        String dest = "上海";

        //三个航班、三个线程同时查询机票
        Thread[] threads = new Thread[company.size()];
        CountDownLatch latch = new CountDownLatch(company.size());

        for (int i = 0; i < threads.length; i++) {
            String name = company.get(i);
            threads[i] = new Thread(()->{
                System.out.printf("%s 查询从%s到%s的机票\n",name,origin,dest);
                //随机产生机票数
                int val = new Random().nextInt(10);
                try {
                    TimeUnit.SECONDS.sleep(val);
                    fightList.add(name+"--"+val);
                    System.out.printf("%s公司查询成功!\n",name);
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
        //这里使用await方法等待latch值为零的时候再执行下面的方法,否则fightList初始为空没有输出
        latch.await();
        System.out.println("==============查询结果如下:================");
        fightList.forEach(System.out::println);
    }
}

输出结果如下:
请添加图片描述
这里latch.await()方法会等待CountDownLatch的值为零再执行下面的代码,第个线程执行完业务代码就调用latch.countDown()方法将CountDownLatch的值减一,直到为零就执行latch.await()后面的代码,这就是CountDownLatch的基本用法。相当于计数器

2、CyclicBarrier

CyclicBarrier与CountDownLatch用法很相似。

这里我们也看一个简单的案例:

public class RaceDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(8);
        Thread[] play = new Thread[8];
        for (int i = 0; i < 8; i++) {
            play[i] = new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(new Random().nextInt(10));
                    System.out.println(Thread.currentThread().getName()+"准备好了");
                    /**
                     * 这里和CountDownLatch用法有所不同,
                     * CountDownLatch每次执行线程方法需要调用countDown方法,直到值为零
                     * 最后调用await方法唤醒其他方法
                     * CyclicBarrier是在每个线程方法中调用await方法,直到一定的值才唤醒其他方法
                     */
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("选手"+Thread.currentThread().getName()+"起跑");
            },"play["+i+"]");
            play[i].start();
        }
    }
}

执行结果如下:
请添加图片描述
相关解释已经在代码中注明,自行理解。

3、Semaphore

字面意思就是信号量的意思,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

常用方法说明:

1、acquire()
从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,或者线程已被中断。

2、acquire(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。就好比是一个学生占两个窗口。这同时也对应了相应的release方法。

3、release(int permits)
释放给定数目的许可,将其返回到信号量。这个是对应于上面的方法,一个学生占几个窗口完事之后还要释放多少

4、availablePermits()
返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。

5、reducePermits(int reduction)
根据指定的缩减量减小可用许可的数目。

6、hasQueuedThreads()
查询是否有线程正在等待获取资源。

7、getQueueLength()
返回正在等待获取的线程的估计数目。该值仅是估计的数字。

8、tryAcquire(int permits, long timeout, TimeUnit unit)
如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。

9、acquireUninterruptibly(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。

简单的来说就是Semaphore指定了访问线程的数量,即信号量,在访问线程方法前通过acquire()等方法去获取一个或者多个信号量,即许可,只有获取许可才能执行后面的代码,否则线程将被阻塞。

举个简单的例子:

public class CarDemo {
    public static void main(String[] args) {
        //创建Semaphore
        Semaphore sp=new Semaphore(5);

        Thread[] car=new Thread[10];
        for (int i = 0; i < 10; i++) {
            car[i]=new Thread(()->{
                //请求许可
                try {
                    sp.acquire();
                    System.out.println(Thread.currentThread().getName()+"可以进停车场");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //使用资源
                try {
                    int val= new Random().nextInt(10);
                    TimeUnit.SECONDS.sleep(val);
                    System.out.println(Thread.currentThread().getName()+"停留了"+val+"秒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                //离开(释放资源)
                try {
                    sp.release();
                    System.out.println(Thread.currentThread().getName()+"离开停车场");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },"car["+i+"]");
            car[i].start();
        }


    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值