并发包的基石

简介:

上一章节学习的是synchronzied关键字来解决了并发过程的内存可见性和竟态条件的问题。但是synchronzied关键字本身也遗留了一部分局限性,比如死锁问题和线程之间的协调只能有一个等待队列(条件队列),让其在开发复杂业务时,开发难度增大。当然有问题,就必然会有解决之道。这个章节我们来学习并发包的基石原子变量和CAS,以及显示锁和显示条件。

原子变量和CAS:

原子变量是什么呢?

下面先看段代码,在来解释原子变量是什么?

public class Counter {
    private int count;
    public synchronized  void incr(){
            count++;
        }
    public synchronized  int  getCount(){
            return count;
        }

对于count++ 这种操作,使用syhchronized 成本太高,需要先获得锁,最后还需要释放锁。获取不到锁的情况还需要等待,还有线程上下文的切换。而使用synchronized 来保证原子操作却可以使用原子变量来替代。

原子变量:

之前分析并发的条件基本是以变量作为条件,而原子变量就是提供了一部分原子操作方法的变量。通过这些方法对原子变量进行操作,而这些操作都是保证原子操作的。原子变量与synchronized锁相比,这种原子更新方式代表一种不同的思维方式。synchronized是悲观的,它假定更新是很可能发生冲突,所以要先获得锁,得到锁才能更新。原子变量的更新逻辑是乐观的,它假定更新冲突比较少但使用CAS更新,也就是进行冲突检查。如果冲突了,那也没关系,继续尝试就好。synchronzied代表一种阻塞式算法,得不到锁,进入等待队列,等待其它线程的唤醒,有上下文切换的开销。原子变量的更新是非阻塞式的,更新冲突的时候,它就重试,不会阻塞,不会有上下文切换的开销。对于大部分简单的操作,无论是低并发还是高并发,这种乐观非阻塞的方式的性能远高于悲观阻塞方式。
Java 并发包下有如下几种基本原子变量类型:
(1)AtomicInteger:原子Integer类型。
(2)AtomicLong:原子Long类型,常用来在程序中生成唯一的序列号。
(3)AtomicBoolean:原子Boolean类型,常用来表示一个标志位。
(4)AtomicReference:原子引用类型,用来原子类型更新复杂的类型。

原子变量基石方法:

compareAndSet(Object expect,Object update) 是一个非常重要的方法,比较并设置,我们以后简称为CAS。该方法有两个参数expect和update,以原子方式实现如下功能:如果当前值等于expect,则更新为update。否则不更新,如果更新成功返回true,否则返回false。

该方法的基础原理和思维:

 //AtomicInteger的基本原理和思维
    static class AtomicCustomInteger {
        //用volatile声明,用来保证内存的可见性
        private volatile  int value;

        AtomicCustomInteger(int value) {
            this.value = value;
        }

        public  final int incrementAndGet(){
            //代码主体是个死循环,先获取当前值current,计算期望值next, 然后调用CAS方式进行更新
            //如果更新没成功,说明value 被其它线程改了,则再去取最新值并尝试更新到成功为止。
            for(;;){
             int current = get();
             int next = current + 1;
             // compareAndSet()是怎么实现的?
             // private static final Unsafe U = Unsafe.getUnsafe();
             // public final boolean compareAndSet(int expectedValue, int newValue) {
             // return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
             //}
             //它是Sun的私有实现,从名字看,表示的也是'不安全',一般应用程序不应该直接使用。
             //原理上,一般计算机系统都在硬件层次上直接支持CAS指令。而JAVA的实现都会利用这些特殊指令。
             //从程序的角度看,可以将compareAndSet视为计算机的基本操作,直接接纳就好。
             if(new AtomicInteger().compareAndSet(current,next)){
                 return next;
             }
            }
        }

        public int get(){
            return value;
        }
    }

compareAndSet()方法底层是直接利用了计算机系统底层的命令去进行数据的 操作。

简单的使用AtomicInteger 原子变量实现的程序计数器:

/**
     *用AtomicInteger作为并发的一个程序计数器
     */
    static class AtomicIntegerDemo extends  Thread{
        private static  AtomicInteger atomicInteger = new AtomicInteger(0);

        @Override
        public void run() {
            for (int i = 0; i<1000; i++){
                atomicInteger.incrementAndGet();
                System.out.println(atomicInteger.get());
            }
        }
    }
  //测试以AtomicInteger并发程序计数器的结果是否正确
        int num = 1000;
        Thread [] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
              threads[i] = new AtomicIntegerDemo();
              threads[i].start();
        }
        for (int i  =0; i < num; i++) {
            threads[i].join();
        }
        AtomicIntegerDemo atomicIntegerDemo = new AtomicIntegerDemo();
        System.out.println(AtomicIntegerDemo.atomicInteger);

