java并发与高并发学习系列2:线程安全性
网站首页 文章专栏 java并发与高并发学习系列2:线程安全性
java并发与高并发学习系列2:线程安全性
编辑时间:2019-05-06 18:07 作者:毛毛小妖 浏览量:161 评论数:0

一、定义

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

二、线程安全性的体现 

1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

下面具体来说明这三个特性。

1>原子性

原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

1.1 、atomic

Jdk里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。

它们是通过CAS完成原子性。

(1)、AtomicInteger

public class CountExample2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}

看下执行结果

可以执行看到最后结果是5000是线程安全的。

那么看AtomicInteger的incrementAndGet()方法:

再看getAndAddInt()方法:

这里面调用了compareAndSwapInt()方法:

它是native修饰的,代表是java底层的方法,不是通过java实现的 。

再重新看getAndAddInt(),传来第一个值是当前的一个对象 ,比如是count.incrementAndGet(),那么在getAndAddInt()中,var1就是count,而var2第二个值是当前的值,比如想执行的是2+1=3操作,那么第二个参数是2,第三个参数是1 。

变量5(var5)是我们调用底层的方法而得到的底层当前的值,如果没有别的线程过来处理我们count变量的时候,那么它正常返回值是2。

因此传到compareAndSwapInt方法里的参数是(count对象,当前值2,当前从底层传过来的2,从底层取出来的值加上改变量var4)。

compareAndSwapInt()希望达到的目标是对于var1对象,如果当前的值var2和底层的值var5相等,那么把它更新成后面的值(var5+var4).

compareAndSwapInt核心就是CAS核心。

关于count值为什么和底层值不一样:count里面的值相当于存在于工作内存的值,底层就是主内存。

(2)、AtomicLong

public class AtomicExample2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static AtomicLong count = new AtomicLong(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}

在jdk1.8种,这个方法已经被LongAdder替代了,这是一个小小的区别。那为什么jdk1.8要这么干呢,新增一个类肯定是有他的优点的。在高并发的时候尽量使用LongAdder,当然了在兵法很小的情况下使用AtomicLong效率会高一点。

(3)、AtomicReference-----关于对变量的原子更新

(4)AtomicIntegerFieldUpdater------基于反射的原子更新字段的值。

对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:

(1)字段必须是volatile类型的,在线程之间共享变量时保证立即可见.eg:volatile int value = 3

(2)字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

(3)只能是实例变量,不能是类变量,也就是说不能加static关键字。

(4)只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。

(5)对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

public class AtomicExample5 {

    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");

    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {

        AtomicExample5 example5 = new AtomicExample5();

        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 1, {}", example5.getCount());
        }

        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 2, {}", example5.getCount());
        } else {
            log.info("update failed, {}", example5.getCount());
        }
    }
}
(5)AtomicStampedReference

关于CAS有一个ABA问题:开始是A,后来改为B,现在又改为A。解决办法就是:每次变量改变的时候,把变量的版本号加1。

这就用到了AtomicStampedReference。

我们来看AtomicStampedReference里的compareAndSet()实现:

而在AtomicInteger里compareAndSet()实现:

可以看到AtomicStampedReference里的compareAndSet()中多了 一个stamp比较(也就是版本),这个值是由每次更新时来维护的。

(6)、AtomicBoolean

public class AtomicExample6 {

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }

    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }
}

执行之后发现,log.info("execute");只执行了一次,且isHappend值为true。
原因就是当它第一次compareAndSet()之后,isHappend变为true,没有别的线程干扰。
通过使用AtomicBoolean,我们可以使某段代码只执行一次。

1.2 、synchronized

在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。所以synchronized关键字也可以用来实现线程同步,主要用来修饰以下四个地方:
修饰代码块:大括号括起来的代码,作用于调用的对象
修饰方法:整个方法、作用于调用的对象
修饰静态方法:整个静态方法、作用于所有对象

修饰类:大括号括起来的代码,作用于所有对象

2>可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。导致共享变量在线程间不可见的原因:
a、线程交叉执行
b、重排序结合线程交叉执行
c、共享变量更新后的值没有在主内存和工作内存中及时更新

2.1、synchronized

JVM中关于synchronized的两条规定:
a、线程解锁前,必须把共享变量的最新值刷新到主内存
b、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

从而,synchronized具有可见性。

2.2、volatile

通过加入内存屏障和禁止重排序优化来实现
a、对volatile变量进行写操作时,会在写操作后加一条store屏障指令,将本地内存中的共享变量的值刷新到主内存中
b、对volatile变量进行读操作时,会在读操作前加一条load指令,从主内存中读取共享变量

从而, volatile具有可见性。

3>有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

3.1、synchronized

synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性

3.2、volatile

在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的

三、总结

*synchronized: 具有原子性,有序性和可见性;
*volatile:具有有序性和可见性

*volatile常用场景:
1.状态标记量​​​​​​
2.double check

 

推荐文章
来说两句吧
最新评论
    还没有人评论哦,快来坐沙发吧!