JAVA并发编程(六)——性能优化(下)

对于高并发的环境下,无论使用什么加锁策略,程序的性能始终不能与无锁的程序相媲美,那么是否存在一种不使用加锁也能实现各个线程之间同步的策略呢?答案当然是肯定的。那就是比较交换(CAS)策略。


  • CAS
  • AtomicInteger、AtomicLong
  • Unsafe
  • AtomicReference、AtomicStampedReference

CAS

CAS是比较交还的英文缩写,和使用锁相比,使用CAS会使得程序更为复杂,但是它是非阻塞的,所以天生对死锁免疫。除此之外,它使用无锁的方式,完全没有锁竞争带来的系统开销,也没有线程之间频繁调度带来的开销,性能更加出色。

CAS的算法原理是这样的:包含三个参数(Value,EexpectValue,NewValue),V表示需要更新的数值,E表示预期数值,N表示新值。当且仅当,V=E时,才会将V的数值改为N;当V!=E时,说明已经有人提前改动了,那么当前线程就不能在继续改动了并且返回当前的数值。当多个线程同时使用CAS改动一个对象时,只有一个线程会胜出,其余失败的线程不会被挂机,而是再次的尝试(很有毅力啊),当然也允许线程放弃。

简单来说,CAS需要一个你希望值,也就是你认为现在这个对象应该的样子,如果和你预想的不一样,说明有人已经改动过了,你就要重新读取,再次尝试修改。但是这种策略存在一个问题,假设有一个线程提把变量改变了一次,接着又变为原来的值;而后一个线程一看,变量还是原来的值,会误以为没有线程改动过,就继续执行。这在某些情况下,会造成程序的错误执行。
这里写图片描述

后面的内容会告诉我们如何解决这中问题。


AtomicInteger、AtomicLong

JDK中的并发包中有一个Atomic包,里面实现了使用CAS操作的线程安全的类型。最常用的就是AtomicInteger,他和Integer不同,是可变且线程安全的

public final int get();
public final void set(int newValue);
//以原子方式设置为给定值,并返回旧值。
public final int getAndSet(int newValue);
//如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
public final boolean compareAndSet(int expect,int update);
public final int getAndIncrement();//以原子方式将当前值加 1。
public final int getAndDecrement();//以原子方式将当前值减 1。
public final int getAndAdd(int delta);//给定值与当前值相加,返回旧值
public final int incrementAndGet();//以原子方式将当前值减 1。
public final int addAndGet(int delta);//给定值与当前值相加,返回新值
//更多的请看源码

AtomicInteger使用非常简单:

public class AtomicIntegerDemo {
    static AtomicInteger i = new AtomicInteger();
    public static class Add implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                AtomicIntegerDemo.i.incrementAndGet();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[10];
        for (int i = 0; i < 10; i++) {
            ts[i] = new Thread(new Add());
        }
        for (int i = 0; i < 10; i++) {
            ts[i].start();
        }
        for (int i = 0; i < 10; i++) {
            ts[i].join();
        }
        System.out.println(i);
    }
}
/*
out:
10000
*/

程序对i进行了10000次的加1,如果线程不安全,输出应该小于10000,从输出看说明程序执行正确。
我们关注一下incrementAndGet()的内部实现

 public final int incrementAndGet() {
        for (;;) {
            //返回数据
            int current = get();
            int next = current + 1;
            //CAS操作
            if (compareAndSet(current, next))
                return next;
        }
    }

对于第二行的for (;;);CAS并不是都是成功的,对于失败的情况,我们需要不停的尝试。如果有线程提前修改了值,那么compareAndSet(current, next)会返回false
AtomicLong实现原理也大同小异,这里不再累述。


Unsafe

让我们来看看compareAndSet(int expect,int update)是如何实现的。

public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

我们可以看到一个特殊的变量unsafe它是sum.misc.Usafe类型,从名字看这是一种不安全的类型。我们知道在C/C++中,指针是不安全的,所以在JAVA中取消了指针的概念,而这里的Usafe类则是封装了一些类似指针的概念。valueOffset是对象内的偏移量,通过这个偏移量可以快速定位到指定的字段expect表示期望值,如果指定的字段值与希望值相等,则设为update