AtomicInteger 原子变量的一些基础方法:

//以AtomicInteger来作为例子进行探讨
        //AtomicInteger有两个构造,一个可以设置初始化值,调用无参构造那默认值就是0
        AtomicInteger atomicInteger = new AtomicInteger(1);
        //获取和设置AtomicInteger的值的方法是get()和set(int value)
        int value = atomicInteger.get();
        System.out.println(value);
        Thread thread = new Thread(() -> {
            atomicInteger.set(5);
        });
        Thread thread1 = new Thread(() -> {
            atomicInteger.set(3);
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        value = atomicInteger.get();
        System.out.println(value);

        //之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方式,部分方法如下:

        //(1)以原子方式获取旧值并设置新值
        value = atomicInteger.getAndSet(6);
        System.out.println(value);

        //(2)以原子方式获取旧值并给当前值加1
        value = atomicInteger.getAndIncrement();
        System.out.println(value);

        //(3)以原子方式获取旧值并给当前值减1
        value = atomicInteger.getAndDecrement();
        System.out.println(value);

        //(4)以原子方式获取旧值并给当前值加delta,任意加多少数
        value = atomicInteger.getAndAdd(10);
        System.out.println(value);

        //(5)以原子方式给当前值加1并获取新值
        value = atomicInteger.incrementAndGet();
        System.out.println(value);

        //(6)以原子方式给当前值减1并获取新值
        value = atomicInteger.decrementAndGet();
        System.out.println(value);

        //(7)以原子方式给当前值加delta,并获取新值
        value = atomicInteger.addAndGet(10);
        System.out.println(value);

CAS方式实现悲观阻塞式锁:

static class MyLock {
        private AtomicInteger atomicInteger = new AtomicInteger(0);

        //lock()和unLock()使用CAS方法更新,lock()只有在更新成功后才退出。实现了阻塞的效果。
        //不过这种阻塞方式过于消耗cpu
        public void lock(){
            //0表示未加锁,1表示加锁
            while(!atomicInteger.compareAndSet(0,1)){
                Thread.yield();
            }
        }

        public void unLock(){
            atomicInteger.compareAndSet(1,0);
        }

    }

这种方式的锁成本太高。

使用CAS方式更新产生的ABA问题:

使用CAS方式更细产生的问题是 :假设当前值为A,如果另外一个线程将A值先修改为B,后在改为A。当前前程的CAS操作无法判断当前值发生过变化。ABA是不是一个问题与程序的逻辑有关。如果确实有问题,解决的方法就是使用AtomicStampedReference,在修改值的同时插入一个时间戳,只有值和时间戳都相同才进行修改。

 //public boolean compareAndSet(V expectedReference,VneWReference,int expectedStamp,int new Stamp)
        Pair pair = new Pair(100,200);
        int stamp = 1;
        AtomicStampedReference<Pair> pairRef = new     AtomicStampedReference<>(pair,stamp);
        int newStamp = 2;
        //AtomicStampedReference在compareAndSet中需要同时修改两个值:一个是引用,一个是时间戳。
        //怎么实现原子性呢。实际上AtomicStampedReference会将两个值组合为一个对象,修改的是一个值。
        //Pair是AtomicStampedReference的内部类,有一个of()方法将值和时间戳合并成一个对象变成单个值去比较和修改
        pairRef.compareAndSet(pair,new Pair(200,200),stamp,newStamp);

显示锁:

显示锁Lock接口:相比synchronized关键字,ReentrantLock (Lock实现类)可以实现与synchronized相同的语义,而且显示锁底层是调用CAS实现支持非阻塞式方式获取锁,可以响应中断,可以限时,更为灵活。不过,synchronized的使用更为简单,写的代码更少,也更不容易出错。
synchronized:代表的是一种声明式的编程思想,程序员更多的是表达一种同步声明,由java系统负责具体实现,程序员不知道其实现细节。显示锁代表一种命令式编程思想,程序员实现所有细节。声明式编程的好处除了简单,还在于性能,在较新的版本的JVM上,ReentrantLock和synchronized的性能是接近的但Java编译器和虚拟机可以不断优化synchronized的实现,比如自动分析synchronized的使用。对于没有竞争的场景,自动省略对锁的获取和释放。总之能用synchronized就使用synchronized。
ReentrantLock类有两个构造方法:public ReentrantLock() 和 public ReentrantLock(boolean fair) 参数fair表示是否保证公平,不指定的情况下,默认为false,表示不保证公平。所谓公平是指,等待时间最长的线程优先获得 锁。保证公平会影响性能,一般也不需要。所以默认不保证公平,synchronized也是不保证公平的。

显示锁基本方法:

方法名返回值说明
lock()void当前线程获得锁,并阻塞到执行完毕
unlock()void当前线程释放锁
lockInterruptibly()void当前线程获得锁,并阻塞到执行完毕,但是支持中断,中断后抛出异常InterruptException
tryLock()boolean尝试获取锁,立即返回不阻塞,如果获取成功返回true,否则返回false
tryLock(long time,TimeUnit unit)boolean尝试获取锁,如果成功返回true,否则阻塞等待,等待的时间由执行的参数设置,在等待的时候响应中断,如果中断抛出InterruptException异常,如果在指定等待时间获得锁,返回true,否则返回false

使用显示锁实现程序计数器:

//比如之前的并发程序计数器,synchronized能实现,原子变量基于CAS能实现,现在显示锁基于CAS也可以实现。
    static class Counter{
        private volatile int count = 0;
        private final Lock lock = new ReentrantLock();

        public void incr(){
            lock.lock();
            try {
               count++;
            } finally {
                lock.unlock();
            }
        }

        public int getCount(){
            return count;
        }
    }

    static class CountThread extends Thread {
        Counter counter;

        public CountThread(Counter counter) {
            this.counter = counter;
        }

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                counter.incr();
            }
        }
    }
       Counter counter = new Counter();
        Thread[] threads = new Thread[1000];
        for (int i = 0; i< 1000; i++){
            threads[i] = new CountThread(counter);
            threads[i].start();
            threads[i].join();
        }
        System.out.println(counter.getCount());

