美文网首页
[并发] 3 线程安全性-原子性

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

作者: LZhan | 来源:发表于2019-11-03 16:19 被阅读0次

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

    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> 使用方法:

    • 1.修饰代码块:大括号括起来的代码,作用于调用的对象
    • 2.修饰方法:整个方法,作用于调用的对象
    • 3.修饰静态方法:整个静态方法,作用于所有对象
    • 4.修饰类,括号括起来的代码,作用于所有对象

    <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性能好;只能同步一个值。

    相关文章

      网友评论

          本文标题:[并发] 3 线程安全性-原子性

          本文链接:https://www.haomeiwen.com/subject/dtwavctx.html