AtomicReference

和AtomicInteger类似,AtomicReference是保证对普通对象引用的线程安全类。之前我说过,CAS存在的一个问题:对象被反复修改回原数据以后,使得之后的线程误以为对象没有被修改。打一个比方:有一家理发店,搞活动,为会员卡里少于20元的顾客免费充值20元,且每人只有一次机会,而顾客获得免费的20元后,洗一次头发需要10元。

public class AtomicReferenceDemo {

    static AtomicReference<Integer> money = new AtomicReference<>();

    public static void main(String[] args) {
        money.set(19);
        for (int i = 0; i < 3; i++) {
            new Thread() {
                public void run() {
                    while (true) {
                        while (true) {
                            Integer m = money.get();
                            if (m < 20) {
                                if (money.compareAndSet(m, m+20)) {
                                    System.out.println("充值20成功,余额:"
                                            + money.get()
                                            + Thread.currentThread().getId());
                                    break;
                                }
                            } else {
                                try {
                                    TimeUnit.SECONDS.sleep(3);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println("不需要充值"+money.get());
                                break;
                            }
                        }
                    }
                }
            }.start();
        }
        new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    while (true) {
                        Integer m = money.get();
                        if (m > 10) {
                            System.out.println("大于10元");
                            if (money.compareAndSet(m, m - 10)) {
                                System.out.println("消费10元,余额"+money.get());
                                break;
                            }
                        } else {
                            System.out.println("钱不够");
                            break;
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}
/*
output:
充值20成功,余额:39
大于10元
消费10元,余额29
大于10元
消费10元,余额19
大于10元
消费10元,余额9
不需要充值9
充值20成功,余额:29
......
*/

我们使用AtomicReference来表示钱,先使得余额只有19,我们用了三台充钱机器,然后对其进行充值,但是当顾客消费了两次以后,余额又回到了19元,其他的充钱器误以为这张卡还没有被充值过,于是又充值了一次。就如输出所看到的。如何解决这个问题呢?答案是:AtomicStampedReference

AtomicStampedReference

AtomicReference出现上面问题的根本原因是对象在修改过程中丢失了状态信息,简单的认为数据值没改变,对象也没有被改变。于是,只要我们能记录下对象在修改中的状态值就能解决上述问题了,AtomicStampedReference就是这样做的。

public class AtomicReferenceDemo {

    //static AtomicReference<Integer> money = new AtomicReference<>();
    static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19,0);

    public static void main(String[] args) {
        //money.set(19);
        for (int i = 0; i < 3; i++) {
            //获得时间戳
            final int stamp = money.getStamp();
            new Thread() {
                public void run() {
                    while (true) {
                        while (true) {
                            Integer m = money.getReference();
                            if (m < 20) {
                                //带有时间戳的CAS
                                if (money.compareAndSet(m, m+20,stamp,stamp+1)) {
                                    System.out.println("充值20成功,余额:"
                                            + money.getReference()
                                            + Thread.currentThread().getId());
                                    break;
                                }
                            } else {
                                try {
                                    TimeUnit.SECONDS.sleep(3);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println("不需要充值");
                                break;
                            }
                        }
                    }
                }
            }.start();
        }
        new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    while (true) {
                        //获得时间戳
                        final int stamp = money.getStamp();
                        Integer m = money.getReference();
                        if (m > 10) {
                            System.out.println("大于10元");
                            ////带有时间戳的CAS
                            if (money.compareAndSet(m, m - 10,stamp,stamp+1)) {
                                System.out.println("消费10元,余额"+money.getReference());
                                break;
                            }
                        } else {
                            System.out.println("钱不够");
                            break;
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}
/*
output:
充值20成功,余额:39
大于10元
消费10元,余额29
大于10元
消费10元,余额19
大于10元
消费10元,余额9
不需要充值
不需要充值
不需要充值
钱不够
*/

使用了AtomicStampedReference代替AtomicReference,从输出可以看出,会员卡只充值一次。

这里写图片描述


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值