显示锁解决死锁例子:

//银行转账死锁案例
    static class Account {
        private Lock lock = new ReentrantLock();
        private volatile double money;

        public Account(double money) {
            this.money = money;
        }
        public void add(double money){
            lock.lock();
            try{
                this.money += money;
            }finally {
                lock.unlock();
            }
        }

        public void reduce(double money){
            lock.lock();
            try{
                this.money -= money;
            }finally {
                lock.unlock();
            }
        }

        public double getMoney(){
            return money;
        }

    }
    //异常类
    static class NoEnoughMoneyException extends Exception{
        public NoEnoughMoneyException() {
            System.out.println("转账失败");
        }
    }
    //转账
    static class AccountMgr {
        private static  volatile NoEnoughMoneyException noEnoughMoneyException;
        //会产生死锁的转账
        public static void transfer(Account from,Account to,double money) throws NoEnoughMoneyException {
            from.lock.lock();
            try{
                to.lock.lock();
                try{
                    if(from.getMoney() > money){
                        from.reduce(money);
                        to.add(money);
                        System.out.println("执行成功");
                    }else{
                        throw noEnoughMoneyException;
                    }
                }finally {
                    to.lock.unlock();
                }
            }finally {
                from.lock.unlock();
            }
        }

        //不会发生死锁的转账,显示锁解决死锁问题
        public static void transfer1(Account from,Account to, double money) throws NoEnoughMoneyException {
            if(from.lock.tryLock()){
                try {
                    if (to.lock.tryLock()) {
                        try {
                            if (from.getMoney() > money) {
                                from.reduce(money);
                                to.reduce(money);
                                System.out.println("执行成功");
                            } else {
                                System.out.println("执行失败");
                                throw noEnoughMoneyException;
                            }
                        }finally {
                            to.lock.unlock();
                        }
                    }
                }finally {
                    from.lock.unlock();
                }
            }
        }

        //用synchronized关键字解决死锁问题
        private static Object lockA = new Object();
        private static Object lockB = new Object();
        public static void synchronizedTranfer(Account from,Account to,double money) throws NoEnoughMoneyException {
            synchronized (lockA){
                synchronized (lockB){
                    if(from.getMoney() > money){
                        from.reduce(money);
                        to.reduce(money);
                        System.out.println("转账成功!");
                    }else{
                        System.out.println("转账失败!");
                            throw noEnoughMoneyException;
                    }
                }
            }

        }
    }

   //演示死锁,随机出现死锁。当两个账户互相给对方转账时便会发生死锁
        int accountNum = 2;
        Account[] accounts = new Account[accountNum];
        for (int i = 0; i< accounts.length; i++){
            accounts[i] = new Account(new Random().nextInt(1000));
        }
        for (int i = 0; i<100;i++) {
            Thread thread = new Thread(() -> {
                try {
                    AccountMgr.synchronizedTranfer(accounts[new Random().nextInt(2)],
                            accounts[new Random().nextInt(2)], new Random().nextInt(10));
                } catch (NoEnoughMoneyException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }

显示条件:

上一章节我们学习了在synchronized代码块下线程之间通过wait和notify来进行协作。既然synchronized有对应的线程间协作的方式,那么显示锁也应该有,而显示锁线程之间相互协作是需要通过显示条件Condition对象里的await和signal方法。同时相比于wait和notify的协作模式只能有一个等待队列(条件队列)的局限,await和signal可以有多个等待队列。这样可以避免不必要的线程唤醒和检查,而且代码更加清晰。
await在进入等待队列后,会释放锁,释放cpu,当其他线程将它唤醒后,或者等待超时后,或发生中断异常后,它都需要重新获得锁。获得锁后,才会从await方法中退出。signal和signalAll与notify和notifyAll一样,调用它们得先获取锁,如果没有锁就会抛出lllegaMonitorStateException异常。signal和notify都是挑一个线程进行唤醒,被唤醒线程获得锁。 signalAll和notifyAll 都是唤醒所有线程,但是这些线程需要重新竞争锁, 但是signalAll唤醒的线程是需要获取的锁后才会从等待队列(这个等待是条件队列)返回,而notifyAll是直接从条件队列中删除直接到等待队列上去竞争锁。同时需要注意的是Condition 也有wait和notify方法,但是显示条件的 方法不能和wait和notify混合使用,不然会抛出异常。

显示条件的基础方法:

方法名返回值说明
await()void当期线程休眠,等待被唤醒,但是响应中断
awaitNanos(long nanosTimeout)long等待相对时间,但参数是纳秒,返回值是nanosTimeout 减去实际等待时间 ,响应中断
await(long time,TimeUint unit)boolean等待相对时间,如果由于等待超时返回false,否则返回true,响应中断
awaitUntil(Date deadline)boolean等待相对时间,如果由于等待超时返回false,否则返回true,响应中断
awaitUnInterruptibly()void该方法不响应中断结束,但当它返回时,如果等待过程中发生中断,中断标志位会被设置
signal()void唤醒等待队列里的任意一个线程 ,并获得锁,并从等待队列删除
signalAll()void唤醒等待队列的所有线程,让它们去竞争锁,获得锁线程移除等待队列

显示条件例子:

/**
     * 用显示条件作为协调的列子
     */
    static class DisplayThread extends Thread {
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
        private boolean fire = false;

        @Override
        public void run() {
            try {
                lock.lock();
                try {
                    while (!fire) {
                        condition.await();
                    }
                } finally {
                    lock.unlock();
                }
                System.out.println("fire");
            }catch (InterruptedException i){
                Thread.interrupted();
            }
        }

        public void fire(){
            lock.lock();
            try {
                this.fire = true;
                condition.signal();
            }finally {
                lock.unlock();
            }
        }
    }

    /**
     * 消费者生产者模式用显示条件去实现,可以避免不必要的检查和唤醒。
     * synchronized 下的wait和notify局限是只能有一个等待队列,这样每次
     * 唤醒都会唤醒所有线程来,同时也需要检查当前线程的条件。而显示条件
     * 可以有多个等待队列,当有多个等待条件时,可以用多个等待队列,代码清晰
     * 也可以减少不必要的唤醒和检查。
     */
    static class MyBlockingQueue<E> {
       private Queue<E> queue = null;
       private int limit;
       private Lock lock = new ReentrantLock();
       private Condition notFull = lock.newCondition();
       private Condition notEmpty = lock.newCondition();

        public MyBlockingQueue(int limit) {
            queue = new ArrayDeque<>(limit);
        }

        public void put(E e)throws InterruptedException {
            lock.lockInterruptibly();
            try {
                while (queue.size() == limit) {
                    notFull.await();
                }
                queue.add(e);
                notEmpty.signal();
            }finally {
                lock.unlock();
            }
        }

        public E take()throws InterruptedException {
            lock.lockInterruptibly();
            try{
                while (queue.isEmpty()){
                    notEmpty.await();
                }
                E e = queue.poll();
                notFull.signal();
                return e;
            }finally {
                lock.unlock();;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        DisplayThread displayThread = new DisplayThread();
        displayThread.start();
        Thread.sleep(1000);
        System.out.println("main is fire");
        displayThread.fire();
    }

总结:

本章节学习了原子变量、CAS、显示锁、显示条件。原子变量是通过CAS方式实现了一些原子操作的方法来对原子变量进行操作。这样在并发情况下操作原子变量相比于synchronized方式,可以减少锁的获取,释放锁,获取不到锁时需要等待时线程之间上下文的切换等成本,效率更高。CAS实现的原子变量是一种全新的思维方式,它是一种乐观的更新逻辑,它假定更新冲突少,但使用CAS去更新检查。如果冲突就重新尝试。而synchronzied是悲观的,它假定更新冲突多,所以要先获取锁,获取锁后才能更新。显示锁和显示条件是为了解决synchronized 的局限性,显示锁可以更灵活的解决死锁问题,显示条件可以有多个等待队列,使代码更清晰,协作条件更容易分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值