[并发] 3 线程安全性-原子性

2019-11-03  本文已影响0人  LZhan

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

1.线程安全的三个特性

原子性:互斥访问
可见性:线程对主内存的修改可以及时被其他行程观察到
有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

2.Atomic包和CAS理论

原子性:Atomic包
Atomic类的incrementAndGet()方法,

image.png
Unsafe类的源码:
image.png

解析:这里的三个参数,Object var1是指当前对象,是需要修改的类对象,即调用increamentAndGet方法的对象;
long var2 是指需要修改的字段的内存地址;int var4是要加上的值;var5是修改前字段的值,var5+var4是修改后字段的值。

var5在没有其他线程处理的情况下,值应该就是var2,所以在while中,当var2和var5相同时,才会执行更新var5+var4,否则的话,就去执行do里面,获取底层最新的数据作为var5。

=====》这就是CAS(Compare And Swap)。

CAS的缺点:
<1> 循环时间长,开销很大:
在执行getAndAddInt方法时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
<2> 只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
<3> ABA问题:
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

3.AtomicLong和LongAdder

https://blog.csdn.net/codingtu/article/details/89047291

LongAdder的increase()方法:

 public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        //第一个if进行了两个判断,(1)如果cells不为空,则直接进入第二个if语句中。
        //(2)同样会先使用cas指令来尝试add,如果成功则直接返回。如果失败则说明存在竞争,需要重新add
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

而这一句a = as[getProbe() & m]其实就是通过getProbe()拿到当前Thread的threadLocalRandomProbe的probe Hash值。这个值其实是一个随机值,这个随机值由当前线程ThreadLocalRandom.current()产生。不用Rondom的原因是因为这里已经是高并发了,多线程情况下Rondom会极大可能得到同一个随机值。因此这里使用threadLocalRandomProbe在高并发时会更加随机,减少冲突。

这里使用到了Cell类对象,Cell对象是LongAdder高并发实现的关键。在casBase冲突严重的时候,就会去创建Cell对象并添加到cells中。

@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        //提供CAS方法修改当前Cell对象上的value
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

总结:在并发处理上,AtomicLong和LongAdder均具有各自优势,需要怎么使用还是得看使用场景。看完这篇文章,其实并不意味着LongAdder就一定比AtomicLong好使,个人认为在QPS统计等统计操作上,LongAdder会更加适合,而AtomicLong在自增控制方面是LongAdder无法代替的。在多数地并发和少数高并发情况下,AtomicLong和LongAdder性能上差异并不是很大,只有在并发极高的时候,才能真正体现LongAdder的优势。

4. AtomicReference和AtomicReferenceFieldUpdater
5.原子性 Synchronized

<1> 使用方法:

<2> 代码示例
1.

@Slf4j
public class SynchronizedExample1 {

    //模拟情况1
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {}- {}", j, i);
            }
        }
    }

    //模拟情况2
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {

        SynchronizedExample1 example1 = new SynchronizedExample1();
        //SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        //线程池开启线程
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            //example2.test1(2);
            example1.test1(1);
        });

    }

}

说明:
这是两个线程,执行同1个对象的synchronized修饰的代码块,符合<1>中的第1种情况,即
线程A执行了某对象的synchronized修饰的代码块,在未结束前,线程B执行到某方法的synchronized修饰的代码块时,无法调用,需等待。
结果:

test1 1- 1
test1 1- 2
test1 1- 3
test1 1- 4
test1 1- 5
test1 1- 6
test1 1- 7
test1 1- 8
test1 1- 9
test1 1- 0
test1 1- 1
test1 1- 2
test1 1- 3
test1 1- 4
test1 1- 5
test1 1- 6
test1 1- 7
test1 1- 8
test1 1- 9

此时,调用test2方法,也是相同的效果。

2.

@Slf4j
public class SynchronizedExample1 {

    //模拟情况1
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {}- {}", j, i);
            }
        }
    }

    //模拟情况2
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {

        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        //线程池开启线程
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example2.test1(2);
        });
    }
}

说明:
这是两个独立的线程,分别调用两个不同对象的synchronized修饰的方法,符合<1>中的第2种情况。
结果是互不影响,交替执行,即

test1 1- 0
test1 2- 0
test1 1- 1
test1 2- 1
test1 1- 2
test1 2- 2
test1 1- 3
test1 2- 3
test1 1- 4
test1 2- 4
test1 1- 5
test1 2- 5
test1 1- 6
test1 2- 6
test1 1- 7
test1 2- 7
test1 1- 8
test1 2- 8
test1 2- 9
test1 1- 9

此时,调用test2方法,也是相同的效果。并不需要等某个线程结束后,才能执行下一个线程,因为两个线程调用的是不同的对象。

另外,通过1、2可证明,如果一个方法内,synchronized修饰的是完整的代码块,那么效果与用synchronized修饰整个方法是一致的。

3.

@Slf4j
public class SynchronizedExample2 {

    //模拟情况3,修饰1个类
    public void test1(int j) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {}- {}", j, i);
            }
        }
    }

    //模拟情况4,修饰1个静态方法
    public static synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {

        SynchronizedExample2 example1 = new SynchronizedExample2();
        SynchronizedExample2 example2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        //线程池开启线程
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            //example2.test1(2);
            example2.test2(2);
        });

    }
}

说明:
模拟的是synchronized修饰的静态方法,这个时候作用于所有对象,所以即使同一时间有两个独立的线程调用两个不同的对象的该方法,那么也得等某个线程执行完,才能执行下一个线程(虽然是不同的对象,也得等执行完)。
结果:

test2 1 - 0
test2 1 - 1
test2 1 - 2
test2 1 - 3
test2 1 - 4
test2 1 - 5
test2 1 - 6
test2 1 - 7
test2 1 - 8
test2 1 - 9
test2 2 - 0
test2 2 - 1
test2 2 - 2
test2 2 - 3
test2 2 - 4
test2 2 - 5
test2 2 - 6
test2 2 - 7
test2 2 - 8
test2 2 - 9

换成执行test1,也是相同的效果。用synchronized修饰某一个类,也是作用于所有对象。

<3> 计数问题:
使用synchronized解决计数问题,这个时候计数不用AtomicInteger,用int也能保证计数的正确性,只要对add()方法,加上synchronized和static关键字,这个时候作用于所有对象。

/**
 * 使用synchronized解决计数问题
 * Created by 凌战 on 2019/11/3
 */
@Slf4j
@ThreadSafe
public class CountExample3 {

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

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


    public static int count=0;

    public static void main(String[] args) throws InterruptedException {
        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);
    }

    private static synchronized void add(){
        count++;
    }

}

6.总结:原子性对比

synchronized:不可中断锁,适合竞争不激烈,可读性好;
Lock:可中断锁,多样化同步,竞争激烈时能维持常态;
Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值。

上一篇下一篇

猜你喜欢

热点阅读